Skip to content

Add support for DECIMAL using Boost.Decimal #399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion example/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -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) : : <warnings-as-errors>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
Expand Down Expand Up @@ -119,4 +119,5 @@ run_example connection_pool :
# Uses heavily Boost.Context coroutines, which aren't fully supported by asan
<address-sanitizer>norecover:<build>no
<address-sanitizer>enable:<build>no
<warnings-as-errors>off # TODO: remove when Boost.Context removes warnings
;
1 change: 1 addition & 0 deletions include/boost/mysql.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <boost/mysql/date.hpp>
#include <boost/mysql/datetime.hpp>
#include <boost/mysql/days.hpp>
#include <boost/mysql/decimal.hpp>
#include <boost/mysql/defaults.hpp>
#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_categories.hpp>
Expand Down
17 changes: 17 additions & 0 deletions include/boost/mysql/decimal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// 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

// Provides the required specializations to be able to use Boost.Decimal types
// 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 <boost/mysql/detail/typing/decimal.hpp>

#endif
169 changes: 169 additions & 0 deletions include/boost/mysql/detail/typing/decimal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//
// 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 <boost/mysql/detail/config.hpp>

#ifdef BOOST_MYSQL_CXX14

#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/metadata.hpp>
#include <boost/mysql/string_view.hpp>

#include <boost/mysql/detail/typing/readable_field_traits.hpp>

#include <boost/decimal.hpp>

#include <system_error>

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<int>(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 <class Decimal>
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::decimal32, void> : decimal_readable_field_traits<decimal::decimal32> {};
template <> struct readable_field_traits<decimal::decimal32_fast, void> : decimal_readable_field_traits<decimal::decimal32_fast> {};
template <> struct readable_field_traits<decimal::decimal64, void> : decimal_readable_field_traits<decimal::decimal64> {};
template <> struct readable_field_traits<decimal::decimal64_fast, void> : decimal_readable_field_traits<decimal::decimal64_fast> {};
template <> struct readable_field_traits<decimal::decimal128, void> : decimal_readable_field_traits<decimal::decimal128> {};
template <> struct readable_field_traits<decimal::decimal128_fast, void> : decimal_readable_field_traits<decimal::decimal128_fast> {};
// clang-format on

// format_sql support
template <class Decimal>
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
// (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<decimal::decimal32> : detail::decimal_formatter<decimal::decimal32> {};
template <> struct formatter<decimal::decimal32_fast> : detail::decimal_formatter<decimal::decimal32_fast> {};
template <> struct formatter<decimal::decimal64> : detail::decimal_formatter<decimal::decimal64> {};
template <> struct formatter<decimal::decimal64_fast> : detail::decimal_formatter<decimal::decimal64_fast> {};
template <> struct formatter<decimal::decimal128> : detail::decimal_formatter<decimal::decimal128> {};
template <> struct formatter<decimal::decimal128_fast> : detail::decimal_formatter<decimal::decimal128_fast> {};
// clang-format on

} // namespace mysql
} // namespace boost

#endif

#endif
34 changes: 17 additions & 17 deletions include/boost/mysql/detail/typing/meta_check_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const std::size_t> pos_map,
Expand All @@ -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();

Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ target_link_libraries(
boost_mysql_compiled
Boost::unit_test_framework
Boost::pfr
Boost::decimal
)
target_include_directories(
boost_mysql_testing
Expand Down
1 change: 1 addition & 0 deletions test/integration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test/integration/db_setup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <boost/mysql/metadata.hpp>
#include <boost/mysql/metadata_collection_view.hpp>

#include <boost/optional/optional.hpp>

#include <vector>

namespace boost {
Expand All @@ -27,7 +29,8 @@ class meta_validator
column_type type,
std::vector<flag_getter> flags = {},
unsigned decimals = 0,
std::vector<flag_getter> ignore_flags = {}
std::vector<flag_getter> ignore_flags = {},
boost::optional<unsigned> length = {}
)
: table_(std::move(table)),
org_table_(table_),
Expand All @@ -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(
Expand Down Expand Up @@ -71,6 +75,7 @@ class meta_validator
column_type type_;
std::vector<flag_getter> flags_;
std::vector<flag_getter> ignore_flags_;
boost::optional<unsigned> length_;
};

void validate_meta(const metadata_collection_view& actual, const std::vector<meta_validator>& expected);
Expand Down
11 changes: 7 additions & 4 deletions test/integration/src/metadata_validator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading