From 1b610778910d0aa594588d1599f5ee1e993480db Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 16 Jan 2025 17:32:22 +0100 Subject: [PATCH 01/20] Initial impl --- CMakeLists.txt | 2 + include/boost/mysql.hpp | 1 + include/boost/mysql/decimal.hpp | 126 ++++++++++++++++++ .../detail/typing/meta_check_context.hpp | 34 ++--- 4 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 include/boost/mysql/decimal.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c9243091..5326dc4b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ endif() # This is generated by boostdep. # Note that Boost::pfr is not listed because it's a peer dependency +# TODO: decimal should be a peer dependency target_link_libraries( boost_mysql INTERFACE @@ -45,6 +46,7 @@ target_link_libraries( Boost::system Boost::throw_exception Boost::variant2 + Boost::decimal Threads::Threads OpenSSL::Crypto OpenSSL::SSL diff --git a/include/boost/mysql.hpp b/include/boost/mysql.hpp index 149900a11..69ac6cda1 100644 --- a/include/boost/mysql.hpp +++ b/include/boost/mysql.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/mysql/decimal.hpp b/include/boost/mysql/decimal.hpp new file mode 100644 index 000000000..1e396ecdb --- /dev/null +++ b/include/boost/mysql/decimal.hpp @@ -0,0 +1,126 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DECIMAL_HPP +#define BOOST_MYSQL_DECIMAL_HPP + +#include + +#ifdef BOOST_MYSQL_CXX14 + +#include +#include + +#include + +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +// Get the number of decimal digits required to represent the given column. +// The server gives this information in required displayed characters, +// but there's a one-to-one mapping to precision. +// This same algorithm is employed by the server. +// Returns -1 in case of error +inline int get_decimal_precision(const metadata& meta) +{ + constexpr unsigned max_precision = 65u; // Max value allowed by the server + unsigned radix_chars = meta.decimals() > 0u ? 1u : 0u; // Number of characters used for the decimal point + unsigned sign_chars = meta.is_unsigned() ? 0u : 1u; // Number of characters used for the sign + unsigned res = meta.column_length() - radix_chars - sign_chars; + return res > max_precision ? -1 : static_cast(res); +} + +// meta_check implementation for decimal types +inline bool meta_check_decimal_impl(meta_check_context& ctx, int cpp_precision, const char* cpp_type_name) +{ + // Check the number of decimals + int required_precision = get_decimal_precision(ctx.current_meta()); + if (required_precision == -1) + { + ctx.add_error() << "Invalid precision received from the server for decimal column: '" + << column_type_to_str(ctx.current_meta()) << "'"; // TODO: better msg + } + if (required_precision > cpp_precision) + { + auto& stream = ctx.add_error(); + stream << "Incompatible types for field "; + ctx.insert_field_name(stream); + stream << ": C++ type '" << cpp_type_name << "' has a precision of " << cpp_precision + << " decimals, while the DB type requires a precision of " << required_precision + << " decimals"; + } + + // Check type (encoded as this function's return value) + return ctx.current_meta().type() == column_type::decimal; +} + +// parse implementation for decimal types +template +error_code parse_decimal_impl(field_view input, Decimal& output) +{ + // Check type + if (!input.is_string()) + return client_errc::static_row_parsing_error; + auto str = input.get_string(); + + // Invoke decimal's charconv. MySQL always uses the fixed format. + auto res = decimal::from_chars(str.begin(), str.end(), output, decimal::chars_format::fixed); + if (res.ec != std::errc{} || res.ptr != str.end()) + return client_errc::static_row_parsing_error; + + // Done + return error_code(); +} + +template <> +struct readable_field_traits +{ + static constexpr bool is_supported = true; + static BOOST_INLINE_CONSTEXPR const char* type_name = "decimal32"; + static bool meta_check(meta_check_context& ctx) { return meta_check_decimal_impl(ctx, 7, type_name); } + static error_code parse(field_view input, decimal::decimal32& output) + { + return parse_decimal_impl(input, output); + } +}; + +template <> +struct readable_field_traits +{ + static constexpr bool is_supported = true; + static BOOST_INLINE_CONSTEXPR const char* type_name = "decimal64"; + static bool meta_check(meta_check_context& ctx) { return meta_check_decimal_impl(ctx, 16, type_name); } + static error_code parse(field_view input, decimal::decimal64& output) + { + return parse_decimal_impl(input, output); + } +}; + +template <> +struct readable_field_traits +{ + static constexpr bool is_supported = true; + static BOOST_INLINE_CONSTEXPR const char* type_name = "decimal128"; + static bool meta_check(meta_check_context& ctx) { return meta_check_decimal_impl(ctx, 34, type_name); } + static error_code parse(field_view input, decimal::decimal128& output) + { + return parse_decimal_impl(input, output); + } +}; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif + +#endif diff --git a/include/boost/mysql/detail/typing/meta_check_context.hpp b/include/boost/mysql/detail/typing/meta_check_context.hpp index c9367aa36..1e5c35bfa 100644 --- a/include/boost/mysql/detail/typing/meta_check_context.hpp +++ b/include/boost/mysql/detail/typing/meta_check_context.hpp @@ -68,23 +68,6 @@ class meta_check_context metadata_collection_view meta_{}; bool nullability_checked_{}; - std::ostringstream& add_error() - { - if (!errors_) - errors_.reset(new std::ostringstream); - else - *errors_ << '\n'; - return *errors_; - } - - void insert_field_name(std::ostringstream& os) - { - if (has_field_names(name_table_)) - os << "'" << name_table_[current_index_] << "'"; - else - os << "in position " << current_index_; - } - public: meta_check_context( span pos_map, @@ -111,6 +94,23 @@ class meta_check_context bool nullability_checked() const noexcept { return nullability_checked_; } // Error reporting + std::ostringstream& add_error() + { + if (!errors_) + errors_.reset(new std::ostringstream); + else + *errors_ << '\n'; + return *errors_; + } + + void insert_field_name(std::ostringstream& os) + { + if (has_field_names(name_table_)) + os << "'" << name_table_[current_index_] << "'"; + else + os << "in position " << current_index_; + } + BOOST_MYSQL_DECL void add_field_absent_error(); From 0ac61641cd888a4b911afa5791732dec2f63c9a9 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 16 Jan 2025 17:43:36 +0100 Subject: [PATCH 02/20] Add fast types and refactor --- include/boost/mysql/decimal.hpp | 89 +++++++++++++++++---------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/include/boost/mysql/decimal.hpp b/include/boost/mysql/decimal.hpp index 1e396ecdb..5f54481fb 100644 --- a/include/boost/mysql/decimal.hpp +++ b/include/boost/mysql/decimal.hpp @@ -10,6 +10,10 @@ #include +#include "boost/decimal/decimal128.hpp" +#include "boost/decimal/fwd.hpp" +#include "boost/mp11/utility.hpp" + #ifdef BOOST_MYSQL_CXX14 #include @@ -63,59 +67,56 @@ inline bool meta_check_decimal_impl(meta_check_context& ctx, int cpp_precision, return ctx.current_meta().type() == column_type::decimal; } -// parse implementation for decimal types -template -error_code parse_decimal_impl(field_view input, Decimal& output) -{ - // Check type - if (!input.is_string()) - return client_errc::static_row_parsing_error; - auto str = input.get_string(); - - // Invoke decimal's charconv. MySQL always uses the fixed format. - auto res = decimal::from_chars(str.begin(), str.end(), output, decimal::chars_format::fixed); - if (res.ec != std::errc{} || res.ptr != str.end()) - return client_errc::static_row_parsing_error; - - // Done - return error_code(); -} +// type names for decimals +constexpr const char* decimal_type_name(decimal::decimal32) { return "decimal32"; } +constexpr const char* decimal_type_name(decimal::decimal64) { return "decimal64"; } +constexpr const char* decimal_type_name(decimal::decimal128) { return "decimal128"; } +constexpr const char* decimal_type_name(decimal::decimal32_fast) { return "decimal32_fast"; } +constexpr const char* decimal_type_name(decimal::decimal64_fast) { return "decimal64_fast"; } +constexpr const char* decimal_type_name(decimal::decimal128_fast) { return "decimal128_fast"; } + +// precisions for decimals +constexpr int decimal_precision(decimal::decimal32) { return 7; } +constexpr int decimal_precision(decimal::decimal64) { return 16; } +constexpr int decimal_precision(decimal::decimal128) { return 34; } +constexpr int decimal_precision(decimal::decimal32_fast) { return 7; } +constexpr int decimal_precision(decimal::decimal64_fast) { return 16; } +constexpr int decimal_precision(decimal::decimal128_fast) { return 34; } -template <> -struct readable_field_traits +template +struct decimal_readable_field_traits { static constexpr bool is_supported = true; - static BOOST_INLINE_CONSTEXPR const char* type_name = "decimal32"; - static bool meta_check(meta_check_context& ctx) { return meta_check_decimal_impl(ctx, 7, type_name); } - static error_code parse(field_view input, decimal::decimal32& output) + static BOOST_INLINE_CONSTEXPR const char* type_name = decimal_type_name(Decimal()); + static bool meta_check(meta_check_context& ctx) { - return parse_decimal_impl(input, output); + return meta_check_decimal_impl(ctx, decimal_precision(Decimal()), type_name); } -}; - -template <> -struct readable_field_traits -{ - static constexpr bool is_supported = true; - static BOOST_INLINE_CONSTEXPR const char* type_name = "decimal64"; - static bool meta_check(meta_check_context& ctx) { return meta_check_decimal_impl(ctx, 16, type_name); } - static error_code parse(field_view input, decimal::decimal64& output) + static error_code parse(field_view input, Decimal& output) { - return parse_decimal_impl(input, output); + // Check type + if (!input.is_string()) + return client_errc::static_row_parsing_error; + auto str = input.get_string(); + + // Invoke decimal's charconv. MySQL always uses the fixed format. + auto res = decimal::from_chars(str.begin(), str.end(), output, decimal::chars_format::fixed); + if (res.ec != std::errc{} || res.ptr != str.end()) + return client_errc::static_row_parsing_error; + + // Done + return error_code(); } }; -template <> -struct readable_field_traits -{ - static constexpr bool is_supported = true; - static BOOST_INLINE_CONSTEXPR const char* type_name = "decimal128"; - static bool meta_check(meta_check_context& ctx) { return meta_check_decimal_impl(ctx, 34, type_name); } - static error_code parse(field_view input, decimal::decimal128& output) - { - return parse_decimal_impl(input, output); - } -}; +// clang-format off +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +// clang-format on } // namespace detail } // namespace mysql From aaa0715144d89f72e94edb08f5922180edb6ece0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 16 Jan 2025 17:44:28 +0100 Subject: [PATCH 03/20] Cleanup --- include/boost/mysql/decimal.hpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/include/boost/mysql/decimal.hpp b/include/boost/mysql/decimal.hpp index 5f54481fb..1288fe463 100644 --- a/include/boost/mysql/decimal.hpp +++ b/include/boost/mysql/decimal.hpp @@ -10,10 +10,6 @@ #include -#include "boost/decimal/decimal128.hpp" -#include "boost/decimal/fwd.hpp" -#include "boost/mp11/utility.hpp" - #ifdef BOOST_MYSQL_CXX14 #include @@ -34,7 +30,7 @@ namespace detail { // but there's a one-to-one mapping to precision. // This same algorithm is employed by the server. // Returns -1 in case of error -inline int get_decimal_precision(const metadata& meta) +inline int decimal_required_precision(const metadata& meta) { constexpr unsigned max_precision = 65u; // Max value allowed by the server unsigned radix_chars = meta.decimals() > 0u ? 1u : 0u; // Number of characters used for the decimal point @@ -47,7 +43,7 @@ inline int get_decimal_precision(const metadata& meta) inline bool meta_check_decimal_impl(meta_check_context& ctx, int cpp_precision, const char* cpp_type_name) { // Check the number of decimals - int required_precision = get_decimal_precision(ctx.current_meta()); + int required_precision = decimal_required_precision(ctx.current_meta()); if (required_precision == -1) { ctx.add_error() << "Invalid precision received from the server for decimal column: '" From 491a942041fca10d28b439f729d60186e8f55a3a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 16 Jan 2025 17:46:39 +0100 Subject: [PATCH 04/20] Move to detail header --- include/boost/mysql/decimal.hpp | 115 +--------------- include/boost/mysql/detail/typing/decimal.hpp | 123 ++++++++++++++++++ 2 files changed, 127 insertions(+), 111 deletions(-) create mode 100644 include/boost/mysql/detail/typing/decimal.hpp diff --git a/include/boost/mysql/decimal.hpp b/include/boost/mysql/decimal.hpp index 1288fe463..f7b50258c 100644 --- a/include/boost/mysql/decimal.hpp +++ b/include/boost/mysql/decimal.hpp @@ -8,116 +8,9 @@ #ifndef BOOST_MYSQL_DECIMAL_HPP #define BOOST_MYSQL_DECIMAL_HPP -#include - -#ifdef BOOST_MYSQL_CXX14 - -#include -#include - -#include - -#include - -#include - -namespace boost { -namespace mysql { -namespace detail { - -// Get the number of decimal digits required to represent the given column. -// The server gives this information in required displayed characters, -// but there's a one-to-one mapping to precision. -// This same algorithm is employed by the server. -// Returns -1 in case of error -inline int decimal_required_precision(const metadata& meta) -{ - constexpr unsigned max_precision = 65u; // Max value allowed by the server - unsigned radix_chars = meta.decimals() > 0u ? 1u : 0u; // Number of characters used for the decimal point - unsigned sign_chars = meta.is_unsigned() ? 0u : 1u; // Number of characters used for the sign - unsigned res = meta.column_length() - radix_chars - sign_chars; - return res > max_precision ? -1 : static_cast(res); -} - -// meta_check implementation for decimal types -inline bool meta_check_decimal_impl(meta_check_context& ctx, int cpp_precision, const char* cpp_type_name) -{ - // Check the number of decimals - int required_precision = decimal_required_precision(ctx.current_meta()); - if (required_precision == -1) - { - ctx.add_error() << "Invalid precision received from the server for decimal column: '" - << column_type_to_str(ctx.current_meta()) << "'"; // TODO: better msg - } - if (required_precision > cpp_precision) - { - auto& stream = ctx.add_error(); - stream << "Incompatible types for field "; - ctx.insert_field_name(stream); - stream << ": C++ type '" << cpp_type_name << "' has a precision of " << cpp_precision - << " decimals, while the DB type requires a precision of " << required_precision - << " decimals"; - } - - // Check type (encoded as this function's return value) - return ctx.current_meta().type() == column_type::decimal; -} - -// type names for decimals -constexpr const char* decimal_type_name(decimal::decimal32) { return "decimal32"; } -constexpr const char* decimal_type_name(decimal::decimal64) { return "decimal64"; } -constexpr const char* decimal_type_name(decimal::decimal128) { return "decimal128"; } -constexpr const char* decimal_type_name(decimal::decimal32_fast) { return "decimal32_fast"; } -constexpr const char* decimal_type_name(decimal::decimal64_fast) { return "decimal64_fast"; } -constexpr const char* decimal_type_name(decimal::decimal128_fast) { return "decimal128_fast"; } - -// precisions for decimals -constexpr int decimal_precision(decimal::decimal32) { return 7; } -constexpr int decimal_precision(decimal::decimal64) { return 16; } -constexpr int decimal_precision(decimal::decimal128) { return 34; } -constexpr int decimal_precision(decimal::decimal32_fast) { return 7; } -constexpr int decimal_precision(decimal::decimal64_fast) { return 16; } -constexpr int decimal_precision(decimal::decimal128_fast) { return 34; } - -template -struct decimal_readable_field_traits -{ - static constexpr bool is_supported = true; - static BOOST_INLINE_CONSTEXPR const char* type_name = decimal_type_name(Decimal()); - static bool meta_check(meta_check_context& ctx) - { - return meta_check_decimal_impl(ctx, decimal_precision(Decimal()), type_name); - } - static error_code parse(field_view input, Decimal& output) - { - // Check type - if (!input.is_string()) - return client_errc::static_row_parsing_error; - auto str = input.get_string(); - - // Invoke decimal's charconv. MySQL always uses the fixed format. - auto res = decimal::from_chars(str.begin(), str.end(), output, decimal::chars_format::fixed); - if (res.ec != std::errc{} || res.ptr != str.end()) - return client_errc::static_row_parsing_error; - - // Done - return error_code(); - } -}; - -// clang-format off -template <> struct readable_field_traits : decimal_readable_field_traits {}; -template <> struct readable_field_traits : decimal_readable_field_traits {}; -template <> struct readable_field_traits : decimal_readable_field_traits {}; -template <> struct readable_field_traits : decimal_readable_field_traits {}; -template <> struct readable_field_traits : decimal_readable_field_traits {}; -template <> struct readable_field_traits : decimal_readable_field_traits {}; -// clang-format on - -} // namespace detail -} // namespace mysql -} // namespace boost - -#endif +// Provides the required specializations to be able to use Boost.Decimal types +// with the static interface. +// No declaration in the public boost::mysql namespace is pulled in. +#include #endif diff --git a/include/boost/mysql/detail/typing/decimal.hpp b/include/boost/mysql/detail/typing/decimal.hpp new file mode 100644 index 000000000..08ad5165a --- /dev/null +++ b/include/boost/mysql/detail/typing/decimal.hpp @@ -0,0 +1,123 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_DETAIL_TYPING_DECIMAL_HPP +#define BOOST_MYSQL_DETAIL_TYPING_DECIMAL_HPP + +#include + +#ifdef BOOST_MYSQL_CXX14 + +#include +#include + +#include + +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +// Get the number of decimal digits required to represent the given column. +// The server gives this information in required displayed characters, +// but there's a one-to-one mapping to precision. +// This same algorithm is employed by the server. +// Returns -1 in case of error +inline int decimal_required_precision(const metadata& meta) +{ + constexpr unsigned max_precision = 65u; // Max value allowed by the server + unsigned radix_chars = meta.decimals() > 0u ? 1u : 0u; // Number of characters used for the decimal point + unsigned sign_chars = meta.is_unsigned() ? 0u : 1u; // Number of characters used for the sign + unsigned res = meta.column_length() - radix_chars - sign_chars; + return res > max_precision ? -1 : static_cast(res); +} + +// meta_check implementation for decimal types +inline bool meta_check_decimal_impl(meta_check_context& ctx, int cpp_precision, const char* cpp_type_name) +{ + // Check the number of decimals + int required_precision = decimal_required_precision(ctx.current_meta()); + if (required_precision == -1) + { + ctx.add_error() << "Invalid precision received from the server for decimal column: '" + << column_type_to_str(ctx.current_meta()) << "'"; // TODO: better msg + } + if (required_precision > cpp_precision) + { + auto& stream = ctx.add_error(); + stream << "Incompatible types for field "; + ctx.insert_field_name(stream); + stream << ": C++ type '" << cpp_type_name << "' has a precision of " << cpp_precision + << " decimals, while the DB type requires a precision of " << required_precision + << " decimals"; + } + + // Check type (encoded as this function's return value) + return ctx.current_meta().type() == column_type::decimal; +} + +// type names for decimals +constexpr const char* decimal_type_name(decimal::decimal32) { return "decimal32"; } +constexpr const char* decimal_type_name(decimal::decimal64) { return "decimal64"; } +constexpr const char* decimal_type_name(decimal::decimal128) { return "decimal128"; } +constexpr const char* decimal_type_name(decimal::decimal32_fast) { return "decimal32_fast"; } +constexpr const char* decimal_type_name(decimal::decimal64_fast) { return "decimal64_fast"; } +constexpr const char* decimal_type_name(decimal::decimal128_fast) { return "decimal128_fast"; } + +// precisions for decimals +constexpr int decimal_precision(decimal::decimal32) { return 7; } +constexpr int decimal_precision(decimal::decimal64) { return 16; } +constexpr int decimal_precision(decimal::decimal128) { return 34; } +constexpr int decimal_precision(decimal::decimal32_fast) { return 7; } +constexpr int decimal_precision(decimal::decimal64_fast) { return 16; } +constexpr int decimal_precision(decimal::decimal128_fast) { return 34; } + +template +struct decimal_readable_field_traits +{ + static constexpr bool is_supported = true; + static BOOST_INLINE_CONSTEXPR const char* type_name = decimal_type_name(Decimal()); + static bool meta_check(meta_check_context& ctx) + { + return meta_check_decimal_impl(ctx, decimal_precision(Decimal()), type_name); + } + static error_code parse(field_view input, Decimal& output) + { + // Check type + if (!input.is_string()) + return client_errc::static_row_parsing_error; + auto str = input.get_string(); + + // Invoke decimal's charconv. MySQL always uses the fixed format. + auto res = decimal::from_chars(str.begin(), str.end(), output, decimal::chars_format::fixed); + if (res.ec != std::errc{} || res.ptr != str.end()) + return client_errc::static_row_parsing_error; + + // Done + return error_code(); + } +}; + +// clang-format off +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +template <> struct readable_field_traits : decimal_readable_field_traits {}; +// clang-format on + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif + +#endif From 6a8faf7d0a8e34a9ab6412f95d1ca7c169c00c3d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 20:03:15 +0100 Subject: [PATCH 05/20] Database types test --- test/integration/CMakeLists.txt | 1 + test/integration/db_setup.sql | 16 +++ .../test_integration/metadata_validator.hpp | 9 +- test/integration/src/metadata_validator.cpp | 11 ++- test/integration/test/decimal.cpp | 97 +++++++++++++++++++ 5 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 test/integration/test/decimal.cpp diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index 07d83fc68..5d3c886bf 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable( test/connection_pool.cpp test/db_specific.cpp test/database_types.cpp + test/decimal.cpp # Snippets test/snippets/tutorials.cpp diff --git a/test/integration/db_setup.sql b/test/integration/db_setup.sql index 650aa56db..11851d7c0 100644 --- a/test/integration/db_setup.sql +++ b/test/integration/db_setup.sql @@ -466,6 +466,22 @@ INSERT INTO types_binary VALUES ("empty", "", "", "", "", "", "") ; +CREATE TABLE types_decimal( + id VARCHAR(50) NOT NULL PRIMARY KEY, + field_4 DECIMAL(4), -- no scale + field_7 DECIMAL(7, 7), -- scale == precision, max precision supported by decimal32 + field_10 DECIMAL, -- default precision, should be (10, 0) + field_16 DECIMAL(16, 4), -- max precision supported by decimal64 + field_20 DECIMAL(20, 2) UNSIGNED, -- unsigned + field_34 DECIMAL(34, 30) -- max precision supported by decimal128, max supported scale +); +INSERT INTO types_decimal VALUES + ("regular", 213, 0.1214295, 9000, 20.1234, 121.20, 1234.567890123456789012345678901234), + ("negative", -213, -0.1214295, -9000, -20.1234, NULL, -1234.567890123456789012345678901234), + ("min", -9999, -0.9999999, -9999999999, -999999999999.9999, 0, -9999.999999999999999999999999999999), + ("max", 9999, 0.9999999, 9999999999, 999999999999.9999, 999999999999999999.99, -9999.999999999999999999999999999999) +; + CREATE TABLE types_not_implemented( id VARCHAR(50) NOT NULL PRIMARY KEY, field_decimal DECIMAL, diff --git a/test/integration/include/test_integration/metadata_validator.hpp b/test/integration/include/test_integration/metadata_validator.hpp index 7fdb0794d..770ac1ec1 100644 --- a/test/integration/include/test_integration/metadata_validator.hpp +++ b/test/integration/include/test_integration/metadata_validator.hpp @@ -11,6 +11,8 @@ #include #include +#include + #include namespace boost { @@ -27,7 +29,8 @@ class meta_validator column_type type, std::vector flags = {}, unsigned decimals = 0, - std::vector ignore_flags = {} + std::vector ignore_flags = {}, + boost::optional length = {} ) : table_(std::move(table)), org_table_(table_), @@ -36,7 +39,8 @@ class meta_validator decimals_(decimals), type_(type), flags_(std::move(flags)), - ignore_flags_(std::move(ignore_flags)) + ignore_flags_(std::move(ignore_flags)), + length_(length) { } meta_validator( @@ -71,6 +75,7 @@ class meta_validator column_type type_; std::vector flags_; std::vector ignore_flags_; + boost::optional length_; }; void validate_meta(const metadata_collection_view& actual, const std::vector& expected); diff --git a/test/integration/src/metadata_validator.cpp b/test/integration/src/metadata_validator.cpp index 0641da78e..03a1686e8 100644 --- a/test/integration/src/metadata_validator.cpp +++ b/test/integration/src/metadata_validator.cpp @@ -14,10 +14,7 @@ using namespace boost::mysql::test; -#define MYSQL_TEST_FLAG_GETTER_NAME_ENTRY(getter) \ - { \ - #getter, &boost::mysql::metadata::getter \ - } +#define MYSQL_TEST_FLAG_GETTER_NAME_ENTRY(getter) {#getter, &boost::mysql::metadata::getter} static struct flag_entry { @@ -78,6 +75,12 @@ void meta_validator::validate(const boost::mysql::metadata& value) const BOOST_TEST(!(value.*entry.getter)(), entry.name); } } + + // Column length + if (length_.has_value()) + { + BOOST_TEST(length_.value() == value.column_length()); + } } void boost::mysql::test::validate_meta( diff --git a/test/integration/test/decimal.cpp b/test/integration/test/decimal.cpp new file mode 100644 index 000000000..1a7af2eb8 --- /dev/null +++ b/test/integration/test/decimal.cpp @@ -0,0 +1,97 @@ +// +// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include + +#ifdef BOOST_MYSQL_CXX14 + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "test_common/network_result.hpp" +#include "test_integration/any_connection_fixture.hpp" +#include "test_integration/metadata_validator.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +using boost::optional; +using boost::test_tools::per_element; +using boost::describe::operators::operator==; +using boost::describe::operators::operator<<; +namespace decimal = boost::decimal; + +// For now, we don't support decimals as statement parameters +// (only when reading rows) +struct decimal_row +{ + std::string id; + optional field_4; + optional field_7; + optional field_10; + optional field_16; + optional field_20; + optional field_34; +}; +BOOST_DESCRIBE_STRUCT(decimal_row, (), (id, field_4, field_7, field_10, field_16, field_20, field_34)) + +namespace { + +BOOST_AUTO_TEST_SUITE(test_decimal) + +BOOST_FIXTURE_TEST_CASE(select, any_connection_fixture) +{ + using namespace boost::decimal; + + // Setup + connect(); + + // Issue the query + static_results result; + conn.async_execute("SELECT * FROM types_decimal ORDER BY id", result, as_netresult).validate_no_error(); + + // Validate metadata + std::vector id_flags{ + &metadata::is_primary_key, + &metadata::is_not_null, + &metadata::has_no_default_value, + }; + std::vector expected_meta{ + {"types_decimal", "id", column_type::varchar, std::move(id_flags), 0u, {}, {} }, + {"types_decimal", "field_4", column_type::decimal, {}, 0u, {}, 5u }, + {"types_decimal", "field_7", column_type::decimal, {}, 7u, {}, 9u }, + {"types_decimal", "field_10", column_type::decimal, {}, 0u, {}, 11u}, + {"types_decimal", "field_16", column_type::decimal, {}, 4u, {}, 18u}, + {"types_decimal", "field_20", column_type::decimal, {&metadata::is_unsigned}, 2u, {}, 21u}, + {"types_decimal", "field_34", column_type::decimal, {}, 30u, {}, 36u}, + }; + validate_meta(result.meta(), expected_meta); + + // Validate rows + // clang-format off + const decimal_row expected_rows [] = { + {"max", 9999_df, 0.9999999_dff, 9999999999_dd, 999999999999.9999_ddf, 999999999999999999.99_dl, -9999.999999999999999999999999999999_dlf}, + {"min", -9999_df, -0.9999999_dff, -9999999999_dd, -999999999999.9999_ddf, 0_dl, -9999.999999999999999999999999999999_dlf}, + {"negative", -213_df, -0.1214295_dff, -9000_dd, -20.1234_ddf, {}, -1234.567890123456789012345678901234_dlf}, + {"regular", 213_df, 0.1214295_dff, 9000_dd, 20.1234_ddf, 121.20_dl, 1234.567890123456789012345678901234_dlf}, + }; + // clang-format on + BOOST_TEST(result.rows() == expected_rows, per_element()); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace + +#endif From 6bde0da3ad1335b19ad8a3eff9fd6211f26a4c1b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 22:51:08 +0100 Subject: [PATCH 06/20] format_sql implementations --- include/boost/mysql/decimal.hpp | 5 ++- include/boost/mysql/detail/typing/decimal.hpp | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/include/boost/mysql/decimal.hpp b/include/boost/mysql/decimal.hpp index f7b50258c..ab8951b3e 100644 --- a/include/boost/mysql/decimal.hpp +++ b/include/boost/mysql/decimal.hpp @@ -9,8 +9,9 @@ #define BOOST_MYSQL_DECIMAL_HPP // Provides the required specializations to be able to use Boost.Decimal types -// with the static interface. -// No declaration in the public boost::mysql namespace is pulled in. +// with the static interface and with format_sql. +// This header doesn't contain anything visible because none of the included +// declarations should be used directly. #include #endif diff --git a/include/boost/mysql/detail/typing/decimal.hpp b/include/boost/mysql/detail/typing/decimal.hpp index 08ad5165a..c1fab12ec 100644 --- a/include/boost/mysql/detail/typing/decimal.hpp +++ b/include/boost/mysql/detail/typing/decimal.hpp @@ -12,12 +12,17 @@ #ifdef BOOST_MYSQL_CXX14 +#include +#include #include +#include #include +#include #include #include +#include #include @@ -114,7 +119,42 @@ template <> struct readable_field_traits : decimal_re template <> struct readable_field_traits : decimal_readable_field_traits {}; // clang-format on +// format_sql support +template +struct decimal_formatter +{ + const char* parse(const char* begin, const char*) { return begin; } + void format(Decimal value, format_context_base& ctx) const + { + // MySQL's DECIMAL uses fixed precision and a max precision of 65. + // With sign and radix, that's 67 characters max. + // Boost.Decimal can represent values that might yield longer representations + // (as it uses floating point representations). Buffer overflows cause a value_too_large error + char buffer[67]{}; + auto result = decimal::to_chars(buffer, buffer + sizeof(buffer), value, decimal::chars_format::fixed); + if (result.ec != std::errc()) + { + ctx.add_error( + result.ec == std::errc::value_too_large ? error_code(client_errc::unformattable_value) + : error_code(std::make_error_code(result.ec)) + ); + return; + } + ctx.append_raw(runtime(string_view(buffer, result.ptr - buffer))); + } +}; + } // namespace detail + +// clang-format off +template <> struct formatter : detail::decimal_formatter {}; +template <> struct formatter : detail::decimal_formatter {}; +template <> struct formatter : detail::decimal_formatter {}; +template <> struct formatter : detail::decimal_formatter {}; +template <> struct formatter : detail::decimal_formatter {}; +template <> struct formatter : detail::decimal_formatter {}; +// clang-format on + } // namespace mysql } // namespace boost From 9dc519715811af3aa3f876069ed49f0c7ea63242 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 22:51:20 +0100 Subject: [PATCH 07/20] Formattable concepts --- test/unit/test/format_sql/formattable.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/test/format_sql/formattable.cpp b/test/unit/test/format_sql/formattable.cpp index 09b81f503..dc5816068 100644 --- a/test/unit/test/format_sql/formattable.cpp +++ b/test/unit/test/format_sql/formattable.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include +#include #include #include @@ -37,6 +39,7 @@ using namespace boost::mysql; using namespace boost::mysql::test; +namespace decimal = boost::decimal; using mysql_time = boost::mysql::time; using std::vector; @@ -179,6 +182,14 @@ using format_fn_t = void (*)(int, format_context_base&); using format_seq_t = format_sequence, format_fn_t>; BOOST_MYSQL_CHECK_FORMATTABLE(format_seq_t, true) +// decimals are formattable +BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal32, true); +BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal32_fast, true); +BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal64, true); +BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal64_fast, true); +BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal128, true); +BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal128_fast, true); + // other stuff not accepted BOOST_MYSQL_CHECK_FORMATTABLE(void*, false) BOOST_MYSQL_CHECK_FORMATTABLE(field*, false) From 9f082b6f44a011a8722ca7b00a59fdf50d62350c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 22:52:34 +0100 Subject: [PATCH 08/20] Individual value first tests --- test/unit/test/format_sql/individual_value.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/unit/test/format_sql/individual_value.cpp b/test/unit/test/format_sql/individual_value.cpp index 84d23cb73..7d8930b73 100644 --- a/test/unit/test/format_sql/individual_value.cpp +++ b/test/unit/test/format_sql/individual_value.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -502,6 +503,22 @@ BOOST_AUTO_TEST_CASE(std_optional) } #endif +BOOST_AUTO_TEST_CASE(decimal32) +{ + using namespace boost::decimal; + BOOST_TEST(format_sql(opts, single_fmt, 200_df) == "SELECT 200.0000;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_df) == "SELECT 1.567890;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_df) == "SELECT 114209.9;"); + BOOST_TEST(format_sql(opts, single_fmt, -1.56789_df) == "SELECT -1.567890;"); + BOOST_TEST(format_sql(opts, single_fmt, 9999999_df) == "SELECT 9999999;"); + BOOST_TEST(format_sql(opts, single_fmt, -9999999_df) == "SELECT -9999999;"); + // BOOST_TEST(format_sql(opts, single_fmt, 0.000001_df) == "SELECT 0.000001;"); // crashes + + // Outside the range of DECIMAL(7), but can be used with more precise decimals + // BOOST_TEST(format_sql(opts, single_fmt, 9.999999e15_df) == "SELECT 999999900000000;"); + // BOOST_TEST(format_sql(opts, single_fmt, 1e-15_df) == "SELECT 0.000000000000001;"); +} + // // Errors when formatting individual fields // From b15b705e9a5536d3cdbed15c6923701065c1a197 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 23:13:01 +0100 Subject: [PATCH 09/20] C++14 guards --- test/unit/test/format_sql/individual_value.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/test/format_sql/individual_value.cpp b/test/unit/test/format_sql/individual_value.cpp index 7d8930b73..e4c440f8f 100644 --- a/test/unit/test/format_sql/individual_value.cpp +++ b/test/unit/test/format_sql/individual_value.cpp @@ -503,6 +503,7 @@ BOOST_AUTO_TEST_CASE(std_optional) } #endif +#ifdef BOOST_MYSQL_CXX14 BOOST_AUTO_TEST_CASE(decimal32) { using namespace boost::decimal; @@ -518,6 +519,7 @@ BOOST_AUTO_TEST_CASE(decimal32) // BOOST_TEST(format_sql(opts, single_fmt, 9.999999e15_df) == "SELECT 999999900000000;"); // BOOST_TEST(format_sql(opts, single_fmt, 1e-15_df) == "SELECT 0.000000000000001;"); } +#endif // // Errors when formatting individual fields From 927d1eadce0c67b668c7c05d2f5de98c5104d03f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 23:13:08 +0100 Subject: [PATCH 10/20] CMake restructure --- CMakeLists.txt | 2 -- test/CMakeLists.txt | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5326dc4b9..4c9243091 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,6 @@ endif() # This is generated by boostdep. # Note that Boost::pfr is not listed because it's a peer dependency -# TODO: decimal should be a peer dependency target_link_libraries( boost_mysql INTERFACE @@ -46,7 +45,6 @@ target_link_libraries( Boost::system Boost::throw_exception Boost::variant2 - Boost::decimal Threads::Threads OpenSSL::Crypto OpenSSL::SSL diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 30e2b26bb..d1d7941fc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries( boost_mysql_compiled Boost::unit_test_framework Boost::pfr + Boost::decimal ) target_include_directories( boost_mysql_testing From b9cb0125996bc85b16c8296c745aebf615f03ee5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 23:17:55 +0100 Subject: [PATCH 11/20] CI --- tools/ci/ci_util/install_boost.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/ci/ci_util/install_boost.py b/tools/ci/ci_util/install_boost.py index 4401f419a..94a3b6f6a 100644 --- a/tools/ci/ci_util/install_boost.py +++ b/tools/ci/ci_util/install_boost.py @@ -57,6 +57,9 @@ def install_boost( # Put our library inside boost root _copy_lib_to_boost(source_dir) + # Clone Boost.Decimal (TODO: this is just for the review) + run(['git', 'clone', '-b', 'develop', '--depth', '1', 'git@github.com:cppalliance/decimal.git', 'libs/decimal']) + # Install Boost dependencies submodules = [ 'libs/context', @@ -74,6 +77,7 @@ def install_boost( run(['python', 'tools/boostdep/depinst/depinst.py', '../tools/quickbook']) else: run(["python", "tools/boostdep/depinst/depinst.py", "--include", "example", "mysql"]) + # Bootstrap if IS_WINDOWS: From 8492a64deff7391936c72782173c1e9d43b847f2 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Jan 2025 23:27:05 +0100 Subject: [PATCH 12/20] Update remote to https --- tools/ci/ci_util/install_boost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/ci_util/install_boost.py b/tools/ci/ci_util/install_boost.py index 94a3b6f6a..fea0f6832 100644 --- a/tools/ci/ci_util/install_boost.py +++ b/tools/ci/ci_util/install_boost.py @@ -58,7 +58,7 @@ def install_boost( _copy_lib_to_boost(source_dir) # Clone Boost.Decimal (TODO: this is just for the review) - run(['git', 'clone', '-b', 'develop', '--depth', '1', 'git@github.com:cppalliance/decimal.git', 'libs/decimal']) + run(['git', 'clone', '-b', 'develop', '--depth', '1', 'https://github.com/cppalliance/decimal.git', 'libs/decimal']) # Install Boost dependencies submodules = [ From bcf46ce46fd2566e1a453f8715833a2a56723bf3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 18 Jan 2025 12:49:54 +0100 Subject: [PATCH 13/20] Disable warnings for Boost.Context using TUs --- test/Jamfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Jamfile b/test/Jamfile index 932b51ca2..7cdf22feb 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -136,7 +136,8 @@ alias common_test_sources # Boost.Context causes failures with warnings-as-errors # under libc++, because it builds objects that raise a -stdlib=libc++ unused warning -alias boost_context_lib : /boost/context//boost_context/off ; +alias boost_context_lib : /boost/context//boost_context/off + : usage-requirements off ; # TODO: remove when Boost.Context fixes warnings # Beast and JSON depend on Container, which causes trouble with on alias boost_beast_lib : /boost/beast//boost_beast/off ; From d2ca75836e4650d26de4a56cb60c9f0641735ba6 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 18 Jan 2025 12:50:04 +0100 Subject: [PATCH 14/20] ifdef formattable tests --- test/unit/test/format_sql/formattable.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/test/format_sql/formattable.cpp b/test/unit/test/format_sql/formattable.cpp index dc5816068..f5fe5778f 100644 --- a/test/unit/test/format_sql/formattable.cpp +++ b/test/unit/test/format_sql/formattable.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -183,12 +182,14 @@ using format_seq_t = format_sequence, format_fn_t>; BOOST_MYSQL_CHECK_FORMATTABLE(format_seq_t, true) // decimals are formattable +#ifdef BOOST_MYSQL_CXX14 BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal32, true); BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal32_fast, true); BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal64, true); BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal64_fast, true); BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal128, true); BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal128_fast, true); +#endif // other stuff not accepted BOOST_MYSQL_CHECK_FORMATTABLE(void*, false) From 53135eab0876b26563eb7040d3298f828a5ab0c8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 19 Jan 2025 12:54:54 +0100 Subject: [PATCH 15/20] warnings-as-errors=off in Boost.Context examples --- example/Jamfile | 3 ++- test/Jamfile | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/Jamfile b/example/Jamfile index 96422adfa..854d73423 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -71,7 +71,7 @@ run_example deletes : 2_simple/deletes.cpp run_example callbacks : 2_simple/callbacks.cpp : $(regular_args) ; run_example coroutines_cpp11 : 2_simple/coroutines_cpp11.cpp /boost/mysql/test//boost_context_lib - : $(regular_args) ; + : $(regular_args) : : off ; # TODO: remove when Boost.Context removes warnings run_example batch_inserts : 2_simple/batch_inserts.cpp /boost/mysql/test//boost_json_lib : $(hostname) : run_batch_inserts.py ; run_example batch_inserts_generic : 2_simple/batch_inserts_generic.cpp /boost/mysql/test//boost_json_lib @@ -119,4 +119,5 @@ run_example connection_pool : # Uses heavily Boost.Context coroutines, which aren't fully supported by asan norecover:no enable:no + off # TODO: remove when Boost.Context removes warnings ; diff --git a/test/Jamfile b/test/Jamfile index 7cdf22feb..932b51ca2 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -136,8 +136,7 @@ alias common_test_sources # Boost.Context causes failures with warnings-as-errors # under libc++, because it builds objects that raise a -stdlib=libc++ unused warning -alias boost_context_lib : /boost/context//boost_context/off - : usage-requirements off ; # TODO: remove when Boost.Context fixes warnings +alias boost_context_lib : /boost/context//boost_context/off ; # Beast and JSON depend on Container, which causes trouble with on alias boost_beast_lib : /boost/beast//boost_beast/off ; From a3c70179f2a4b8f5b9ac8064abfa079479249253 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 19 Jan 2025 12:56:18 +0100 Subject: [PATCH 16/20] Fix formattable in C++11 --- test/unit/test/format_sql/formattable.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/unit/test/format_sql/formattable.cpp b/test/unit/test/format_sql/formattable.cpp index f5fe5778f..fa1d4ce1b 100644 --- a/test/unit/test/format_sql/formattable.cpp +++ b/test/unit/test/format_sql/formattable.cpp @@ -38,7 +38,6 @@ using namespace boost::mysql; using namespace boost::mysql::test; -namespace decimal = boost::decimal; using mysql_time = boost::mysql::time; using std::vector; @@ -183,12 +182,12 @@ BOOST_MYSQL_CHECK_FORMATTABLE(format_seq_t, true) // decimals are formattable #ifdef BOOST_MYSQL_CXX14 -BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal32, true); -BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal32_fast, true); -BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal64, true); -BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal64_fast, true); -BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal128, true); -BOOST_MYSQL_CHECK_FORMATTABLE(decimal::decimal128_fast, true); +BOOST_MYSQL_CHECK_FORMATTABLE(boost::decimal::decimal32, true); +BOOST_MYSQL_CHECK_FORMATTABLE(boost::decimal::decimal32_fast, true); +BOOST_MYSQL_CHECK_FORMATTABLE(boost::decimal::decimal64, true); +BOOST_MYSQL_CHECK_FORMATTABLE(boost::decimal::decimal64_fast, true); +BOOST_MYSQL_CHECK_FORMATTABLE(boost::decimal::decimal128, true); +BOOST_MYSQL_CHECK_FORMATTABLE(boost::decimal::decimal128_fast, true); #endif // other stuff not accepted From a48f16bf4f41a383fad77c7aa8bf2da1d1f66298 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 19 Jan 2025 13:00:30 +0100 Subject: [PATCH 17/20] Add check for isnan/isinf --- include/boost/mysql/detail/typing/decimal.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/boost/mysql/detail/typing/decimal.hpp b/include/boost/mysql/detail/typing/decimal.hpp index c1fab12ec..226855e03 100644 --- a/include/boost/mysql/detail/typing/decimal.hpp +++ b/include/boost/mysql/detail/typing/decimal.hpp @@ -126,6 +126,13 @@ struct decimal_formatter const char* parse(const char* begin, const char*) { return begin; } void format(Decimal value, format_context_base& ctx) const { + // MySQL's DECIMAL doesn't support NaN or Inf + if (decimal::isnan(value) || decimal::isinf(value)) + { + ctx.add_error(client_errc::unformattable_value); + return; + } + // MySQL's DECIMAL uses fixed precision and a max precision of 65. // With sign and radix, that's 67 characters max. // Boost.Decimal can represent values that might yield longer representations From cd2bf6760d47b238fecb53dd6458ba0fcee53d37 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 19 Jan 2025 15:23:12 +0100 Subject: [PATCH 18/20] Finished unit tests --- .../unit/test/format_sql/individual_value.cpp | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/test/unit/test/format_sql/individual_value.cpp b/test/unit/test/format_sql/individual_value.cpp index e4c440f8f..6abb44606 100644 --- a/test/unit/test/format_sql/individual_value.cpp +++ b/test/unit/test/format_sql/individual_value.cpp @@ -504,7 +504,7 @@ BOOST_AUTO_TEST_CASE(std_optional) #endif #ifdef BOOST_MYSQL_CXX14 -BOOST_AUTO_TEST_CASE(decimal32) +BOOST_AUTO_TEST_CASE(decimal32_) { using namespace boost::decimal; BOOST_TEST(format_sql(opts, single_fmt, 200_df) == "SELECT 200.0000;"); @@ -519,6 +519,56 @@ BOOST_AUTO_TEST_CASE(decimal32) // BOOST_TEST(format_sql(opts, single_fmt, 9.999999e15_df) == "SELECT 999999900000000;"); // BOOST_TEST(format_sql(opts, single_fmt, 1e-15_df) == "SELECT 0.000000000000001;"); } + +BOOST_AUTO_TEST_CASE(decimal64_) +{ + using namespace boost::decimal; + BOOST_TEST(format_sql(opts, single_fmt, 200_dd) == "SELECT 200.0000000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dd) == "SELECT 1.567890000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_dd) == "SELECT 114209.9000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, -1.56789_dd) == "SELECT -1.567890000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 9999999999999999_dd) == "SELECT 9999999999999999;"); + BOOST_TEST(format_sql(opts, single_fmt, -9999999999999999_dd) == "SELECT -9999999999999999;"); + BOOST_TEST(format_sql(opts, single_fmt, 99999.99999999999_dd) == "SELECT 99999.99999999999;"); + BOOST_TEST(format_sql(opts, single_fmt, -99999.99999999999_dd) == "SELECT -99999.99999999999;"); +} + +BOOST_AUTO_TEST_CASE(decimal128_) +{ + using namespace boost::decimal; + BOOST_TEST(format_sql(opts, single_fmt, 200_dl) == "SELECT 200.0000000000000000000000000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dl) == "SELECT 1.567890000000000000000000000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_dl) == "SELECT 114209.9000000000000000000000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, -1.56789_dl) == "SELECT -1.567890000000000000000000000000000;"); + BOOST_TEST( + format_sql(opts, single_fmt, "9999999999999999999999999999999999"_dl) == + "SELECT 9999999999999999999999999999999999;" + ); + BOOST_TEST( + format_sql(opts, single_fmt, -"9999999999999999999999999999999999"_dl) == + "SELECT -9999999999999999999999999999999999;" + ); + BOOST_TEST( + format_sql(opts, single_fmt, 9.999999999999999999999999999999999_dl) == + "SELECT 9.999999999999999999999999999999999;" + ); + BOOST_TEST( + format_sql(opts, single_fmt, -9.999999999999999999999999999999999_dl) == + "SELECT -9.999999999999999999999999999999999;" + ); +} + +BOOST_AUTO_TEST_CASE(decimal_fast) +{ + // Spotcheck: fast decimals are also formattable + using namespace boost::decimal; + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dff) == "SELECT 1.567890;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.5678912345678_ddf) == "SELECT 1.567891234567800;"); + BOOST_TEST( + format_sql(opts, single_fmt, 1.567891234567887237237943928290828_dlf) == + "SELECT 1.567891234567887237237943928290828;" + ); +} #endif // @@ -704,4 +754,23 @@ BOOST_AUTO_TEST_CASE(boost_optional_error) { optional_error_test(); } #endif +#ifdef BOOST_MYSQL_CXX14 +BOOST_AUTO_TEST_CASE(decimal_error) +{ + using namespace boost::decimal; + + // NaN and Inf + using lims32 = std::numeric_limits; + BOOST_TEST(format_single_error("{}", lims32::infinity()) == client_errc::unformattable_value); + BOOST_TEST(format_single_error("{}", -lims32::infinity()) == client_errc::unformattable_value); + BOOST_TEST(format_single_error("{}", lims32::quiet_NaN()) == client_errc::unformattable_value); + + // Too long values + BOOST_TEST(format_single_error("{}", 9.99e90_df) == client_errc::unformattable_value); + BOOST_TEST(format_single_error("{}", 1e-94_df) == client_errc::unformattable_value); + // Subnormal values don't seem to work +} + +#endif + BOOST_AUTO_TEST_SUITE_END() From 5a737a88bdaa0a42b59a4e4782fa2969087ed765 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 19 Jan 2025 16:50:37 +0100 Subject: [PATCH 19/20] Adjust tests to new behavior --- include/boost/mysql/detail/typing/decimal.hpp | 1 - .../unit/test/format_sql/individual_value.cpp | 28 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/include/boost/mysql/detail/typing/decimal.hpp b/include/boost/mysql/detail/typing/decimal.hpp index 226855e03..b466401ec 100644 --- a/include/boost/mysql/detail/typing/decimal.hpp +++ b/include/boost/mysql/detail/typing/decimal.hpp @@ -22,7 +22,6 @@ #include #include -#include #include diff --git a/test/unit/test/format_sql/individual_value.cpp b/test/unit/test/format_sql/individual_value.cpp index 6abb44606..1d9168179 100644 --- a/test/unit/test/format_sql/individual_value.cpp +++ b/test/unit/test/format_sql/individual_value.cpp @@ -507,13 +507,13 @@ BOOST_AUTO_TEST_CASE(std_optional) BOOST_AUTO_TEST_CASE(decimal32_) { using namespace boost::decimal; - BOOST_TEST(format_sql(opts, single_fmt, 200_df) == "SELECT 200.0000;"); - BOOST_TEST(format_sql(opts, single_fmt, 1.56789_df) == "SELECT 1.567890;"); + BOOST_TEST(format_sql(opts, single_fmt, 200_df) == "SELECT 200;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_df) == "SELECT 1.56789;"); BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_df) == "SELECT 114209.9;"); - BOOST_TEST(format_sql(opts, single_fmt, -1.56789_df) == "SELECT -1.567890;"); + BOOST_TEST(format_sql(opts, single_fmt, -1.56789_df) == "SELECT -1.56789;"); BOOST_TEST(format_sql(opts, single_fmt, 9999999_df) == "SELECT 9999999;"); BOOST_TEST(format_sql(opts, single_fmt, -9999999_df) == "SELECT -9999999;"); - // BOOST_TEST(format_sql(opts, single_fmt, 0.000001_df) == "SELECT 0.000001;"); // crashes + BOOST_TEST(format_sql(opts, single_fmt, 0.000001_df) == "SELECT 0.000001;"); // Outside the range of DECIMAL(7), but can be used with more precise decimals // BOOST_TEST(format_sql(opts, single_fmt, 9.999999e15_df) == "SELECT 999999900000000;"); @@ -523,10 +523,10 @@ BOOST_AUTO_TEST_CASE(decimal32_) BOOST_AUTO_TEST_CASE(decimal64_) { using namespace boost::decimal; - BOOST_TEST(format_sql(opts, single_fmt, 200_dd) == "SELECT 200.0000000000000;"); - BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dd) == "SELECT 1.567890000000000;"); - BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_dd) == "SELECT 114209.9000000000;"); - BOOST_TEST(format_sql(opts, single_fmt, -1.56789_dd) == "SELECT -1.567890000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 200_dd) == "SELECT 200;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dd) == "SELECT 1.56789;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_dd) == "SELECT 114209.9;"); + BOOST_TEST(format_sql(opts, single_fmt, -1.56789_dd) == "SELECT -1.56789;"); BOOST_TEST(format_sql(opts, single_fmt, 9999999999999999_dd) == "SELECT 9999999999999999;"); BOOST_TEST(format_sql(opts, single_fmt, -9999999999999999_dd) == "SELECT -9999999999999999;"); BOOST_TEST(format_sql(opts, single_fmt, 99999.99999999999_dd) == "SELECT 99999.99999999999;"); @@ -536,10 +536,10 @@ BOOST_AUTO_TEST_CASE(decimal64_) BOOST_AUTO_TEST_CASE(decimal128_) { using namespace boost::decimal; - BOOST_TEST(format_sql(opts, single_fmt, 200_dl) == "SELECT 200.0000000000000000000000000000000;"); - BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dl) == "SELECT 1.567890000000000000000000000000000;"); - BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_dl) == "SELECT 114209.9000000000000000000000000000;"); - BOOST_TEST(format_sql(opts, single_fmt, -1.56789_dl) == "SELECT -1.567890000000000000000000000000000;"); + BOOST_TEST(format_sql(opts, single_fmt, 200_dl) == "SELECT 200;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dl) == "SELECT 1.56789;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.142099e5_dl) == "SELECT 114209.9;"); + BOOST_TEST(format_sql(opts, single_fmt, -1.56789_dl) == "SELECT -1.56789;"); BOOST_TEST( format_sql(opts, single_fmt, "9999999999999999999999999999999999"_dl) == "SELECT 9999999999999999999999999999999999;" @@ -562,8 +562,8 @@ BOOST_AUTO_TEST_CASE(decimal_fast) { // Spotcheck: fast decimals are also formattable using namespace boost::decimal; - BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dff) == "SELECT 1.567890;"); - BOOST_TEST(format_sql(opts, single_fmt, 1.5678912345678_ddf) == "SELECT 1.567891234567800;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.56789_dff) == "SELECT 1.56789;"); + BOOST_TEST(format_sql(opts, single_fmt, 1.5678912345678_ddf) == "SELECT 1.5678912345678;"); BOOST_TEST( format_sql(opts, single_fmt, 1.567891234567887237237943928290828_dlf) == "SELECT 1.567891234567887237237943928290828;" From 96bf1c9c235452cadf57b673dbca2d9f738e0a1e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 19 Jan 2025 18:05:49 +0100 Subject: [PATCH 20/20] Fix test issue in pos_map.cpp --- test/unit/test/detail/typing/pos_map.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test/detail/typing/pos_map.cpp b/test/unit/test/detail/typing/pos_map.cpp index 45bfdcaae..9b133209d 100644 --- a/test/unit/test/detail/typing/pos_map.cpp +++ b/test/unit/test/detail/typing/pos_map.cpp @@ -47,7 +47,7 @@ BOOST_AUTO_TEST_CASE(reset_nonempty) BOOST_TEST(map[0] == pos_absent); BOOST_TEST(map[1] == pos_absent); BOOST_TEST(map[2] == pos_absent); - BOOST_TEST(map[3] == 45u); // didn't modify any extra storage + BOOST_TEST(storage[3] == 45u); // didn't modify any extra storage } BOOST_AUTO_TEST_CASE(add_field_empty)