0
0
mirror of https://github.com/rbock/sqlpp11.git synced 2024-11-15 12:29:41 +08:00

Treat PostgreSQL time values as being in UTC time zone (#487)

* When inserting values into "timestamp with time zone" fields treat the value as being in the UTC time zone.

* Simplify parsing of PostgreSQL date/time responses by using regular expressions. Always convert response times with time zone to UTC.

* Add tests which check if timestamp and date fields are treated as having a UTC time zone.

* Clarify the test comments.
This commit is contained in:
MeanSquaredError 2023-06-16 07:57:19 +03:00 committed by GitHub
parent 38aba217d4
commit a72b172a52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 252 additions and 155 deletions

View File

@ -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

View File

@ -34,6 +34,7 @@
#include <iomanip>
#include <iostream>
#include <regex>
#include <sstream>
#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<int>(_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<int>(_index);
auto index = static_cast<int>(_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)

View File

@ -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()
endforeach()

View File

@ -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 <vector>
#include <sqlpp11/postgresql/postgresql.h>
#include <sqlpp11/sqlpp11.h>
#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 <typename L, typename R>
void require_equal(int line, const L& l, const R& r)
{
if (l != r)
{
std::cerr << line << ": ";
serialize(::sqlpp::wrap_operand_t<L>{l}, std::cerr);
std::cerr << " != ";
serialize(::sqlpp::wrap_operand_t<R>{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<sqlpp::integer>("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<sqlpp::integer>("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<sqlpp::integer>("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<sqlpp::chrono::days> (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<sqlpp::chrono::microsecond_point> tps {
static_cast<date::sys_days>(date::January/1/1970) + std::chrono::hours{1} + std::chrono::minutes{20} + std::chrono::seconds{14} + std::chrono::microseconds{1},
static_cast<date::sys_days>(date::June/13/1986) + std::chrono::hours{12} + std::chrono::minutes{0} + std::chrono::seconds{1} + std::chrono::microseconds{123},
static_cast<date::sys_days>(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;
}