diff --git a/src/fb-cpp/NumericConverter.h b/src/fb-cpp/NumericConverter.h index ca0a627..dfb12dc 100644 --- a/src/fb-cpp/NumericConverter.h +++ b/src/fb-cpp/NumericConverter.h @@ -155,7 +155,9 @@ namespace fbcpp::impl [[noreturn]] void throwNumericOutOfRange() { static constexpr std::intptr_t STATUS_NUMERIC_OUT_OF_RANGE[] = { + isc_arg_gds, isc_arith_except, + isc_arg_gds, isc_numeric_out_of_range, isc_arg_end, }; @@ -166,7 +168,9 @@ namespace fbcpp::impl [[noreturn]] void throwConversionErrorFromString(const std::string& str) { const std::intptr_t STATUS_CONVERSION_ERROR_FROM_STRING[] = { + isc_arg_gds, isc_convert_error, + isc_arg_string, reinterpret_cast(str.c_str()), isc_arg_end, }; @@ -209,14 +213,21 @@ namespace fbcpp::impl if (std::isnan(from) || std::isinf(from)) throwNumericOutOfRange(); } +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + else if constexpr (std::same_as || std::same_as) + { + if ((boost::multiprecision::isnan) (from) || (boost::multiprecision::isinf) (from)) + throwNumericOutOfRange(); + } +#endif ComputeType value{from}; const ComputeType eps = conversionEpsilon(); if (toScale > 0) - value /= powerOfTen(toScale); + value /= powerOfTen(toScale); else if (toScale < 0) - value *= powerOfTen(-toScale); + value *= powerOfTen(-toScale); if (value > 0) value += 0.5f + eps; @@ -258,9 +269,9 @@ namespace fbcpp::impl throwNumericOutOfRange(); if (from.scale > 0) - value *= powerOfTen(from.scale); + value *= powerOfTen(from.scale); else if (from.scale < 0) - value /= powerOfTen(-from.scale); + value /= powerOfTen(-from.scale); } return static_cast(value); @@ -271,10 +282,15 @@ namespace fbcpp::impl { assert(toScale == 0); +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + if constexpr (std::same_as && std::same_as) + return boostDecFloat34ToBoostDecFloat16(from); +#endif + if constexpr (std::is_floating_point_v && !std::is_floating_point_v) return To{std::format("{:.16e}", from)}; - else - return static_cast(from); + + return static_cast(from); } template @@ -349,6 +365,16 @@ namespace fbcpp::impl return from > 0 ? "Infinity" : "-Infinity"; return std::to_string(from); } +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + else if constexpr (std::same_as || std::same_as) + { + if ((boost::multiprecision::isnan) (from)) + return "NaN"; + if ((boost::multiprecision::isinf) (from)) + return from > 0 ? "Infinity" : "-Infinity"; + return from.str(); + } +#endif else return from.str(); } @@ -378,24 +404,32 @@ namespace fbcpp::impl } #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 - OpaqueInt128 boostInt128ToOpaqueInt128(const BoostInt128& boostInt128) + OpaqueInt128 boostInt128ToOpaqueInt128(StatusWrapper* statusWrapper, const BoostInt128& boostInt128) { - const boost::multiprecision::uint128_t boostUInt128{boostInt128}; + validateFirebirdInt128(boostInt128); OpaqueInt128 opaqueInt128; - opaqueInt128.fb_data[0] = static_cast(boostUInt128 & 0xFFFFFFFFFFFFFFFFULL); - opaqueInt128.fb_data[1] = static_cast(boostUInt128 >> 64); + const auto value = boostInt128.str(); + client->getInt128Util(statusWrapper)->fromString(statusWrapper, 0, value.c_str(), &opaqueInt128); return opaqueInt128; } + OpaqueInt128 boostInt128ToOpaqueInt128(const BoostInt128& boostInt128) + { + StatusWrapper status{*client}; + return boostInt128ToOpaqueInt128(&status, boostInt128); + } + + BoostInt128 opaqueInt128ToBoostInt128(StatusWrapper* statusWrapper, const OpaqueInt128& opaqueInt128) + { + return BoostInt128{opaqueInt128ToString(statusWrapper, opaqueInt128, 0)}; + } + BoostInt128 opaqueInt128ToBoostInt128(const OpaqueInt128& opaqueInt128) { - const auto high = static_cast(opaqueInt128.fb_data[1]); - BoostInt128 boostInt128 = static_cast(high); - boostInt128 <<= 64; - boostInt128 += static_cast(opaqueInt128.fb_data[0]); - return boostInt128; + StatusWrapper status{*client}; + return opaqueInt128ToBoostInt128(&status, opaqueInt128); } OpaqueDecFloat16 boostDecFloat16ToOpaqueDecFloat16( @@ -403,14 +437,31 @@ namespace fbcpp::impl { const auto decFloat16Util = client->getDecFloat16Util(statusWrapper); OpaqueDecFloat16 opaqueDecFloat16; - decFloat16Util->fromString(statusWrapper, boostDecFloat16.str().c_str(), &opaqueDecFloat16); + const auto value = numberToString(boostDecFloat16); + decFloat16Util->fromString(statusWrapper, value.c_str(), &opaqueDecFloat16); return opaqueDecFloat16; } BoostDecFloat16 opaqueDecFloat16ToBoostDecFloat16( StatusWrapper* statusWrapper, const OpaqueDecFloat16& opaqueDecFloat16) { - return BoostDecFloat16{opaqueDecFloat16ToString(statusWrapper, opaqueDecFloat16)}; + const auto value = opaqueDecFloat16ToString(statusWrapper, opaqueDecFloat16); + + if (isSignalingNaN(value)) + throw FbCppException("BoostDecFloat16 cannot represent a signaling NaN"); + else if (value == "Infinity") + return std::numeric_limits::infinity(); + else if (value == "-Infinity") + return -std::numeric_limits::infinity(); + + try + { + return BoostDecFloat16{value}; + } + catch (const std::exception&) + { + throwConversionErrorFromString(value); + } } OpaqueDecFloat34 boostDecFloat34ToOpaqueDecFloat34( @@ -418,14 +469,40 @@ namespace fbcpp::impl { const auto decFloat34Util = client->getDecFloat34Util(statusWrapper); OpaqueDecFloat34 opaqueDecFloat34; - decFloat34Util->fromString(statusWrapper, boostDecFloat34.str().c_str(), &opaqueDecFloat34); + const auto value = numberToString(boostDecFloat34); + decFloat34Util->fromString(statusWrapper, value.c_str(), &opaqueDecFloat34); return opaqueDecFloat34; } BoostDecFloat34 opaqueDecFloat34ToBoostDecFloat34( StatusWrapper* statusWrapper, const OpaqueDecFloat34& opaqueDecFloat34) { - return BoostDecFloat34{opaqueDecFloat34ToString(statusWrapper, opaqueDecFloat34)}; + const auto value = opaqueDecFloat34ToString(statusWrapper, opaqueDecFloat34); + + if (isSignalingNaN(value)) + throw FbCppException("BoostDecFloat34 cannot represent a signaling NaN; use OpaqueDecFloat34"); + else if (value == "Infinity") + return std::numeric_limits::infinity(); + else if (value == "-Infinity") + return -std::numeric_limits::infinity(); + + try + { + return BoostDecFloat34{value}; + } + catch (const std::exception&) + { + throwConversionErrorFromString(value); + } + } + + BoostDecFloat16 boostDecFloat34ToBoostDecFloat16(const BoostDecFloat34& boostDecFloat34) + { + StatusWrapper status{*client}; + OpaqueDecFloat16 opaqueDecFloat16; + const auto value = numberToString(boostDecFloat34); + client->getDecFloat16Util(&status)->fromString(&status, value.c_str(), &opaqueDecFloat16); + return opaqueDecFloat16ToBoostDecFloat16(&status, opaqueDecFloat16); } #endif @@ -453,15 +530,8 @@ namespace fbcpp::impl } private: - double powerOfTen(int scale) noexcept // FIXME: only for double? + double powerOfTenDouble(int scale) noexcept { - /* FIXME: - BoostDecFloat34 powerOfTenDecInternal(unsigned scale) - { - return boost::multiprecision::pow(BoostDecFloat34{10}, scale); - } - */ - static constexpr double UPPER_PART[] = { 1.e000, 1.e032, @@ -518,6 +588,47 @@ namespace fbcpp::impl return upper * lower; } + template + T powerOfTen(int scale) + { + assert((scale >= 0) && (scale < 320)); + + if constexpr (std::same_as || std::same_as) + return static_cast(powerOfTenDouble(scale)); + else + { + T result{1}; + + for (int i = 0; i < scale; ++i) + result *= 10; + + return result; + } + } + +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + void validateFirebirdInt128(const BoostInt128& value) + { + constexpr BoostInt128 FB_MAX_INT128 = (BoostInt128{1} << 127) - 1; + constexpr BoostInt128 FB_MIN_INT128 = -(BoostInt128{1} << 127); + + if (value < FB_MIN_INT128 || value > FB_MAX_INT128) + throwNumericOutOfRange(); + } + + static bool isSignalingNaN(std::string_view value) + { + std::string normalized{value}; + if (!normalized.empty() && (normalized.front() == '+' || normalized.front() == '-')) + normalized.erase(0, 1); + + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + + return normalized.starts_with("snan"); + } +#endif + template void adjustScale(T& val, int scale, const T minLimit, const T maxLimit) { diff --git a/src/fb-cpp/Row.h b/src/fb-cpp/Row.h index adb5d92..003ef53 100644 --- a/src/fb-cpp/Row.h +++ b/src/fb-cpp/Row.h @@ -785,10 +785,10 @@ namespace fbcpp return V{get>(index).value()}; break; -#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 case DescriptorAdjustedType::INT128: if constexpr (variantContainsV) return V{get>(index).value()}; +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 else if (descriptor.scale != 0) { if constexpr (variantContainsV) @@ -796,8 +796,8 @@ namespace fbcpp } else if constexpr (variantContainsV) return V{get>(index).value()}; - break; #endif + break; case DescriptorAdjustedType::FLOAT: if constexpr (variantContainsV) @@ -809,21 +809,23 @@ namespace fbcpp return V{get>(index).value()}; break; -#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 case DescriptorAdjustedType::DECFLOAT16: if constexpr (variantContainsV) return V{get>(index).value()}; +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 else if constexpr (variantContainsV) return V{get>(index).value()}; +#endif break; case DescriptorAdjustedType::DECFLOAT34: if constexpr (variantContainsV) return V{get>(index).value()}; +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 else if constexpr (variantContainsV) return V{get>(index).value()}; - break; #endif + break; case DescriptorAdjustedType::STRING: if constexpr (variantContainsV) @@ -924,8 +926,8 @@ namespace fbcpp { #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 case DescriptorAdjustedType::INT128: - boostInt128.emplace( - numericConverter.opaqueInt128ToBoostInt128(*reinterpret_cast(data))); + boostInt128.emplace(numericConverter.opaqueInt128ToBoostInt128( + &statusWrapper, *reinterpret_cast(data))); data = reinterpret_cast(&boostInt128.value()); break; diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index d3b4d53..504773e 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -533,6 +533,46 @@ namespace fbcpp *reinterpret_cast(&message[descriptor.nullOffset]) = FB_FALSE; } + /// + /// @brief Binds a scaled 128-bit integer in Firebird's representation or null. + /// + void setScaledOpaqueInt128(unsigned index, std::optional optValue) + { + if (!optValue.has_value()) + { + setNull(index); + return; + } + + assert(isValid()); + + const auto& value = optValue.value(); + const auto& descriptor = getInDescriptor(index); + auto* const message = inMessage.data(); + + switch (descriptor.adjustedType) + { + case DescriptorAdjustedType::INT128: + if (value.scale == descriptor.scale) + *reinterpret_cast(&message[descriptor.offset]) = value.value; + else + { + const auto valueString = + numericConverter.opaqueInt128ToString(&statusWrapper, value.value, value.scale); + getClient() + .getInt128Util(&statusWrapper) + ->fromString(&statusWrapper, descriptor.scale, valueString.c_str(), + reinterpret_cast(&message[descriptor.offset])); + } + break; + + default: + throwInvalidType("ScaledOpaqueInt128", descriptor.adjustedType); + } + + *reinterpret_cast(&message[descriptor.nullOffset]) = FB_FALSE; + } + #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 /// /// @brief Binds a 128-bit integer value expressed with Boost.Multiprecision or null. @@ -1106,7 +1146,6 @@ namespace fbcpp calendarConverter.stringToOpaqueTimestampTz(&statusWrapper, value); break; -#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 case DescriptorAdjustedType::DECFLOAT16: { std::string strValue{value}; @@ -1122,7 +1161,6 @@ namespace fbcpp ->fromString(&statusWrapper, strValue.c_str(), reinterpret_cast(data)); break; } -#endif case DescriptorAdjustedType::STRING: if (value.length() > descriptor.length) @@ -1265,6 +1303,14 @@ namespace fbcpp setOpaqueInt128(index, value); } + /// + /// @brief Convenience overload that binds a scaled Firebird 128-bit integer. + /// + void set(unsigned index, ScaledOpaqueInt128 value) + { + setScaledOpaqueInt128(index, value); + } + #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 /// /// @brief Convenience overload that binds a Boost-provided 128-bit integer. @@ -1914,7 +1960,7 @@ namespace fbcpp const auto boostInt128 = convertNumber(valueDescriptor, valueAddress, descriptorScale, "BoostInt128"); *reinterpret_cast(descriptorData) = - numericConverter.boostInt128ToOpaqueInt128(boostInt128); + numericConverter.boostInt128ToOpaqueInt128(&statusWrapper, boostInt128); break; } #endif diff --git a/src/fb-cpp/VariantTypeTraits.h b/src/fb-cpp/VariantTypeTraits.h index 57bdbf1..abd77d1 100644 --- a/src/fb-cpp/VariantTypeTraits.h +++ b/src/fb-cpp/VariantTypeTraits.h @@ -159,6 +159,7 @@ namespace fbcpp::impl::reflection struct IsSupportedVariantType : std::true_type { }; +#endif // Opaque multiprecision types template <> @@ -173,7 +174,6 @@ namespace fbcpp::impl::reflection struct IsSupportedVariantType : std::true_type { }; -#endif // std::monostate is always allowed for NULL representation template <> diff --git a/src/fb-cpp/types.h b/src/fb-cpp/types.h index 49b000f..e175cdd 100644 --- a/src/fb-cpp/types.h +++ b/src/fb-cpp/types.h @@ -32,6 +32,7 @@ #include #include #include +#include #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 #include @@ -50,7 +51,16 @@ namespace fbcpp template struct ScaledNumber final { - bool operator==(const ScaledNumber&) const noexcept = default; + bool operator==(const ScaledNumber& o) const noexcept + { + if constexpr (std::is_same_v) + { + return value.fb_data[0] == o.value.fb_data[0] && value.fb_data[1] == o.value.fb_data[1] && + scale == o.scale; + } + else + return value == o.value && scale == o.scale; + } /// /// Unscaled numeric value. @@ -324,8 +334,8 @@ struct std::formatter> : std::formatter template auto format(const fbcpp::ScaledNumber& scaledNumber, FormatContext& ctx) const { - return std::format_to( - ctx.out(), "{}e{}", std::formatter::format(scaledNumber.value, ctx), scaledNumber.scale); + const auto valueEnd = std::formatter::format(scaledNumber.value, ctx); + return std::format_to(valueEnd, "e{}", scaledNumber.scale); } }; diff --git a/src/test/NumericConverter.cpp b/src/test/NumericConverter.cpp index 1a4e13b..187c175 100644 --- a/src/test/NumericConverter.cpp +++ b/src/test/NumericConverter.cpp @@ -799,6 +799,43 @@ BOOST_AUTO_TEST_CASE(convertDecFloat34) BOOST_CHECK_EQUAL(converter.numberToString(BoostDecFloat34{"-3.2768"}), "-3.2768"); } +BOOST_AUTO_TEST_CASE(firebirdPrecisionAndSpecialValueBoundaries) +{ + impl::NumericConverter converter{getClient()}; + + const BoostInt128 firebirdMax{"170141183460469231731687303715884105727"}; + const BoostInt128 firebirdMin{"-170141183460469231731687303715884105728"}; + + BOOST_CHECK_EQUAL( + converter.opaqueInt128ToBoostInt128(converter.boostInt128ToOpaqueInt128(firebirdMax)), firebirdMax); + BOOST_CHECK_EQUAL( + converter.opaqueInt128ToBoostInt128(converter.boostInt128ToOpaqueInt128(firebirdMin)), firebirdMin); + + const auto checkNumericOutOfRange = [&converter](const BoostInt128& value) + { + try + { + converter.boostInt128ToOpaqueInt128(value); + } + catch (const DatabaseException& exception) + { + return exception.getErrorCode() == isc_arith_except; + } + + return false; + }; + + BOOST_CHECK(checkNumericOutOfRange(firebirdMax + 1)); + BOOST_CHECK(checkNumericOutOfRange(firebirdMin - 1)); + + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecFloat34{"12345678901234565"}), + BoostDecFloat16{"12345678901234570"}); + BOOST_CHECK_THROW(converter.numberToNumber(std::numeric_limits::quiet_NaN(), 0), + DatabaseException); + BOOST_CHECK_THROW( + converter.numberToNumber(std::numeric_limits::infinity(), 0), DatabaseException); +} + BOOST_AUTO_TEST_CASE(decFloat16NumberLimits) { impl::NumericConverter converter{getClient()}; diff --git a/src/test/Statement.cpp b/src/test/Statement.cpp index 962326a..cf8ac4b 100644 --- a/src/test/Statement.cpp +++ b/src/test/Statement.cpp @@ -1706,6 +1706,33 @@ BOOST_AUTO_TEST_CASE(setBoostInt128ToInt128) BOOST_CHECK(result.value() == testValue); } +BOOST_AUTO_TEST_CASE(boostInt128BoundaryValues) +{ + const auto database = getTempFile("Statement-boostInt128BoundaryValues.fdb"); + + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + const BoostInt128 firebirdMax{"170141183460469231731687303715884105727"}; + const BoostInt128 firebirdMin{"-170141183460469231731687303715884105728"}; + + Statement minValue{attachment, transaction, "select cast(? as int128) from rdb$database"}; + minValue.setBoostInt128(0, firebirdMin); + BOOST_REQUIRE(minValue.execute(transaction)); + BOOST_CHECK_EQUAL(minValue.getBoostInt128(0).value(), firebirdMin); + + Statement maxValue{attachment, transaction, "select cast(? as int128) from rdb$database"}; + maxValue.setBoostInt128(0, firebirdMax); + BOOST_REQUIRE(maxValue.execute(transaction)); + BOOST_CHECK_EQUAL(maxValue.getBoostInt128(0).value(), firebirdMax); + + Statement outOfRange{attachment, transaction, "select cast(? as int128) from rdb$database"}; + BOOST_CHECK_THROW(outOfRange.setBoostInt128(0, firebirdMax + 1), DatabaseException); + BOOST_CHECK_THROW(outOfRange.setBoostInt128(0, firebirdMin - 1), DatabaseException); +} + BOOST_AUTO_TEST_CASE(getBoostInt128FromInt128) { const auto database = getTempFile("Statement-getBoostInt128FromInt128.fdb"); @@ -2054,6 +2081,56 @@ BOOST_AUTO_TEST_CASE(setBoostDecFloat34ToDecFloat16) BOOST_CHECK(result.value() == BoostDecFloat16{"123456.789"}); } +BOOST_AUTO_TEST_CASE(decFloatRoundingAndSpecialValues) +{ + const auto database = getTempFile("Statement-decFloatRoundingAndSpecialValues.fdb"); + + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + Statement rounded{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + rounded.setBoostDecFloat34(0, BoostDecFloat34{"12345678901234565"}); + BOOST_REQUIRE(rounded.execute(transaction)); + BOOST_CHECK_EQUAL(rounded.getBoostDecFloat16(0).value(), BoostDecFloat16{"12345678901234570"}); + + Statement positiveInfinity{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + positiveInfinity.setBoostDecFloat16(0, std::numeric_limits::infinity()); + BOOST_REQUIRE(positiveInfinity.execute(transaction)); + const auto positiveInfinityValue = positiveInfinity.getBoostDecFloat16(0); + BOOST_REQUIRE(positiveInfinityValue.has_value()); + BOOST_CHECK((boost::multiprecision::isinf) (positiveInfinityValue.value())); + BOOST_CHECK(positiveInfinityValue.value() > 0); + + Statement negativeInfinity{attachment, transaction, "select cast(? as decfloat(34)) from rdb$database"}; + negativeInfinity.setBoostDecFloat34(0, -std::numeric_limits::infinity()); + BOOST_REQUIRE(negativeInfinity.execute(transaction)); + const auto negativeInfinityValue = negativeInfinity.getBoostDecFloat34(0); + BOOST_REQUIRE(negativeInfinityValue.has_value()); + BOOST_CHECK((boost::multiprecision::isinf) (negativeInfinityValue.value())); + BOOST_CHECK(negativeInfinityValue.value() < 0); + + Statement quietNaN{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + quietNaN.setBoostDecFloat16(0, BoostDecFloat16{"NaN"}); + BOOST_REQUIRE(quietNaN.execute(transaction)); + BOOST_CHECK_EQUAL(quietNaN.getString(0).value(), "NaN"); + + Statement signalingNaN{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + signalingNaN.setString(0, "sNaN"); + BOOST_REQUIRE(signalingNaN.execute(transaction)); + BOOST_CHECK_THROW(signalingNaN.getBoostDecFloat16(0), FbCppException); + const auto rawSignalingNaN = signalingNaN.getOpaqueDecFloat16(0); + BOOST_REQUIRE(rawSignalingNaN.has_value()); + + Statement rawRoundTrip{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + rawRoundTrip.setOpaqueDecFloat16(0, rawSignalingNaN.value()); + BOOST_REQUIRE(rawRoundTrip.execute(transaction)); + const auto rawRoundTripValue = rawRoundTrip.getOpaqueDecFloat16(0); + BOOST_REQUIRE(rawRoundTripValue.has_value()); + BOOST_CHECK_EQUAL(rawRoundTripValue->fb_data[0], rawSignalingNaN->fb_data[0]); +} + BOOST_AUTO_TEST_SUITE_END() #endif // FB_CPP_USE_BOOST_MULTIPRECISION @@ -2564,6 +2641,35 @@ BOOST_AUTO_TEST_CASE(opaqueInt128RoundTrip) BOOST_CHECK_EQUAL(select.getString(0).value(), "170141183460469231731687303715884105727"); } +BOOST_AUTO_TEST_CASE(scaledOpaqueInt128BindingPreservesAndConvertsScale) +{ + const auto database = getTempFile("Statement-scaledOpaqueInt128BindingPreservesAndConvertsScale.fdb"); + + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + Statement known{attachment, transaction, "select cast(12345.6789 as numeric(38, 4)) from rdb$database"}; + BOOST_REQUIRE(known.execute(transaction)); + const auto original = known.getScaledOpaqueInt128(0); + BOOST_REQUIRE(original.has_value()); + + Statement sameScale{attachment, transaction, "select cast(? as numeric(38, 4)) from rdb$database"}; + sameScale.setScaledOpaqueInt128(0, original); + BOOST_REQUIRE(sameScale.execute(transaction)); + const auto sameScaleResult = sameScale.getScaledOpaqueInt128(0); + BOOST_REQUIRE(sameScaleResult.has_value()); + BOOST_CHECK_EQUAL(sameScaleResult->scale, -4); + BOOST_CHECK_EQUAL(sameScaleResult->value.fb_data[0], original->value.fb_data[0]); + BOOST_CHECK_EQUAL(sameScaleResult->value.fb_data[1], original->value.fb_data[1]); + + Statement convertedScale{attachment, transaction, "select cast(? as numeric(38, 2)) from rdb$database"}; + convertedScale.set(0, original.value()); + BOOST_REQUIRE(convertedScale.execute(transaction)); + BOOST_CHECK_EQUAL(convertedScale.getString(0).value(), "12345.68"); +} + BOOST_AUTO_TEST_CASE(opaqueInt128NullHandling) { const auto database = getTempFile("Statement-opaqueInt128NullHandling.fdb"); @@ -3437,6 +3543,37 @@ BOOST_AUTO_TEST_CASE(getVariantOpaqueDecFloat34Preferred) #endif +BOOST_AUTO_TEST_CASE(rawNumericVariantsWorkWithoutBoostHelpers) +{ + using NumericVariant = std::variant; + + const auto database = getTempFile("Statement-rawNumericVariantsWorkWithoutBoostHelpers.fdb"); + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + Statement numeric{attachment, transaction, "select cast(123.45 as numeric(38, 2)) from rdb$database"}; + BOOST_REQUIRE(numeric.execute(transaction)); + const auto numericValue = numeric.get(0); + BOOST_REQUIRE(std::holds_alternative(numericValue)); + + Statement numericCopy{attachment, transaction, "select cast(? as numeric(38, 2)) from rdb$database"}; + numericCopy.set(0, NumericVariant{std::get(numericValue)}); + BOOST_REQUIRE(numericCopy.execute(transaction)); + BOOST_CHECK_EQUAL(numericCopy.getString(0).value(), "123.45"); + + Statement decFloat{attachment, transaction, "select cast(123.45 as decfloat(16)) from rdb$database"}; + BOOST_REQUIRE(decFloat.execute(transaction)); + const auto decFloatValue = decFloat.get(0); + BOOST_REQUIRE(std::holds_alternative(decFloatValue)); + + Statement decFloatCopy{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + decFloatCopy.set(0, NumericVariant{std::get(decFloatValue)}); + BOOST_REQUIRE(decFloatCopy.execute(transaction)); + BOOST_CHECK_EQUAL(decFloatCopy.getString(0).value(), "123.45"); +} + BOOST_AUTO_TEST_CASE(getVariantOpaqueDatePreferred) { // When variant has both OpaqueDate and Date, prefer OpaqueDate