diff --git a/include/sqlpp11/data_types/time_point/operand.h b/include/sqlpp11/data_types/time_point/operand.h index dff77368..bc2db479 100644 --- a/include/sqlpp11/data_types/time_point/operand.h +++ b/include/sqlpp11/data_types/time_point/operand.h @@ -67,7 +67,7 @@ namespace sqlpp const auto dp = ::sqlpp::chrono::floor<::date::days>(t._t); const auto time = ::date::make_time(t._t - dp); const auto ymd = ::date::year_month_day{dp}; - context << "TIMESTAMP '" << ymd << ' ' << time << "'"; + context << "TIMESTAMP WITH TIME ZONE '" << ymd << ' ' << time << "+00'"; return context; } } // namespace sqlpp diff --git a/include/sqlpp11/postgresql/bind_result.h b/include/sqlpp11/postgresql/bind_result.h index 2157a477..9de3c6a5 100644 --- a/include/sqlpp11/postgresql/bind_result.h +++ b/include/sqlpp11/postgresql/bind_result.h @@ -34,6 +34,7 @@ #include #include +#include #include #include "detail/prepared_statement_handle.h" @@ -225,7 +226,6 @@ namespace sqlpp } } - // same parsing logic as SQLite connector // PostgreSQL will return one of those (using the default ISO client): // // 2010-10-11 01:02:03 - ISO timestamp without timezone @@ -234,71 +234,6 @@ namespace sqlpp // 1992-10-10 01:02:03-06:30 - for some timezones with non-hour offset // 1900-01-01 - date only // we do not support time-only values ! - namespace detail - { - inline auto check_first_digit(const char* text, bool digitFlag) -> bool - { - if (digitFlag) - { - if (not std::isdigit(*text)) - { - return false; - } - } - else - { - if (std::isdigit(*text) or *text == '\0') - { - return false; - } - } - return true; - } - - inline auto check_date_digits(const char* text) -> bool - { - for (const auto digitFlag : {true, true, true, true, false, true, true, false, true, true}) // YYYY-MM-DD - { - if (not check_first_digit(text, digitFlag)) - return false; - ++text; - } - return true; - } - - inline auto check_time_digits(const char* text) -> bool - { - for (const auto digitFlag : {true, true, false, true, true, false, true, true}) // hh:mm:ss - { - if (not check_first_digit(text, digitFlag)) - return false; - ++text; - } - return true; - } - - inline auto check_us_digits(const char* text) -> bool - { - for (const auto digitFlag : {true, true, true, true, true, true}) - { - if (not check_first_digit(text, digitFlag)) - return false; - ++text; - } - return true; - } - - inline auto check_tz_digits(const char* text) -> bool - { - for (const auto digitFlag : {false, true, true, false, true, true}) - { - if (not check_first_digit(text, digitFlag)) - return false; - ++text; - } - return true; - } - } // namespace inline void bind_result_t::_bind_date_result(size_t _index, ::sqlpp::chrono::day_point* value, bool* is_null) { @@ -320,16 +255,19 @@ namespace sqlpp std::cerr << "PostgreSQL debug: date string: " << date_string << std::endl; } - if (detail::check_date_digits(date_string)) - { - const auto ymd = - ::date::year(std::atoi(date_string)) / std::atoi(date_string + 5) / std::atoi(date_string + 8); - *value = ::sqlpp::chrono::day_point(ymd); - } - else - { - if (_handle->debug()) + static const std::regex rx {"(\\d{4})-(\\d{2})-(\\d{2})"}; + std::cmatch mr; + if (std::regex_match (date_string, mr, rx)) { + *value = + ::sqlpp::chrono::day_point{ + ::date::year{std::atoi(date_string + mr.position(1))} / // Year + std::atoi(date_string + mr.position(2)) / // Month + std::atoi(date_string + mr.position(3)) // Day of month + }; + } else { + if (_handle->debug()) { std::cerr << "PostgreSQL debug: got invalid date '" << date_string << "'" << std::endl; + } *value = {}; } } @@ -339,7 +277,7 @@ namespace sqlpp } } - // always returns local time for timestamp with time zone + // always returns UTC time for timestamp with time zone inline void bind_result_t::_bind_date_time_result(size_t _index, ::sqlpp::chrono::microsecond_point* value, bool* is_null) { auto index = static_cast(_index); @@ -358,97 +296,91 @@ namespace sqlpp { std::cerr << "PostgreSQL debug: got date_time string: " << date_string << std::endl; } - if (detail::check_date_digits(date_string)) - { - const auto ymd = - ::date::year(std::atoi(date_string)) / std::atoi(date_string + 5) / std::atoi(date_string + 8); - *value = ::sqlpp::chrono::day_point(ymd); - } - else - { - if (_handle->debug()) - std::cerr << "PostgreSQL debug: got invalid date_time" << std::endl; - *value = {}; - return; - } - if (std::strlen(date_string) <= 11) - return; - const auto time_string = date_string + 11; // YYYY-MM-DDT - if (detail::check_time_digits(time_string)) - { - *value += std::chrono::hours(std::atoi(time_string)) + std::chrono::minutes(std::atoi(time_string + 3)) + - std::chrono::seconds(std::atoi(time_string + 6)); - } - else - { - return; - } - - if (std::strlen(time_string) <= 9) - return; - auto us_string = time_string + 9; // hh:mm:ss. - int usec = 0; - for (size_t i = 0u; i < 6u; ++i) - { - if (std::isdigit(us_string[0])) - { - usec = 10 * usec + (us_string[0] - '0'); - ++us_string; + static const std::regex rx { + "(\\d{4})-(\\d{2})-(\\d{2}) " + "(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?" + "(?:([+-])(\\d{2})(?::(\\d{2})(?::(\\d{2}))?)?)?" + }; + std::cmatch mr; + if (std::regex_match (date_string, mr, rx)) { + *value = + ::sqlpp::chrono::day_point{ + ::date::year{std::atoi(date_string + mr.position(1))} / // Year + std::atoi(date_string + mr.position(2)) / // Month + std::atoi(date_string + mr.position(3)) // Day of month + } + + std::chrono::hours{std::atoi(date_string + mr.position(4))} + // Hour + std::chrono::minutes{std::atoi(date_string + mr.position(5))} + // Minute + std::chrono::seconds{std::atoi(date_string + mr.position(6))} + // Second + ::std::chrono::microseconds{ // Microsecond + mr[7].matched ? std::stoi((mr[7].str() + "000000").substr(0, 6)) : 0 + }; + if (mr[8].matched) { + const auto tz_sign = (date_string[mr.position(8)] == '+') ? 1 : -1; + const auto tz_offset = + std::chrono::hours{std::atoi(date_string + mr.position(9))} + + std::chrono::minutes{mr[10].matched ? std::atoi(date_string + mr.position(10)) : 0} + + std::chrono::seconds{mr[11].matched ? std::atoi(date_string + mr.position(11)) : 0}; + *value -= tz_sign * tz_offset; + } + } else { + if (_handle->debug()) { + std::cerr << "PostgreSQL debug: got invalid date_time '" << date_string << "'" << std::endl; } - else - usec *= 10; + *value = {}; } - *value += ::std::chrono::microseconds(usec); } } - // always returns local time for time with time zone + // always returns UTC time for time with time zone inline void bind_result_t::_bind_time_of_day_result(size_t _index, ::std::chrono::microseconds* value, bool* is_null) { - auto index = static_cast(_index); + auto index = static_cast(_index); + if (_handle->debug()) + { + std::cerr << "PostgreSQL debug: binding time result at index: " << index << std::endl; + } + + *is_null = _handle->result.isNull(_handle->count, index); + + if (!(*is_null)) + { + const auto time_string = _handle->result.getCharPtrValue(_handle->count, index); + if (_handle->debug()) { - std::cerr << "PostgreSQL debug: binding time result at index: " << index << std::endl; + std::cerr << "PostgreSQL debug: got time string: " << time_string << std::endl; } - *is_null = _handle->result.isNull(_handle->count, index); - - if (!(*is_null)) - { - const auto time_string = _handle->result.getCharPtrValue(_handle->count, index); - - if (_handle->debug()) - { - std::cerr << "PostgreSQL debug: got time string: " << time_string << std::endl; + static const std::regex rx { + "(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?" + "(?:([+-])(\\d{2})(?::(\\d{2})(?::(\\d{2}))?)?)?" + }; + std::cmatch mr; + if (std::regex_match (time_string, mr, rx)) { + *value = + std::chrono::hours{std::atoi(time_string + mr.position(1))} + // Hour + std::chrono::minutes{std::atoi(time_string + mr.position(2))} + // Minute + std::chrono::seconds{std::atoi(time_string + mr.position(3))} + // Second + ::std::chrono::microseconds{ // Microsecond + mr[4].matched ? std::stoi((mr[4].str() + "000000").substr(0, 6)) : 0 + }; + if (mr[5].matched) { + const auto tz_sign = (time_string[mr.position(5)] == '+') ? 1 : -1; + const auto tz_offset = + std::chrono::hours{std::atoi(time_string + mr.position(6))} + + std::chrono::minutes{mr[7].matched ? std::atoi(time_string + mr.position(7)) : 0} + + std::chrono::seconds{mr[8].matched ? std::atoi(time_string + mr.position(8)) : 0}; + *value -= tz_sign * tz_offset; } - - if (detail::check_time_digits(time_string)) - { - *value += std::chrono::hours(std::atoi(time_string)) + std::chrono::minutes(std::atoi(time_string + 3)) + - std::chrono::seconds(std::atoi(time_string + 6)); - } - else - { - return; - } - - if (std::strlen(time_string) <= 9) - return; - auto us_string = time_string + 9; // hh:mm:ss. - int usec = 0; - for (size_t i = 0u; i < 6u; ++i) - { - if (std::isdigit(us_string[0])) - { - usec = 10 * usec + (us_string[0] - '0'); - ++us_string; - } - else - usec *= 10; - } - *value += ::std::chrono::microseconds(usec); + } else { + if (_handle->debug()) { + std::cerr << "PostgreSQL debug: got invalid time '" << time_string << "'" << std::endl; + } + *value = {}; } + } } inline void bind_result_t::_bind_blob_result(size_t _index, const uint8_t** value, size_t* len) diff --git a/tests/postgresql/usage/CMakeLists.txt b/tests/postgresql/usage/CMakeLists.txt index 7fabc9f7..fb7ae37e 100644 --- a/tests/postgresql/usage/CMakeLists.txt +++ b/tests/postgresql/usage/CMakeLists.txt @@ -36,6 +36,7 @@ set(test_files InsertOnConflict.cpp Returning.cpp Select.cpp + TimeZone.cpp Transaction.cpp Type.cpp ) @@ -59,4 +60,4 @@ foreach(test_file IN LISTS test_files) add_test(NAME sqlpp11.postgresql.usage.${test} COMMAND sqlpp11_postgresql_tests ${test} ) -endforeach() \ No newline at end of file +endforeach() diff --git a/tests/postgresql/usage/TimeZone.cpp b/tests/postgresql/usage/TimeZone.cpp new file mode 100644 index 00000000..dfa0383d --- /dev/null +++ b/tests/postgresql/usage/TimeZone.cpp @@ -0,0 +1,164 @@ +/** + * Copyright © 2023 Vesselin Atanasov + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include +#include + +#include "make_test_connection.h" +#include "TabDateTime.h" + +namespace +{ + void save_regular (sqlpp::postgresql::connection& dbc, sqlpp::chrono::microsecond_point tp, std::chrono::microseconds tod, sqlpp::chrono::day_point dp) + { + model::TabDateTime tab {}; + dbc( + update(tab) + .set( + tab.c_timepoint = tp, + tab.c_time = tod, + tab.c_day = dp + ) + .unconditionally() + ); + } + + void save_prepared (sqlpp::postgresql::connection& dbc, sqlpp::chrono::microsecond_point tp, std::chrono::microseconds tod, sqlpp::chrono::day_point dp) + { + model::TabDateTime tab {}; + auto prepared_update = dbc.prepare( + update(tab) + .set( + tab.c_timepoint = parameter(tab.c_timepoint), + tab.c_time = parameter(tab.c_time), + tab.c_day = parameter(tab.c_day) + ) + .unconditionally() + ); + prepared_update.params.c_timepoint = tp; + prepared_update.params.c_time = tod; + prepared_update.params.c_day = dp; + dbc(prepared_update); + } + + template + void require_equal(int line, const L& l, const R& r) + { + if (l != r) + { + std::cerr << line << ": "; + serialize(::sqlpp::wrap_operand_t{l}, std::cerr); + std::cerr << " != "; + serialize(::sqlpp::wrap_operand_t{r}, std::cerr); + throw std::runtime_error("Unexpected result"); + } + } + + void check_saved_values(sqlpp::postgresql::connection& dbc, sqlpp::chrono::microsecond_point tp, std::chrono::microseconds tod, sqlpp::chrono::day_point dp) + { + model::TabDateTime tab {}; + + const auto &rows_1 = dbc( + select( + // c_timepoint as microseconds from the start of the UNIX epoch (1970-01-01 00:00:00 UTC) + sqlpp::verbatim("floor(extract(epoch from c_timepoint)*1000000)::int8").as(sqlpp::alias::a), + // c_time as microseconds from the start of the day (00:00:00 UTC) + sqlpp::verbatim("floor(extract(epoch from c_time)*1000000)::int8").as(sqlpp::alias::b), + // c_day as days from 1970-01-01 (timezone is not applicable to date fields) + sqlpp::verbatim("floor(extract(epoch from c_day)/86400)::int8").as(sqlpp::alias::c) + ) + .from(tab) + .unconditionally() + ); + // Check if the internal values of our C++ time variables match the internal values of the PostgreSQL date/time fields. + // This tests the conversion of date/time types from C++ to PostgreSQL while skipping the conversion from C++ to PostgreSQL. + const auto &row_1 = rows_1.front(); + require_equal(__LINE__, row_1.a.value(), tp.time_since_epoch().count()); + require_equal(__LINE__, row_1.b.value(), tod.count()); + require_equal(__LINE__, row_1.c.value(), dp.time_since_epoch().count()); + + // Check if saving date/time variables from C++ to PostgreSQL and then reading them back yields the same values. + // This tests the conversion of date/time types from C++ to PostgreSQL and then back from PostgreSQL to C++. + const auto rows_2 = dbc(select(all_of(tab)).from(tab).unconditionally()); + const auto &row_2 = rows_2.front(); + require_equal(__LINE__, row_2.c_timepoint.value(), tp); + require_equal(__LINE__, row_2.c_time.value(), tod); + require_equal(__LINE__, row_2.c_day.value(), dp); + } + + void test_time_point(sqlpp::postgresql::connection& dbc, sqlpp::chrono::microsecond_point tp) + { + auto dp = date::floor (tp); + auto tod = tp - dp; // Time of day + + // Test time values passed in a regular (non-prepared) statement + save_regular(dbc, tp, tod, dp); + check_saved_values(dbc, tp, tod, dp); + + // Test time values passed in a prepared statement + save_prepared(dbc, tp, tod, dp); + check_saved_values(dbc, tp, tod, dp); + } +}; + +int TimeZone(int, char*[]) +{ + namespace sql = sqlpp::postgresql; + + auto dbc = sql::make_test_connection(); + + dbc.execute("DROP TABLE IF EXISTS tabdatetime;"); + dbc.execute( + "CREATE TABLE tabdatetime " + "(" + "c_timepoint timestamp with time zone," + "c_time time with time zone," + "c_day date" + ")" + ); + + model::TabDateTime tab {}; + try { + dbc(insert_into(tab).default_values()); + + std::vector tps { + static_cast(date::January/1/1970) + std::chrono::hours{1} + std::chrono::minutes{20} + std::chrono::seconds{14} + std::chrono::microseconds{1}, + static_cast(date::June/13/1986) + std::chrono::hours{12} + std::chrono::minutes{0} + std::chrono::seconds{1} + std::chrono::microseconds{123}, + static_cast(date::December/31/2022) + std::chrono::hours{0} + std::chrono::minutes{59} + std::chrono::seconds{59} + std::chrono::microseconds{987654} + }; + for (const auto &tp : tps) { + test_time_point(dbc, tp); + } + } catch (const sql::failure& e) { + std::cerr << "Exception: " << e.what() << std::endl; + return 1; + } + + return 0; +}