diff --git a/CMakeLists.txt b/CMakeLists.txt index ab1c198d..7d93cbdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,9 +28,6 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) mark_as_advanced(BOOST_MYSQL_SHA256_TESTS) endif() -option(BOOST_MYSQL_ALLOW_FETCH_CONTENT ON "Allow FetchContent for date library") -mark_as_advanced(BOOST_MYSQL_ALLOW_FETCH_CONTENT) - # Includes include(GNUInstallDirs) include(FetchContent) @@ -39,28 +36,6 @@ include(FetchContent) find_package(Boost 1.72.0 REQUIRED COMPONENTS system) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) -if (NOT BOOST_MYSQL_ALLOW_FETCH_CONTENT) - find_package(date REQUIRED) -else() - find_package(date QUIET) -endif() - -if (NOT date_FOUND) - message("-- Could NOT find date, fetching it") - FetchContent_Declare( - date - GIT_REPOSITORY https://github.com/HowardHinnant/date.git - GIT_TAG v2.4.1 - ) - - FetchContent_GetProperties(date) - if(NOT date_POPULATED) - FetchContent_Populate(date) - set(ENABLE_DATE_TESTING OFF CACHE BOOL "") - set(USE_SYSTEM_TZ_DB ON CACHE BOOL "") - add_subdirectory(${date_SOURCE_DIR} ${date_BINARY_DIR}) - endif() -endif() # Interface library (header-only) add_library(boost_mysql INTERFACE) @@ -78,7 +53,6 @@ target_link_libraries( Threads::Threads OpenSSL::Crypto OpenSSL::SSL - date_interface ) target_include_directories( boost_mysql @@ -92,17 +66,10 @@ target_compile_features( cxx_std_11 ) -# If we're on MSVC, the value of the __cplusplus macro is incorrect. -# We could fix it using /Zc:__cplusplus, but that breaks date.h -# Asio detects default function template arguments using __cplusplus, -# which are required for default completion tokens. This feature is -# available on every C++11 or higher capable compiler. Force it. +# Asio bases C++ feature detection on __cplusplus. Make MSVC +# define it correctly if (MSVC) - target_compile_definitions( - boost_mysql - INTERFACE - BOOST_ASIO_HAS_DEFAULT_FUNCTION_TEMPLATE_ARGUMENTS=1 - ) + target_compile_options(boost_mysql INTERFACE /Zc:__cplusplus) endif() # Installing diff --git a/Jamfile b/Jamfile index 6638951e..afad3a4d 100644 --- a/Jamfile +++ b/Jamfile @@ -9,18 +9,11 @@ import os ; project /boost/mysql ; -local DATE_ROOT = [ os.environ DATE_ROOT ] ; - -alias date : : : : - $(DATE_ROOT)/include - ; - alias boost_mysql : # Sources /boost/system//boost_system /user-config//ssl /user-config//crypto - date : # Requirements : # Default build : # Usage requirements diff --git a/README.md b/README.md index 30811ffd..76348b4a 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,6 @@ Finally, link your target against the **Boost::mysql** interface library, and yo - Boost 1.72 or higher. - OpenSSL. - CMake 3.13.0 or higher, if using CMake to build against the library (this is the preferred way). -- Howard Hinnant's date library (https://github.com/HowardHinnant/date) v2.4.1 or higher. - If you are using CMake to link against Boost.MySQL (the preferred way), it will be fetched automatically. - (no need for a manual download). - Tested with MySQL v5.7.29, MySQL v8.0.19, MariaDB v10.3 and MariaDB v10.5. ## Documentation and examples diff --git a/TODO.txt b/TODO.txt index 1c7401f1..a3c72b26 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,4 @@ +Remove gtest Better docs Breaking up the tutorial in pieces Explaining the different overloads and async methods available @@ -5,9 +6,6 @@ Better docs BoostBook Automate the process Make undocumented enum values show (for collation) -License issues - date - gtest CI tests for older compilers GCCs Clangs @@ -42,6 +40,8 @@ Other possible features CLIENT_OPTIONAL_RESULTSET_METADATA Status flags accessors in resultset (for OK_Packet) Technical debt + Do we need --with-system --with-date_time + Constexpr issues with value and optional Increase int serialization test coverage Increase DB version test coverage Conan for dependencies @@ -67,4 +67,3 @@ Technical debt prepared_statement::execute(): static_assert(), handle value&, const value&, anything convertible Test for too many connections Executor tests? - Update date version to remove target_compile_definitions() for default completion tokens diff --git a/cmake/config.cmake.in b/cmake/config.cmake.in index da7e1113..4e60152b 100644 --- a/cmake/config.cmake.in +++ b/cmake/config.cmake.in @@ -12,6 +12,5 @@ include(CMakeFindDependencyMacro) find_dependency(Boost 1.72.0 REQUIRED COMPONENTS system) find_dependency(Threads REQUIRED) find_dependency(OpenSSL REQUIRED) -find_dependency(date REQUIRED) include("${CMAKE_CURRENT_LIST_DIR}/boost_mysql-targets.cmake") \ No newline at end of file diff --git a/doc/main_page.hpp b/doc/main_page.hpp index 1178378d..4aca2c30 100644 --- a/doc/main_page.hpp +++ b/doc/main_page.hpp @@ -38,9 +38,6 @@ * - Boost 1.72 or higher. * - OpenSSL. * - CMake 3.13.0 or higher, if using CMake to build against the library (this is the preferred way). - * - Howard Hinnant's date library (https://github.com/HowardHinnant/date) v2.4.1 or higher. - * If you are using CMake to link against Boost.MySQL (the preferred way), it will be fetched automatically. - * (no need for a manual download). * - Tested with MySQL v5.7.29, MySQL v8.0.19, MariaDB v10.3 and MariaDB v10.5. * * \section Features diff --git a/include/boost/mysql/detail/protocol/date.hpp b/include/boost/mysql/detail/protocol/date.hpp new file mode 100644 index 00000000..accfdc36 --- /dev/null +++ b/include/boost/mysql/detail/protocol/date.hpp @@ -0,0 +1,38 @@ +// +// Copyright (c) 2019-2020 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_PROTOCOL_DATE_HPP +#define BOOST_MYSQL_DETAIL_PROTOCOL_DATE_HPP + +// All these algorithms have been taken from: +// http://howardhinnant.github.io/date_algorithms.html + +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +struct year_month_day +{ + int years; + unsigned month; + unsigned day; +}; + +BOOST_CXX14_CONSTEXPR inline bool is_valid(const year_month_day& ymd) noexcept; +BOOST_CXX14_CONSTEXPR inline int ymd_to_days(const year_month_day& ymd) noexcept; +BOOST_CXX14_CONSTEXPR inline year_month_day days_to_ymd(int num_days) noexcept; + +} // detail +} // mysql +} // boost + +#include "boost/mysql/detail/protocol/impl/date.hpp" + +#endif diff --git a/include/boost/mysql/detail/protocol/impl/binary_deserialization.ipp b/include/boost/mysql/detail/protocol/impl/binary_deserialization.ipp index 3a3bf7c5..6370c39a 100644 --- a/include/boost/mysql/detail/protocol/impl/binary_deserialization.ipp +++ b/include/boost/mysql/detail/protocol/impl/binary_deserialization.ipp @@ -11,6 +11,7 @@ #include "boost/mysql/detail/protocol/serialization.hpp" #include "boost/mysql/detail/protocol/null_bitmap_traits.hpp" #include "boost/mysql/detail/protocol/constants.hpp" +#include "boost/mysql/detail/protocol/date.hpp" namespace boost { namespace mysql { @@ -90,7 +91,7 @@ errc deserialize_binary_value_float( // Time types inline errc deserialize_binary_ymd( deserialization_context& ctx, - ::date::year_month_day& output + year_month_day& output ) { std::uint16_t year; @@ -110,11 +111,7 @@ inline errc deserialize_binary_ymd( return errc::protocol_value_error; } - output = ::date::year_month_day ( - ::date::year(year), - ::date::month(month), - ::date::day(day) - ); + output = year_month_day {year, month, day}; return errc::ok; } @@ -138,25 +135,20 @@ inline errc deserialize_binary_value_date( } // Deserialize rest of fields - ::date::year_month_day ymd; + year_month_day ymd; err = deserialize_binary_ymd(ctx, ymd); if (err != errc::ok) return err; // Check for invalid dates, represented as NULL in C++ - if (!ymd.ok()) + if (!is_valid(ymd)) { output = value(nullptr); return errc::ok; } - // Range check - date d (ymd); - if (is_out_of_range(d)) - return errc::protocol_value_error; - - // Convert to sys_days (date) - output = value(static_cast(ymd)); + // Convert to value + output = value(date(days(ymd_to_days(ymd)))); return errc::ok; } @@ -175,7 +167,7 @@ inline errc deserialize_binary_value_datetime( // Deserialize date. If the DATETIME does not contain these values, // they are supposed to be zero (invalid date) - ::date::year_month_day ymd (::date::year(0), ::date::month(0), ::date::day(0)); + year_month_day ymd {}; if (length >= datetime_d_sz) { err = deserialize_binary_ymd(ctx, ymd); @@ -219,20 +211,14 @@ inline errc deserialize_binary_value_datetime( // Check for invalid dates, represented in C++ as NULL. // Note: we do the check here to ensure we consume all the bytes // associated to this datetime - if (!ymd.ok()) + if (!is_valid(ymd)) { output = value(nullptr); return errc::ok; } - // Range check - date d (ymd); - if (is_out_of_range(d)) - { - return errc::protocol_value_error; - } - // Compose the final datetime. Doing time of day and date separately to avoid overflow + date d (days(ymd_to_days(ymd))); auto time_of_day = std::chrono::hours(hours) + std::chrono::minutes(minutes) + @@ -257,7 +243,7 @@ inline errc deserialize_binary_value_time( // If the TIME contains no value for these fields, they are zero std::uint8_t is_negative = 0; - std::uint32_t days = 0; + std::uint32_t num_days = 0; std::uint8_t hours = 0; std::uint8_t minutes = 0; std::uint8_t seconds = 0; @@ -269,7 +255,7 @@ inline errc deserialize_binary_value_time( err = deserialize( ctx, is_negative, - days, + num_days, hours, minutes, seconds @@ -287,7 +273,7 @@ inline errc deserialize_binary_value_time( } // Range check - if (days > time_max_days || + if (num_days > time_max_days || hours > max_hour || minutes > max_min || seconds > max_sec || @@ -298,7 +284,7 @@ inline errc deserialize_binary_value_time( // Compose the final time output = value(time((is_negative ? -1 : 1) * ( - ::date::days(days) + + days(num_days) + std::chrono::hours(hours) + std::chrono::minutes(minutes) + std::chrono::seconds(seconds) + diff --git a/include/boost/mysql/detail/protocol/impl/binary_serialization.ipp b/include/boost/mysql/detail/protocol/impl/binary_serialization.ipp index b8c397d0..e8ca4437 100644 --- a/include/boost/mysql/detail/protocol/impl/binary_serialization.ipp +++ b/include/boost/mysql/detail/protocol/impl/binary_serialization.ipp @@ -9,6 +9,7 @@ #define BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_BINARY_SERIALIZATION_IPP #include "boost/mysql/detail/protocol/constants.hpp" +#include "boost/mysql/detail/protocol/date.hpp" namespace boost { namespace mysql { @@ -32,14 +33,15 @@ serialize_binary_value_impl( // Does not add the length prefix byte inline void serialize_binary_ymd( serialization_context& ctx, - const ::date::year_month_day& ymd + const year_month_day& ymd ) noexcept { + assert(ymd.years >= 0 && ymd.years <= 0xffff); serialize( ctx, - static_cast(static_cast(ymd.year())), - static_cast(static_cast(ymd.month())), - static_cast(static_cast(ymd.day())) + static_cast(ymd.years), + static_cast(ymd.month), + static_cast(ymd.day) ); } @@ -48,8 +50,8 @@ inline void serialize_binary_value_impl( const date& input ) { - ::date::year_month_day ymd (input); - assert(ymd.ok()); + assert(input >= min_date && input <= max_date); + auto ymd = days_to_ymd(input.time_since_epoch().count()); serialize(ctx, static_cast(binc::date_sz)); serialize_binary_ymd(ctx, ymd); @@ -60,21 +62,26 @@ inline void serialize_binary_value_impl( const datetime& input ) { + assert(input >= min_datetime && input <= max_datetime); + // Break datetime - auto days = ::date::floor<::date::days>(input); - ::date::year_month_day ymd (days); - ::date::time_of_day tod (input - days); - assert(ymd.ok()); + using namespace std::chrono; + auto input_dur = input.time_since_epoch(); + auto num_micros = duration_cast(input_dur % seconds(1)); + auto num_secs = duration_cast(input_dur % minutes(1) - num_micros); + auto num_mins = duration_cast(input_dur % hours(1) - num_secs); + auto num_hours = duration_cast(input_dur % days(1) - num_mins); + auto num_days = duration_cast(input_dur - num_hours); // Serialize serialize(ctx, static_cast(binc::datetime_dhmsu_sz)); - serialize_binary_ymd(ctx, ymd); + serialize_binary_ymd(ctx, days_to_ymd(num_days.count())); serialize( ctx, - static_cast(tod.hours().count()), - static_cast(tod.minutes().count()), - static_cast(tod.seconds().count()), - static_cast(tod.subseconds().count()) + static_cast(num_hours.count()), + static_cast(num_mins.count()), + static_cast(num_secs.count()), + static_cast(num_micros.count()) ); } @@ -84,11 +91,12 @@ inline void serialize_binary_value_impl( ) { // Break time - auto days = std::chrono::duration_cast<::date::days>(input); - auto hours = std::chrono::duration_cast(input % ::date::days(1)); - auto minutes = std::chrono::duration_cast(input % std::chrono::hours(1)); - auto seconds = std::chrono::duration_cast(input % std::chrono::minutes(1)); - auto microseconds = input % std::chrono::seconds(1); + using namespace std::chrono; + auto num_micros = duration_cast(input % seconds(1)); + auto num_secs = duration_cast(input % minutes(1) - num_micros); + auto num_mins = duration_cast(input % hours(1) - num_secs); + auto num_hours = duration_cast(input % days(1) - num_mins); + auto num_days = duration_cast(input - num_hours); std::uint8_t is_negative = (input.count() < 0) ? 1 : 0; // Serialize @@ -96,11 +104,11 @@ inline void serialize_binary_value_impl( ctx, static_cast(binc::time_dhmsu_sz), is_negative, - static_cast(std::abs(days.count())), - static_cast(std::abs(hours.count())), - static_cast(std::abs(minutes.count())), - static_cast(std::abs(seconds.count())), - static_cast(std::abs(microseconds.count())) + static_cast(std::abs(num_days.count())), + static_cast(std::abs(num_hours.count())), + static_cast(std::abs(num_mins.count())), + static_cast(std::abs(num_secs.count())), + static_cast(std::abs(num_micros.count())) ); } diff --git a/include/boost/mysql/detail/protocol/impl/date.hpp b/include/boost/mysql/detail/protocol/impl/date.hpp new file mode 100644 index 00000000..0a247c4c --- /dev/null +++ b/include/boost/mysql/detail/protocol/impl/date.hpp @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019-2020 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_PROTOCOL_IMPL_DATE_HPP +#define BOOST_MYSQL_DETAIL_PROTOCOL_IMPL_DATE_HPP + +#include "boost/mysql/detail/protocol/constants.hpp" +#include + +namespace boost { +namespace mysql { +namespace detail { + +constexpr bool is_leap(int y) noexcept +{ + return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); +} + +BOOST_CXX14_CONSTEXPR inline unsigned last_month_day(int y, unsigned m) noexcept +{ + constexpr unsigned char a [] = + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + return m != 2 || !is_leap(y) ? a[m-1] : 29u; +} + +} // detail +} // mysql +} // boost + +BOOST_CXX14_CONSTEXPR inline bool boost::mysql::detail::is_valid( + const year_month_day& ymd +) noexcept +{ + return + ymd.years >= 0 && + ymd.years <= static_cast(max_year) && + ymd.month > 0 && + ymd.month <= max_month && + ymd.day > 0 && + ymd.day <= last_month_day(ymd.years, ymd.month); +} + +BOOST_CXX14_CONSTEXPR inline int boost::mysql::detail::ymd_to_days( + const year_month_day& ymd +) noexcept +{ + assert(is_valid(ymd)); + int y = ymd.years; + const int m = ymd.month; + const int d = ymd.day; + y -= m <= 2; + const int era = (y >= 0 ? y : y-399) / 400; + const unsigned yoe = static_cast(y - era * 400); // [0, 399] + const unsigned doy = (153*(m + (m > 2 ? -3 : 9)) + 2)/5 + d-1; // [0, 365] + const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy; // [0, 146096] + return era * 146097 + static_cast(doe) - 719468; +} + +BOOST_CXX14_CONSTEXPR inline boost::mysql::detail::year_month_day +boost::mysql::detail::days_to_ymd( + int num_days +) noexcept +{ + num_days += 719468; + const int era = (num_days >= 0 ? num_days : num_days - 146096) / 146097; + const unsigned doe = static_cast(num_days - era * 146097); // [0, 146096] + const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; // [0, 399] + const int y = static_cast(yoe) + era * 400; + const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100); // [0, 365] + const unsigned mp = (5*doy + 2)/153; // [0, 11] + const unsigned d = doy - (153*mp+2)/5 + 1; // [1, 31] + const unsigned m = mp + (mp < 10 ? 3 : -9); // [1, 12] + return year_month_day{y + (m <= 2), m, d}; +} + + + + + +#endif diff --git a/include/boost/mysql/detail/protocol/impl/text_deserialization.ipp b/include/boost/mysql/detail/protocol/impl/text_deserialization.ipp index 5a66f166..762b6f7a 100644 --- a/include/boost/mysql/detail/protocol/impl/text_deserialization.ipp +++ b/include/boost/mysql/detail/protocol/impl/text_deserialization.ipp @@ -14,6 +14,7 @@ #include #include #include "boost/mysql/detail/protocol/constants.hpp" +#include "boost/mysql/detail/protocol/date.hpp" #ifdef BOOST_MSVC #pragma warning( push ) @@ -90,7 +91,7 @@ inline unsigned compute_micros(unsigned parsed_micros, unsigned decimals) noexce inline errc deserialize_text_ymd( boost::string_view from, - ::date::year_month_day& to + year_month_day& to ) { using namespace textc; @@ -114,11 +115,7 @@ inline errc deserialize_text_ymd( if (year > max_year || month > max_month || day > max_day) return errc::protocol_value_error; - to = ::date::year_month_day( - ::date::year{static_cast(year)}, - ::date::month{month}, - ::date::day{day} - ); + to = year_month_day{static_cast(year), month, day}; return errc::ok; } @@ -128,26 +125,21 @@ inline errc deserialize_text_value_date( ) noexcept { // Deserialize ymd - ::date::year_month_day ymd; + year_month_day ymd {}; auto err = deserialize_text_ymd(from, ymd); if (err != errc::ok) return err; // Verify date validity. MySQL allows zero and invalid dates, which // we represent in C++ as NULL - if (!ymd.ok()) + if (!is_valid(ymd)) { to = value(nullptr); return errc::ok; } - // Range check - date d (ymd); - if (is_out_of_range(d)) - return errc::protocol_value_error; - // Done - to = value(d); + to = value(date(days(ymd_to_days(ymd)))); return errc::ok; } @@ -168,7 +160,7 @@ inline errc deserialize_text_value_datetime( return errc::protocol_value_error; // Deserialize date part - ::date::year_month_day ymd; + year_month_day ymd {}; auto err = deserialize_text_ymd(from.substr(0, date_sz), ymd); if (err != errc::ok) return err; @@ -211,18 +203,14 @@ inline errc deserialize_text_value_datetime( // Date validity. MySQL allows DATETIMEs with invalid dates, which // we represent here as NULL - if (!ymd.ok()) + if (!is_valid(ymd)) { to = value(nullptr); return errc::ok; } - // Range check for date - date d (ymd); - if (is_out_of_range(d)) - return errc::protocol_value_error; - // Sum it up. Doing time of day independently to prevent overflow + date d (days(ymd_to_days(ymd))); auto time_of_day = std::chrono::hours(hours) + std::chrono::minutes(minutes) + diff --git a/include/boost/mysql/impl/value.hpp b/include/boost/mysql/impl/value.hpp index 769e9ccc..ffc20b36 100644 --- a/include/boost/mysql/impl/value.hpp +++ b/include/boost/mysql/impl/value.hpp @@ -9,28 +9,13 @@ #define BOOST_MYSQL_IMPL_VALUE_HPP #include "boost/mysql/detail/auxiliar/container_equals.hpp" +#include "boost/mysql/detail/protocol/date.hpp" +#include namespace boost { namespace mysql { namespace detail { -inline bool is_out_of_range( - const date& d -) -{ - return d < min_date || d > max_date; -} - -// Range checks -#ifndef BOOST_NO_CXX14_CONSTEXPR -static_assert(date::min() <= min_date, "Range check failed"); -static_assert(date::max() >= max_date, "Range check failed"); -static_assert(datetime::min() <= min_datetime, "Range check failed"); -static_assert(datetime::max() >= max_datetime, "Range check failed"); -static_assert(time::min() <= min_time, "Range check failed"); -static_assert(time::max() >= max_time, "Range check failed"); -#endif - struct print_visitor { std::ostream& os; @@ -40,27 +25,46 @@ struct print_visitor template void operator()(const T& value) const { os << value; } - void operator()(const date& value) const { ::date::operator<<(os, value); } + void operator()(const date& value) const + { + assert(value >= min_date && value <= max_date); + auto ymd = days_to_ymd(value.time_since_epoch().count()); + char buffer [32] {}; + snprintf(buffer, sizeof(buffer), "%04d-%02u-%02u", ymd.years, ymd.month, ymd.day); + os << buffer; + } + void operator()(const datetime& value) const + { + using namespace std::chrono; + date date_part = time_point_cast(value); + if (date_part > value) + date_part -= days(1); + auto tod = value - date_part; + (*this)(date_part); // date part + os << ' '; // separator + (*this)(duration_cast