Skip to content

Commit

Permalink
Support PostgreSQL interval as std::microseconds
Browse files Browse the repository at this point in the history
  • Loading branch information
bmichail authored and thed636 committed Oct 8, 2019
1 parent 4ae2a4d commit 872af85
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Sergei Khandrikov <[email protected]>
Jonas Stoehr <[email protected]>
Evgeny Nikulin <[email protected]>
Martijn Otto <[email protected]>
Mikhail Belyavsky <[email protected]>
1 change: 1 addition & 0 deletions include/ozo/ext/std.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
#include <ozo/ext/std/weak_ptr.h>
#include <ozo/ext/std/array.h>
#include <ozo/ext/std/time_point.h>
#include <ozo/ext/std/duration.h>
112 changes: 112 additions & 0 deletions include/ozo/ext/std/duration.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once

#include <ozo/io/send.h>
#include <ozo/io/recv.h>
#include <ozo/type_traits.h>
#include <boost/fusion/adapted/struct/define_struct.hpp>

#include <chrono>
#include <cstdint>

/**
* @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 <ozo/ext/std/duration.h>
*@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<std::int64_t>::min() &&
std::chrono::microseconds::max().count() == std::numeric_limits<std::int64_t>::max(),
"std::chrono::microseconds tick representation type is not supported"
);

using days = std::chrono::duration<std::int32_t, std::ratio<24 * std::chrono::hours::period::num>>;

return detail::pg_interval{(in % days(1)).count(), std::chrono::duration_cast<days>(in).count(), 0};
}

inline std::chrono::microseconds to_chrono_duration(const pg_interval& interval) {
static_assert(
std::chrono::microseconds::min().count() == std::numeric_limits<std::int64_t>::min() &&
std::chrono::microseconds::max().count() == std::numeric_limits<std::int64_t>::max(),
"std::chrono::microseconds tick representation type is not supported"
);

using std::chrono::microseconds;
using std::chrono::duration_cast;

using usecs = std::chrono::duration<std::int64_t, std::micro>;
using days = std::chrono::duration<std::int64_t, std::ratio<24 * std::chrono::hours::period::num>>;
using months = std::chrono::duration<std::int32_t, std::ratio<30 * days::period::num>>;

auto usecs_surplus = usecs(interval.microseconds) % days(1);
auto days_total = months(interval.months) + days(interval.days) + duration_cast<days>(usecs(interval.microseconds));

if (days_total < (duration_cast<days>(microseconds::min()) - days(1))
|| ((days_total < days(0)) && ((days_total + days(1)) + usecs_surplus < microseconds::min() + days(1)))) {
return microseconds::min();
}

if ((duration_cast<days>(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<std::chrono::microseconds> {
template <typename OidMap>
static ostream& apply(ostream& out, const OidMap&, const std::chrono::microseconds& in) {
static_assert(ozo::OidMap<OidMap>, "OidMap should model ozo::OidMap");

return write(out, detail::from_chrono_duration(in));
}
};

template <>
struct recv_impl<std::chrono::microseconds> {
template <typename OidMap>
static istream& apply(istream& in, size_type, const OidMap&, std::chrono::microseconds& out) {
static_assert(ozo::OidMap<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<std::chrono::microseconds> : pg::type_definition<decltype("interval"_s)>{};

} // namespace ozo::definitions
1 change: 1 addition & 0 deletions include/ozo/pg/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
#include <ozo/pg/types/text.h>
#include <ozo/pg/types/uuid.h>
#include <ozo/pg/types/timestamp.h>
#include <ozo/pg/types/interval.h>
9 changes: 9 additions & 0 deletions include/ozo/pg/types/interval.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <ozo/ext/std/duration.h>

namespace ozo::pg {

using interval = std::chrono::microseconds;

} // namespace ozo::pg
72 changes: 72 additions & 0 deletions tests/binary_deserialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <boost/range/adaptor/transformed.hpp>

BOOST_FUSION_DEFINE_STRUCT((),
fusion_adapted_test_result,
(std::string, text)
Expand Down Expand Up @@ -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<std::tuple<ozo::detail::pg_interval, std::chrono::microseconds>> {
};

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<decltype(ozo::detail::pg_interval::microseconds)>::min(),
std::numeric_limits<decltype(ozo::detail::pg_interval::days)>::min(),
std::numeric_limits<decltype(ozo::detail::pg_interval::months)>::min()
},
std::chrono::microseconds::min()
),

std::make_tuple(
ozo::detail::pg_interval{
std::numeric_limits<decltype(ozo::detail::pg_interval::microseconds)>::max(),
std::numeric_limits<decltype(ozo::detail::pg_interval::days)>::max(),
std::numeric_limits<decltype(ozo::detail::pg_interval::months)>::max()
},
std::chrono::microseconds::max()
)
));

} // namespace
10 changes: 10 additions & 0 deletions tests/binary_serialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>({
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
19 changes: 19 additions & 0 deletions tests/integration/request_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ozo::pg::interval> 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;
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/result_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::chrono::microseconds> 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();
Expand Down

0 comments on commit 872af85

Please sign in to comment.