diff --git a/AUTHORS b/AUTHORS index ba4f9ef7d..32e8b0017 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,3 +7,4 @@ Sergei Khandrikov Jonas Stoehr Evgeny Nikulin Martijn Otto +Mikhail Belyavsky diff --git a/include/ozo/ext/std.h b/include/ozo/ext/std.h index d9c70f8ae..570adbeb9 100644 --- a/include/ozo/ext/std.h +++ b/include/ozo/ext/std.h @@ -20,3 +20,4 @@ #include #include #include +#include diff --git a/include/ozo/ext/std/duration.h b/include/ozo/ext/std/duration.h new file mode 100644 index 000000000..22eba5474 --- /dev/null +++ b/include/ozo/ext/std/duration.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +/** + * @defgroup group-ext-std-chrono-duration-microseconds std::chrono::microseconds + * @ingroup group-ext-std + * @brief [std::chrono::microseconds](https://en.cppreference.com/w/cpp/chrono/duration) support + * + *@code +#include + *@endcode + * + * `std::chrono::microseconds` is mapped as `interval` PostgreSQL type. + * + * @note Supported 64-bit microseconds representation values only. + * @note In case of overflow (underflow) maximum (minimum) valid value is used. + */ + +BOOST_FUSION_DEFINE_STRUCT((ozo)(detail), pg_interval, + (std::int64_t, microseconds) + (std::int32_t, days) + (std::int32_t, months) +) + +namespace ozo::detail { + +inline detail::pg_interval from_chrono_duration(const std::chrono::microseconds& in) { + static_assert( + std::chrono::microseconds::min().count() == std::numeric_limits::min() && + std::chrono::microseconds::max().count() == std::numeric_limits::max(), + "std::chrono::microseconds tick representation type is not supported" + ); + + using days = std::chrono::duration>; + + return detail::pg_interval{(in % days(1)).count(), std::chrono::duration_cast(in).count(), 0}; +} + +inline std::chrono::microseconds to_chrono_duration(const pg_interval& interval) { + static_assert( + std::chrono::microseconds::min().count() == std::numeric_limits::min() && + std::chrono::microseconds::max().count() == std::numeric_limits::max(), + "std::chrono::microseconds tick representation type is not supported" + ); + + using std::chrono::microseconds; + using std::chrono::duration_cast; + + using usecs = std::chrono::duration; + using days = std::chrono::duration>; + using months = std::chrono::duration>; + + auto usecs_surplus = usecs(interval.microseconds) % days(1); + auto days_total = months(interval.months) + days(interval.days) + duration_cast(usecs(interval.microseconds)); + + if (days_total < (duration_cast(microseconds::min()) - days(1)) + || ((days_total < days(0)) && ((days_total + days(1)) + usecs_surplus < microseconds::min() + days(1)))) { + return microseconds::min(); + } + + if ((duration_cast(microseconds::max()) + days(1)) < days_total + || ((days(0) < days_total) && (microseconds::max() - days(1) < (days_total - days(1)) + usecs_surplus))) { + return microseconds::max(); + } + + return days_total + usecs_surplus; +} + +} // namespace ozo::detail + +namespace ozo { + +template <> +struct send_impl { + template + static ostream& apply(ostream& out, const OidMap&, const std::chrono::microseconds& in) { + static_assert(ozo::OidMap, "OidMap should model ozo::OidMap"); + + return write(out, detail::from_chrono_duration(in)); + } +}; + +template <> +struct recv_impl { + template + static istream& apply(istream& in, size_type, const OidMap&, std::chrono::microseconds& out) { + static_assert(ozo::OidMap, "OidMap should model ozo::OidMap"); + + detail::pg_interval interval; + read(in, interval); + + out = detail::to_chrono_duration(interval); + + return in; + } +}; + +} // namespace ozo + +namespace ozo::definitions { + +template <> +struct type : pg::type_definition{}; + +} // namespace ozo::definitions diff --git a/include/ozo/pg/types.h b/include/ozo/pg/types.h index 7a5714e47..b25178c5a 100644 --- a/include/ozo/pg/types.h +++ b/include/ozo/pg/types.h @@ -20,3 +20,4 @@ #include #include #include +#include diff --git a/include/ozo/pg/types/interval.h b/include/ozo/pg/types/interval.h new file mode 100644 index 000000000..c31ba1a72 --- /dev/null +++ b/include/ozo/pg/types/interval.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace ozo::pg { + +using interval = std::chrono::microseconds; + +} // namespace ozo::pg diff --git a/tests/binary_deserialization.cpp b/tests/binary_deserialization.cpp index 8ea8ebe73..49da2d804 100644 --- a/tests/binary_deserialization.cpp +++ b/tests/binary_deserialization.cpp @@ -8,6 +8,8 @@ #include #include +#include + BOOST_FUSION_DEFINE_STRUCT((), fusion_adapted_test_result, (std::string, text) @@ -776,4 +778,74 @@ TEST_F(recv, should_convert_TIMESTAMPOID_to_time_point) { EXPECT_EQ(result, expected); } +TEST_F(recv, should_convert_INTERVALOID_to_chrono_microseconds) { + const char bytes[] = { + char(0x00), char(0x00), char(0x00), char(0x08), char(0x89), char(0xD2), char(0x82), char(0xD6), // microseconds + char(0x00), char(0x00), char(0x00), char(0x09), // days + char(0x00), char(0x00), char(0x00), char(0x5C) // months + }; + + EXPECT_CALL(mock, field_type(_)).WillRepeatedly(Return(1186)); + EXPECT_CALL(mock, get_value(_, _)).WillRepeatedly(Return(bytes)); + EXPECT_CALL(mock, get_length(_, _)).WillRepeatedly(Return(16)); + EXPECT_CALL(mock, get_isnull(_, _)).WillRepeatedly(Return(false)); + + std::chrono::microseconds expected{239278272013014LL}; // 7y 8m 9d 10h 11m 12s 13ms 14us + std::chrono::microseconds result; + ozo::recv(value, oid_map, result); + + EXPECT_EQ(result, expected); +} + +struct to_duration : TestWithParam> { +}; + +TEST_P(to_duration, should_convert_pg_interval_to_chrono_microseconds) { + const auto [interval, expected] = GetParam(); + + const auto result = ozo::detail::to_chrono_duration(interval); + EXPECT_EQ(result, expected); +} + +INSTANTIATE_TEST_CASE_P(convert_success, to_duration, Values( + std::make_tuple(ozo::detail::pg_interval{ 36672013014LL, 9, 92}, 239278272013014us), + std::make_tuple(ozo::detail::pg_interval{ -49727986986LL, -20, 93}, 239278272013014us), + std::make_tuple(ozo::detail::pg_interval{ 239278272013014LL, 0, 0}, 239278272013014us), + + std::make_tuple(ozo::detail::pg_interval{ 3333333333333333LL, 0, 0}, 3333333333333333us), + std::make_tuple(ozo::detail::pg_interval{ 0LL, 200000, 0}, 17280000000000000us), + std::make_tuple(ozo::detail::pg_interval{ 0LL, 0, 5555}, 14398560000000000us), + + std::make_tuple(ozo::detail::pg_interval{ -14454775808LL, -106751991, 0}, std::chrono::microseconds::min()), + std::make_tuple(ozo::detail::pg_interval{ 71945224192LL, -106751992, 0}, std::chrono::microseconds::min()), + std::make_tuple(ozo::detail::pg_interval{ -532854775808LL, -555555555, 14960119}, std::chrono::microseconds::min()), + + std::make_tuple(ozo::detail::pg_interval{ 14454775807LL, 106751991, 0}, std::chrono::microseconds::max()), + std::make_tuple(ozo::detail::pg_interval{ -71945224193LL, 106751992, 0}, std::chrono::microseconds::max()), + std::make_tuple(ozo::detail::pg_interval{9223370740854775807LL, 555555555, -18518518}, std::chrono::microseconds::max()) +)); + +INSTANTIATE_TEST_CASE_P(convert_success_with_overflow, to_duration, Values( + std::make_tuple(ozo::detail::pg_interval{-14454775809LL, -106751991, 0}, std::chrono::microseconds::min()), + std::make_tuple(ozo::detail::pg_interval{ 14454775808LL, 106751991, 0}, std::chrono::microseconds::max()), + + std::make_tuple( + ozo::detail::pg_interval{ + std::numeric_limits::min(), + std::numeric_limits::min(), + std::numeric_limits::min() + }, + std::chrono::microseconds::min() + ), + + std::make_tuple( + ozo::detail::pg_interval{ + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max() + }, + std::chrono::microseconds::max() + ) +)); + } // namespace diff --git a/tests/binary_serialization.cpp b/tests/binary_serialization.cpp index 58cfcf3e4..ba60c2c01 100644 --- a/tests/binary_serialization.cpp +++ b/tests/binary_serialization.cpp @@ -198,4 +198,14 @@ TEST_F(send, with_std_chrono_time_point_should_store_as_microseconds) { })); } +TEST_F(send, with_std_chrono_microseconds_should_store_as_days_and_microseconds) { + const auto microseconds = 239278272013014LL; + ozo::send(os, oid_map, std::chrono::microseconds(microseconds)); + EXPECT_EQ(buffer, std::vector({ + char(0x00), char(0x00), char(0x00), char(0x08), char(0x89), char(0xD2), char(0x82), char(0xD6), // microseconds + char(0x00), char(0x00), char(0x0A), char(0xD1), // days + char(0x00), char(0x00), char(0x00), char(0x00), // months + })); +} + } // namespace diff --git a/tests/integration/request_integration.cpp b/tests/integration/request_integration.cpp index 38a2d5449..00b6718fc 100644 --- a/tests/integration/request_integration.cpp +++ b/tests/integration/request_integration.cpp @@ -403,6 +403,25 @@ TEST(request, should_send_and_receive_empty_optional) { io.run(); } +TEST(request, should_send_and_receive_interval) { + using namespace ozo::literals; + + const auto interval = ozo::pg::interval(239278272013014LL); + + ozo::io_context io; + const ozo::connection_info<> conn_info(OZO_PG_TEST_CONNINFO); + + ozo::rows_of result; + auto query = "SELECT '2769 days 10 hours 11 minutes 12 seconds 13014 microseconds'::interval + "_SQL + interval; + ozo::request(conn_info[io], query, ozo::into(result), [&](ozo::error_code ec, auto conn) { + ASSERT_REQUEST_OK(ec, conn); + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(std::get<0>(result[0]), ozo::pg::interval(478556544026028LL)); + }); + + io.run(); +} + TEST(request, should_send_and_receive_composite_with_empty_optional) { using namespace ozo::literals; namespace asio = boost::asio; diff --git a/tests/integration/result_integration.cpp b/tests/integration/result_integration.cpp index a0356cd12..a704abad3 100644 --- a/tests/integration/result_integration.cpp +++ b/tests/integration/result_integration.cpp @@ -55,6 +55,18 @@ TEST(result, should_convert_into_tuple_time_point_and_text) { EXPECT_EQ(std::get<1>(r[0]), "2"); } +TEST(result, should_convert_into_tuple_microseconds) { + auto result = execute_query( + "SELECT '7 years 8 months 9 days 10 hours 11 minutes 12 seconds 13 milliseconds 14 microseconds'::interval" + ); + auto oid_map = ozo::empty_oid_map(); + ozo::rows_of rows; + ozo::recv_result(result, oid_map, std::back_inserter(rows)); + + ASSERT_EQ(rows.size(), 1u); + EXPECT_EQ(std::get<0>(rows[0]), std::chrono::microseconds(239278272013014LL)); +} + TEST(result, should_convert_into_tuple_float_and_text) { auto result = execute_query("select 42.13::float4, 'text'::text;"); auto oid_map = ozo::empty_oid_map();