diff --git a/src/fb-cpp/Descriptor.h b/src/fb-cpp/Descriptor.h index ed7f500..ca3c3f4 100644 --- a/src/fb-cpp/Descriptor.h +++ b/src/fb-cpp/Descriptor.h @@ -34,6 +34,11 @@ /// namespace fbcpp { + /// + /// Firebird character set identifier for OCTETS. + /// + inline constexpr unsigned octetsCharSetId = 1u; + /// /// Descriptor original type. /// diff --git a/src/fb-cpp/Row.h b/src/fb-cpp/Row.h index 9da9902..0e27182 100644 --- a/src/fb-cpp/Row.h +++ b/src/fb-cpp/Row.h @@ -634,6 +634,31 @@ namespace fbcpp } } + /// + /// @brief Reads a text or varying column as its raw bytes. + /// + std::optional> getBytes(unsigned index) + { + const auto& descriptor = getDescriptor(index); + + if (*reinterpret_cast(&message[descriptor.nullOffset]) != FB_FALSE) + return std::nullopt; + + const auto data = &message[descriptor.offset]; + + switch (descriptor.adjustedType) + { + case DescriptorAdjustedType::STRING: + { + const auto length = *reinterpret_cast(data); + return std::vector{data + sizeof(std::uint16_t), data + sizeof(std::uint16_t) + length}; + } + + default: + throwInvalidType("std::vector", descriptor.adjustedType); + } + } + /// /// @} /// @@ -871,8 +896,20 @@ namespace fbcpp break; case DescriptorAdjustedType::STRING: - if constexpr (variantContainsV) - return V{get>(index).value()}; + if (descriptor.charSetId == octetsCharSetId) + { + if constexpr (variantContainsV, V>) + return V{get>>(index).value()}; + if constexpr (variantContainsV) + return V{get>(index).value()}; + } + else + { + if constexpr (variantContainsV) + return V{get>(index).value()}; + if constexpr (variantContainsV, V>) + return V{get>>(index).value()}; + } break; case DescriptorAdjustedType::DATE: @@ -1300,6 +1337,12 @@ namespace fbcpp return getString(index); } + template <> + inline std::optional> Row::get>>(unsigned index) + { + return getBytes(index); + } + /// /// @} /// diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index 3141bea..567eb3a 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -1249,6 +1250,72 @@ namespace fbcpp *reinterpret_cast(&message[descriptor.nullOffset]) = FB_FALSE; } + /// + /// @brief Binds raw bytes to a text or varying parameter. + /// + void setBytes(unsigned index, std::span value) + { + assert(isValid()); + + const auto& descriptor = getInDescriptor(index); + auto* const message = inMessage.data(); + + switch (descriptor.adjustedType) + { + case DescriptorAdjustedType::STRING: + if (value.size() > descriptor.length) + { + static constexpr std::intptr_t STATUS_STRING_TRUNCATION[] = { + isc_arith_except, + isc_string_truncation, + isc_arg_end, + }; + + throw DatabaseException(getClient(), STATUS_STRING_TRUNCATION); + } + + *reinterpret_cast(&message[descriptor.offset]) = + static_cast(value.size()); + std::copy(value.begin(), value.end(), &message[descriptor.offset + sizeof(std::uint16_t)]); + break; + + default: + throwInvalidType("std::span", descriptor.adjustedType); + } + + *reinterpret_cast(&message[descriptor.nullOffset]) = FB_FALSE; + } + + /// + /// @brief Binds a vector of raw bytes to a text or varying parameter. + /// + void setBytes(unsigned index, const std::vector& value) + { + setBytes(index, std::span{value}); + } + + /// + /// @brief Binds an optional vector of raw bytes to a text or varying parameter. + /// + void setBytes(unsigned index, std::optional> optValue) + { + if (!optValue.has_value()) + { + setNull(index); + return; + } + + setBytes(index, std::span{optValue.value()}); + } + + /// + /// @brief Binds a null byte value. + /// + void setBytes(unsigned index, std::nullopt_t) + { + setNull(index); + } + /// /// @brief Binds a blob identifier to the specified parameter or null. /// @@ -1560,6 +1627,22 @@ namespace fbcpp setString(index, value); } + /// + /// @brief Convenience overload that binds a vector of raw bytes. + /// + void set(unsigned index, const std::vector& value) + { + setBytes(index, value); + } + + /// + /// @brief Convenience overload that binds a span of raw bytes. + /// + void set(unsigned index, std::span value) + { + setBytes(index, value); + } + /// /// @brief Convenience template that forwards optional values to specialized overloads. /// @@ -1878,6 +1961,15 @@ namespace fbcpp return outRow->getString(index); } + /// + /// @brief Reads a text or varying column as its raw bytes. + /// + std::optional> getBytes(unsigned index) + { + assert(isValid()); + return outRow->getBytes(index); + } + /// /// @} /// @@ -2498,6 +2590,12 @@ namespace fbcpp return getString(index); } + template <> + inline std::optional> Statement::get>>(unsigned index) + { + return getBytes(index); + } + /// /// @} /// diff --git a/src/fb-cpp/VariantTypeTraits.h b/src/fb-cpp/VariantTypeTraits.h index 899ad4e..4db1b3a 100644 --- a/src/fb-cpp/VariantTypeTraits.h +++ b/src/fb-cpp/VariantTypeTraits.h @@ -28,6 +28,8 @@ #include "config.h" #include "types.h" #include "StructBinding.h" +#include +#include #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 #include @@ -95,6 +97,12 @@ namespace fbcpp::impl::reflection { }; + // Bytes + template <> + struct IsSupportedVariantType> : std::true_type + { + }; + // Date/Time types template <> struct IsSupportedVariantType : std::true_type @@ -267,6 +275,11 @@ namespace fbcpp::impl::reflection struct IsOpaqueType : std::true_type { }; + + template <> + struct IsOpaqueType> : std::true_type + { + }; } // namespace fbcpp::impl::reflection diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index 9393258..8512cc2 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -28,7 +28,9 @@ #include "fb-cpp/RowSet.h" #include "fb-cpp/Statement.h" #include "fb-cpp/Transaction.h" +#include #include +#include BOOST_AUTO_TEST_SUITE(AttachmentSuite) @@ -266,6 +268,39 @@ BOOST_AUTO_TEST_CASE(queryScalarReturnsFirstColumnOfFirstRow) transaction.commit(); } +BOOST_AUTO_TEST_CASE(queryScalarReturnsBytes) +{ + const auto database = getTempFile("Attachment-queryScalarReturnsBytes.fdb"); + Attachment attachment{ + getClient(), database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = attachment.queryScalar>( + transaction, "select cast('attachment bytes' as varchar(32) character set octets) from rdb$database"); + + const std::vector expected{ + std::byte{'a'}, + std::byte{'t'}, + std::byte{'t'}, + std::byte{'a'}, + std::byte{'c'}, + std::byte{'h'}, + std::byte{'m'}, + std::byte{'e'}, + std::byte{'n'}, + std::byte{'t'}, + std::byte{' '}, + std::byte{'b'}, + std::byte{'y'}, + std::byte{'t'}, + std::byte{'e'}, + std::byte{'s'}, + }; + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK(*value == expected); +} + BOOST_AUTO_TEST_CASE(queryScalarReturnsNulloptForNoRows) { const auto database = getTempFile("Attachment-queryScalarReturnsNulloptForNoRows.fdb"); diff --git a/src/test/RowSet.cpp b/src/test/RowSet.cpp index 34dccc7..b540acd 100644 --- a/src/test/RowSet.cpp +++ b/src/test/RowSet.cpp @@ -26,6 +26,8 @@ #include "fb-cpp/RowSet.h" #include "fb-cpp/Statement.h" #include "fb-cpp/Transaction.h" +#include +#include BOOST_AUTO_TEST_SUITE(RowSetSuite) @@ -337,4 +339,39 @@ BOOST_AUTO_TEST_CASE(includesCurrentRowAndDoesNotDuplicateAcrossUses) BOOST_CHECK_EQUAL(procedureRowSet.getRow(0).getInt32(0).value(), 42); } +BOOST_AUTO_TEST_CASE(readBytesFromDisconnectedRowSet) +{ + const auto database = getTempFile("RowSet-readBytesFromDisconnectedRowSet.fdb"); + + Attachment attachment{ + getClient(), database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement select{ + attachment, transaction, "select cast('row bytes' as varchar(16) character set octets) from rdb$database"}; + BOOST_REQUIRE(select.execute(transaction)); + + RowSet rowSet{select, 1u}; + select.free(); + + const std::vector expected{ + std::byte{'r'}, + std::byte{'o'}, + std::byte{'w'}, + std::byte{' '}, + std::byte{'b'}, + std::byte{'y'}, + std::byte{'t'}, + std::byte{'e'}, + std::byte{'s'}, + }; + const auto result = rowSet.getRow(0).getBytes(0); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK(*result == expected); + const auto typedResult = rowSet.getRow(0).get>>(0); + BOOST_REQUIRE(typedResult.has_value()); + BOOST_CHECK(*typedResult == expected); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/Statement.cpp b/src/test/Statement.cpp index 5481309..38ae4e0 100644 --- a/src/test/Statement.cpp +++ b/src/test/Statement.cpp @@ -28,8 +28,10 @@ #include "fb-cpp/Statement.h" #include "fb-cpp/Transaction.h" #include +#include #include #include +#include BOOST_AUTO_TEST_SUITE(StatementLifecycleSuite) @@ -1199,6 +1201,81 @@ BOOST_AUTO_TEST_CASE(stringTruncationThrows) BOOST_CHECK_THROW(stmt.setString(0, "This is too long"), DatabaseException); } +BOOST_AUTO_TEST_CASE(bytesRoundTripForTextAndVarying) +{ + const auto database = getTempFile("Statement-bytesRoundTripForTextAndVarying.fdb"); + + Attachment attachment{getClient(), database, + AttachmentOptions().setCreateDatabase(true).setForcedWrites(false).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const std::vector expected{ + std::byte{0x00}, + std::byte{0x01}, + std::byte{0x7f}, + std::byte{0x80}, + std::byte{0xfe}, + std::byte{0xff}, + }; + + Statement varying{attachment, transaction, "select cast(? as varchar(6) character set octets) from rdb$database"}; + varying.setBytes(0, expected); + BOOST_REQUIRE(varying.execute(transaction)); + const auto varyingResult = varying.getBytes(0); + BOOST_REQUIRE(varyingResult.has_value()); + BOOST_CHECK(*varyingResult == expected); + + Statement text{attachment, transaction, "select cast(? as char(6) character set octets) from rdb$database"}; + text.set(0, expected); + BOOST_REQUIRE(text.execute(transaction)); + const auto textResult = text.get>>(0); + BOOST_REQUIRE(textResult.has_value()); + BOOST_CHECK(*textResult == expected); +} + +BOOST_AUTO_TEST_CASE(bytesDistinguishEmptyAndNull) +{ + const auto database = getTempFile("Statement-bytesDistinguishEmptyAndNull.fdb"); + + Attachment attachment{getClient(), database, + AttachmentOptions().setCreateDatabase(true).setForcedWrites(false).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select cast(? as varchar(6) character set none) from rdb$database"}; + + stmt.setBytes(0, std::vector{}); + BOOST_REQUIRE(stmt.execute(transaction)); + BOOST_CHECK(!stmt.isNull(0)); + const auto emptyResult = stmt.getBytes(0); + BOOST_REQUIRE(emptyResult.has_value()); + BOOST_CHECK(emptyResult->empty()); + + stmt.setBytes(0, std::nullopt); + BOOST_REQUIRE(stmt.execute(transaction)); + BOOST_CHECK(stmt.isNull(0)); + BOOST_CHECK(!stmt.getBytes(0).has_value()); +} + +BOOST_AUTO_TEST_CASE(bytesValidateTypeAndLength) +{ + const auto database = getTempFile("Statement-bytesValidateTypeAndLength.fdb"); + + Attachment attachment{getClient(), database, + AttachmentOptions().setCreateDatabase(true).setForcedWrites(false).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const std::vector value(16, std::byte{0x01}); + + Statement truncating{attachment, transaction, "select cast(? as varchar(3)) from rdb$database"}; + BOOST_CHECK_THROW(truncating.setBytes(0, value), DatabaseException); + + Statement wrongType{attachment, transaction, "select cast(? as integer) from rdb$database"}; + BOOST_CHECK_THROW(wrongType.setBytes(0, value), FbCppException); +} + BOOST_AUTO_TEST_SUITE_END() @@ -3101,6 +3178,50 @@ BOOST_AUTO_TEST_CASE(setStructWithOptionalNull) BOOST_CHECK(!stmt.getString(1).has_value()); } +BOOST_AUTO_TEST_CASE(bytesSupportInStructAndTupleBinding) +{ + using ByteVector = std::vector; + + struct Params + { + ByteVector first; + ByteVector second; + }; + + struct Result + { + ByteVector first; + std::optional second; + }; + + using ResultTuple = std::tuple; + + const auto database = getTempFile("Statement-bytesSupportInStructAndTupleBinding.fdb"); + Attachment attachment{getClient(), database, + AttachmentOptions().setCreateDatabase(true).setForcedWrites(false).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const ByteVector first{std::byte{0x00}, std::byte{0x11}, std::byte{0xff}}; + const ByteVector second{std::byte{0x22}, std::byte{0x33}}; + Statement stmt{attachment, transaction, + "select cast(? as varchar(8) character set octets), cast(? as varchar(8) character set octets) from " + "rdb$database"}; + + stmt.set(Params{first, second}); + BOOST_REQUIRE(stmt.execute(transaction)); + const auto structResult = stmt.get(); + BOOST_CHECK(structResult.first == first); + BOOST_REQUIRE(structResult.second.has_value()); + BOOST_CHECK(*structResult.second == second); + + stmt.set(std::tuple{first, second}); + BOOST_REQUIRE(stmt.execute(transaction)); + const auto tupleResult = stmt.get(); + BOOST_CHECK(std::get<0>(tupleResult) == first); + BOOST_CHECK(std::get<1>(tupleResult) == second); +} + BOOST_AUTO_TEST_SUITE_END() @@ -3313,6 +3434,49 @@ BOOST_AUTO_TEST_CASE(getVariantExactMatchString) BOOST_CHECK_EQUAL(std::get(result), "hello"); } +BOOST_AUTO_TEST_CASE(getVariantPrefersBytesForOctets) +{ + using ByteVector = std::vector; + using Both = std::variant; + + const auto database = getTempFile("Statement-getVariantPrefersBytesForOctets.fdb"); + Attachment attachment{getClient(), database, + AttachmentOptions().setCreateDatabase(true).setForcedWrites(false).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement octets{ + attachment, transaction, "select cast('octets' as varchar(10) character set octets) from rdb$database"}; + BOOST_REQUIRE(octets.execute(transaction)); + const auto octetsResult = octets.get(0); + BOOST_REQUIRE(std::holds_alternative(octetsResult)); + const ByteVector expected{ + std::byte{'o'}, + std::byte{'c'}, + std::byte{'t'}, + std::byte{'e'}, + std::byte{'t'}, + std::byte{'s'}, + }; + BOOST_CHECK(std::get(octetsResult) == expected); + + Statement ordinary{ + attachment, transaction, "select cast('text' as varchar(10) character set none) from rdb$database"}; + BOOST_REQUIRE(ordinary.execute(transaction)); + const auto ordinaryResult = ordinary.get(0); + BOOST_REQUIRE(std::holds_alternative(ordinaryResult)); + BOOST_CHECK_EQUAL(std::get(ordinaryResult), "text"); + + Statement byteParameter{ + attachment, transaction, "select cast(? as varchar(10) character set none) from rdb$database"}; + const ByteVector parameterValue{std::byte{0x00}, std::byte{0x01}}; + byteParameter.set(0, std::variant{parameterValue}); + BOOST_REQUIRE(byteParameter.execute(transaction)); + const auto parameterResult = byteParameter.get>(0); + BOOST_REQUIRE(parameterResult.has_value()); + BOOST_CHECK(*parameterResult == parameterValue); +} + BOOST_AUTO_TEST_CASE(getVariantScaledIntPreferred) { using MyVariant = std::variant;