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:
parent
38aba217d4
commit
a72b172a52
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
164
tests/postgresql/usage/TimeZone.cpp
Normal file
164
tests/postgresql/usage/TimeZone.cpp
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user