Skip to content

Commit a72b172

Browse files
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.
1 parent 38aba21 commit a72b172

File tree

4 files changed

+252
-155
lines changed

4 files changed

+252
-155
lines changed

include/sqlpp11/data_types/time_point/operand.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ namespace sqlpp
6767
const auto dp = ::sqlpp::chrono::floor<::date::days>(t._t);
6868
const auto time = ::date::make_time(t._t - dp);
6969
const auto ymd = ::date::year_month_day{dp};
70-
context << "TIMESTAMP '" << ymd << ' ' << time << "'";
70+
context << "TIMESTAMP WITH TIME ZONE '" << ymd << ' ' << time << "+00'";
7171
return context;
7272
}
7373
} // namespace sqlpp

include/sqlpp11/postgresql/bind_result.h

+85-153
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
#include <iomanip>
3636
#include <iostream>
37+
#include <regex>
3738
#include <sstream>
3839

3940
#include "detail/prepared_statement_handle.h"
@@ -225,7 +226,6 @@ namespace sqlpp
225226
}
226227
}
227228

228-
// same parsing logic as SQLite connector
229229
// PostgreSQL will return one of those (using the default ISO client):
230230
//
231231
// 2010-10-11 01:02:03 - ISO timestamp without timezone
@@ -234,71 +234,6 @@ namespace sqlpp
234234
// 1992-10-10 01:02:03-06:30 - for some timezones with non-hour offset
235235
// 1900-01-01 - date only
236236
// we do not support time-only values !
237-
namespace detail
238-
{
239-
inline auto check_first_digit(const char* text, bool digitFlag) -> bool
240-
{
241-
if (digitFlag)
242-
{
243-
if (not std::isdigit(*text))
244-
{
245-
return false;
246-
}
247-
}
248-
else
249-
{
250-
if (std::isdigit(*text) or *text == '\0')
251-
{
252-
return false;
253-
}
254-
}
255-
return true;
256-
}
257-
258-
inline auto check_date_digits(const char* text) -> bool
259-
{
260-
for (const auto digitFlag : {true, true, true, true, false, true, true, false, true, true}) // YYYY-MM-DD
261-
{
262-
if (not check_first_digit(text, digitFlag))
263-
return false;
264-
++text;
265-
}
266-
return true;
267-
}
268-
269-
inline auto check_time_digits(const char* text) -> bool
270-
{
271-
for (const auto digitFlag : {true, true, false, true, true, false, true, true}) // hh:mm:ss
272-
{
273-
if (not check_first_digit(text, digitFlag))
274-
return false;
275-
++text;
276-
}
277-
return true;
278-
}
279-
280-
inline auto check_us_digits(const char* text) -> bool
281-
{
282-
for (const auto digitFlag : {true, true, true, true, true, true})
283-
{
284-
if (not check_first_digit(text, digitFlag))
285-
return false;
286-
++text;
287-
}
288-
return true;
289-
}
290-
291-
inline auto check_tz_digits(const char* text) -> bool
292-
{
293-
for (const auto digitFlag : {false, true, true, false, true, true})
294-
{
295-
if (not check_first_digit(text, digitFlag))
296-
return false;
297-
++text;
298-
}
299-
return true;
300-
}
301-
} // namespace
302237

303238
inline void bind_result_t::_bind_date_result(size_t _index, ::sqlpp::chrono::day_point* value, bool* is_null)
304239
{
@@ -320,16 +255,19 @@ namespace sqlpp
320255
std::cerr << "PostgreSQL debug: date string: " << date_string << std::endl;
321256
}
322257

323-
if (detail::check_date_digits(date_string))
324-
{
325-
const auto ymd =
326-
::date::year(std::atoi(date_string)) / std::atoi(date_string + 5) / std::atoi(date_string + 8);
327-
*value = ::sqlpp::chrono::day_point(ymd);
328-
}
329-
else
330-
{
331-
if (_handle->debug())
258+
static const std::regex rx {"(\\d{4})-(\\d{2})-(\\d{2})"};
259+
std::cmatch mr;
260+
if (std::regex_match (date_string, mr, rx)) {
261+
*value =
262+
::sqlpp::chrono::day_point{
263+
::date::year{std::atoi(date_string + mr.position(1))} / // Year
264+
std::atoi(date_string + mr.position(2)) / // Month
265+
std::atoi(date_string + mr.position(3)) // Day of month
266+
};
267+
} else {
268+
if (_handle->debug()) {
332269
std::cerr << "PostgreSQL debug: got invalid date '" << date_string << "'" << std::endl;
270+
}
333271
*value = {};
334272
}
335273
}
@@ -339,7 +277,7 @@ namespace sqlpp
339277
}
340278
}
341279

342-
// always returns local time for timestamp with time zone
280+
// always returns UTC time for timestamp with time zone
343281
inline void bind_result_t::_bind_date_time_result(size_t _index, ::sqlpp::chrono::microsecond_point* value, bool* is_null)
344282
{
345283
auto index = static_cast<int>(_index);
@@ -358,97 +296,91 @@ namespace sqlpp
358296
{
359297
std::cerr << "PostgreSQL debug: got date_time string: " << date_string << std::endl;
360298
}
361-
if (detail::check_date_digits(date_string))
362-
{
363-
const auto ymd =
364-
::date::year(std::atoi(date_string)) / std::atoi(date_string + 5) / std::atoi(date_string + 8);
365-
*value = ::sqlpp::chrono::day_point(ymd);
366-
}
367-
else
368-
{
369-
if (_handle->debug())
370-
std::cerr << "PostgreSQL debug: got invalid date_time" << std::endl;
371-
*value = {};
372-
return;
373-
}
374-
375-
if (std::strlen(date_string) <= 11)
376-
return;
377-
const auto time_string = date_string + 11; // YYYY-MM-DDT
378-
if (detail::check_time_digits(time_string))
379-
{
380-
*value += std::chrono::hours(std::atoi(time_string)) + std::chrono::minutes(std::atoi(time_string + 3)) +
381-
std::chrono::seconds(std::atoi(time_string + 6));
382-
}
383-
else
384-
{
385-
return;
386-
}
387299

388-
if (std::strlen(time_string) <= 9)
389-
return;
390-
auto us_string = time_string + 9; // hh:mm:ss.
391-
int usec = 0;
392-
for (size_t i = 0u; i < 6u; ++i)
393-
{
394-
if (std::isdigit(us_string[0]))
395-
{
396-
usec = 10 * usec + (us_string[0] - '0');
397-
++us_string;
300+
static const std::regex rx {
301+
"(\\d{4})-(\\d{2})-(\\d{2}) "
302+
"(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?"
303+
"(?:([+-])(\\d{2})(?::(\\d{2})(?::(\\d{2}))?)?)?"
304+
};
305+
std::cmatch mr;
306+
if (std::regex_match (date_string, mr, rx)) {
307+
*value =
308+
::sqlpp::chrono::day_point{
309+
::date::year{std::atoi(date_string + mr.position(1))} / // Year
310+
std::atoi(date_string + mr.position(2)) / // Month
311+
std::atoi(date_string + mr.position(3)) // Day of month
312+
} +
313+
std::chrono::hours{std::atoi(date_string + mr.position(4))} + // Hour
314+
std::chrono::minutes{std::atoi(date_string + mr.position(5))} + // Minute
315+
std::chrono::seconds{std::atoi(date_string + mr.position(6))} + // Second
316+
::std::chrono::microseconds{ // Microsecond
317+
mr[7].matched ? std::stoi((mr[7].str() + "000000").substr(0, 6)) : 0
318+
};
319+
if (mr[8].matched) {
320+
const auto tz_sign = (date_string[mr.position(8)] == '+') ? 1 : -1;
321+
const auto tz_offset =
322+
std::chrono::hours{std::atoi(date_string + mr.position(9))} +
323+
std::chrono::minutes{mr[10].matched ? std::atoi(date_string + mr.position(10)) : 0} +
324+
std::chrono::seconds{mr[11].matched ? std::atoi(date_string + mr.position(11)) : 0};
325+
*value -= tz_sign * tz_offset;
326+
}
327+
} else {
328+
if (_handle->debug()) {
329+
std::cerr << "PostgreSQL debug: got invalid date_time '" << date_string << "'" << std::endl;
398330
}
399-
else
400-
usec *= 10;
331+
*value = {};
401332
}
402-
*value += ::std::chrono::microseconds(usec);
403333
}
404334
}
405335

406-
// always returns local time for time with time zone
336+
// always returns UTC time for time with time zone
407337
inline void bind_result_t::_bind_time_of_day_result(size_t _index, ::std::chrono::microseconds* value, bool* is_null)
408338
{
409-
auto index = static_cast<int>(_index);
410-
if (_handle->debug())
411-
{
412-
std::cerr << "PostgreSQL debug: binding time result at index: " << index << std::endl;
413-
}
414-
415-
*is_null = _handle->result.isNull(_handle->count, index);
339+
auto index = static_cast<int>(_index);
340+
if (_handle->debug())
341+
{
342+
std::cerr << "PostgreSQL debug: binding time result at index: " << index << std::endl;
343+
}
416344

417-
if (!(*is_null))
418-
{
419-
const auto time_string = _handle->result.getCharPtrValue(_handle->count, index);
345+
*is_null = _handle->result.isNull(_handle->count, index);
420346

421-
if (_handle->debug())
422-
{
423-
std::cerr << "PostgreSQL debug: got time string: " << time_string << std::endl;
424-
}
347+
if (!(*is_null))
348+
{
349+
const auto time_string = _handle->result.getCharPtrValue(_handle->count, index);
425350

426-
if (detail::check_time_digits(time_string))
427-
{
428-
*value += std::chrono::hours(std::atoi(time_string)) + std::chrono::minutes(std::atoi(time_string + 3)) +
429-
std::chrono::seconds(std::atoi(time_string + 6));
430-
}
431-
else
432-
{
433-
return;
434-
}
351+
if (_handle->debug())
352+
{
353+
std::cerr << "PostgreSQL debug: got time string: " << time_string << std::endl;
354+
}
435355

436-
if (std::strlen(time_string) <= 9)
437-
return;
438-
auto us_string = time_string + 9; // hh:mm:ss.
439-
int usec = 0;
440-
for (size_t i = 0u; i < 6u; ++i)
441-
{
442-
if (std::isdigit(us_string[0]))
443-
{
444-
usec = 10 * usec + (us_string[0] - '0');
445-
++us_string;
446-
}
447-
else
448-
usec *= 10;
356+
static const std::regex rx {
357+
"(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?"
358+
"(?:([+-])(\\d{2})(?::(\\d{2})(?::(\\d{2}))?)?)?"
359+
};
360+
std::cmatch mr;
361+
if (std::regex_match (time_string, mr, rx)) {
362+
*value =
363+
std::chrono::hours{std::atoi(time_string + mr.position(1))} + // Hour
364+
std::chrono::minutes{std::atoi(time_string + mr.position(2))} + // Minute
365+
std::chrono::seconds{std::atoi(time_string + mr.position(3))} + // Second
366+
::std::chrono::microseconds{ // Microsecond
367+
mr[4].matched ? std::stoi((mr[4].str() + "000000").substr(0, 6)) : 0
368+
};
369+
if (mr[5].matched) {
370+
const auto tz_sign = (time_string[mr.position(5)] == '+') ? 1 : -1;
371+
const auto tz_offset =
372+
std::chrono::hours{std::atoi(time_string + mr.position(6))} +
373+
std::chrono::minutes{mr[7].matched ? std::atoi(time_string + mr.position(7)) : 0} +
374+
std::chrono::seconds{mr[8].matched ? std::atoi(time_string + mr.position(8)) : 0};
375+
*value -= tz_sign * tz_offset;
449376
}
450-
*value += ::std::chrono::microseconds(usec);
377+
} else {
378+
if (_handle->debug()) {
379+
std::cerr << "PostgreSQL debug: got invalid time '" << time_string << "'" << std::endl;
380+
}
381+
*value = {};
451382
}
383+
}
452384
}
453385

454386
inline void bind_result_t::_bind_blob_result(size_t _index, const uint8_t** value, size_t* len)

tests/postgresql/usage/CMakeLists.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ set(test_files
3636
InsertOnConflict.cpp
3737
Returning.cpp
3838
Select.cpp
39+
TimeZone.cpp
3940
Transaction.cpp
4041
Type.cpp
4142
)
@@ -59,4 +60,4 @@ foreach(test_file IN LISTS test_files)
5960
add_test(NAME sqlpp11.postgresql.usage.${test}
6061
COMMAND sqlpp11_postgresql_tests ${test}
6162
)
62-
endforeach()
63+
endforeach()

0 commit comments

Comments
 (0)