Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ext/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ module PG
"or upgrade your database to at least PostgreSQL-10."
# optional headers/functions
have_func 'PQresultMemorySize', 'libpq-fe.h' # since PostgreSQL-12
have_func 'timegm'
have_func 'rb_hash_new_capa' # since ruby-3.2

have_header 'inttypes.h'
Expand Down
106 changes: 56 additions & 50 deletions ext/pg_text_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
*
*/

#include "ruby/version.h"
#include "pg.h"
#include "pg_util.h"
#ifdef HAVE_INTTYPES_H
Expand Down Expand Up @@ -622,6 +621,25 @@ static int parse_year(const char **str) {
return year;
}

/*
* Seconds since 1970-01-01 00:00:00 UTC in the proleptic Gregorian calendar, like timegm() but much faster on macOS.
* Days are counted per http://howardhinnant.github.io/date_algorithms.html#days_from_civil
*/
static int64_t seconds_since_epoch(int year, int mon, int day, int hour, int min, int sec)
{
int64_t era, days;
int year_of_era, day_of_year, day_of_era;

year -= mon <= 2;
era = (year >= 0 ? year : year - 399) / 400;
year_of_era = (int)(year - era * 400);
day_of_year = (153 * (mon > 2 ? mon - 3 : mon + 9) + 2) / 5 + day - 1;
day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
days = era * 146097 + day_of_era - 719468;

return days * 86400 + hour * 3600 + min * 60 + sec;
}

#define TZ_NEG 1
#define TZ_POS 2

Expand Down Expand Up @@ -709,71 +727,59 @@ static VALUE pg_text_dec_timestamp(t_pg_coder *conv, const char *val, int len, i
VALUE sec_value;
VALUE gmt_offset_value;
VALUE res;

#if (RUBY_API_VERSION_MAJOR > 2 || (RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR >= 3)) && defined(HAVE_TIMEGM)
/* Fast path for time conversion */
struct tm tm;
struct timespec ts;
tm.tm_year = year - 1900;
tm.tm_mon = mon - 1;
tm.tm_mday = day;
tm.tm_hour = hour;
tm.tm_min = min;
tm.tm_sec = sec;
tm.tm_isdst = -1;
int gmt_offset = tz_hour * 3600 + tz_min * 60 + tz_sec;

if (tz_given) {
/* with timezone */
time_t time = timegm(&tm);
if (time != -1){
int gmt_offset;
if (tz_given == TZ_NEG)
{
gmt_offset = - gmt_offset;
}

gmt_offset = tz_hour * 3600 + tz_min * 60 + tz_sec;
if (tz_given == TZ_NEG)
{
gmt_offset = - gmt_offset;
}
ts.tv_sec = time - gmt_offset;
ts.tv_nsec = nsec;
return rb_time_timespec_new(&ts, gmt_offset);
/* Fast path for time conversion */
ts.tv_nsec = nsec;
if (!tz_given && (conv->flags & PG_CODER_TIMESTAMP_DB_LOCAL)) {
/* without timezone, interpreted as local time */
#ifndef _WIN32
/* Windows' mktime() fails on certain dates, so it takes the slow path */
struct tm tm;
tm.tm_year = year - 1900;
tm.tm_mon = mon - 1;
tm.tm_mday = day;
tm.tm_hour = hour;
tm.tm_min = min;
tm.tm_sec = sec;
tm.tm_isdst = -1;

ts.tv_sec = mktime(&tm);
if (ts.tv_sec != -1) {
return rb_time_timespec_new(&ts, conv->flags & PG_CODER_TIMESTAMP_APP_LOCAL ? INT_MAX : INT_MAX-1);
}
#endif
} else {
/* without timezone */
time_t time;

if( conv->flags & PG_CODER_TIMESTAMP_DB_LOCAL ) {
time = mktime(&tm);
} else {
time = timegm(&tm);
}
if (time != -1){
ts.tv_sec = time;
ts.tv_nsec = nsec;
return rb_time_timespec_new(&ts, conv->flags & PG_CODER_TIMESTAMP_APP_LOCAL ? INT_MAX : INT_MAX-1);
/* with timezone, or without timezone interpreted as UTC */
int64_t time = seconds_since_epoch(year, mon, day, hour, min, sec) - gmt_offset;
ts.tv_sec = (time_t)time;
if (ts.tv_sec == time) {
return rb_time_timespec_new(&ts, tz_given ? gmt_offset : conv->flags & PG_CODER_TIMESTAMP_APP_LOCAL ? INT_MAX : INT_MAX-1);
}
}
/* Some libc implementations fail to convert certain values,
/* mktime() fails on some values with some libc implementations,
* and a 32 bit time_t can't hold every PostgreSQL timestamp,
* so that we fall through to the slow path.
*/
#endif
if (nsec) {
int sec_numerator = sec * 1000000 + nsec / 1000;
int sec_denominator = 1000000;
if (nsec % 1000) {
sec_value = rb_funcall(Qnil, s_id_Rational, 2,
LL2NUM((LONG_LONG)sec * 1000000000 + nsec), INT2NUM(1000000000));
} else if (nsec) {
/* Counted in microseconds, the numerator stays a Fixnum where long is 32 bit, like on Windows */
sec_value = rb_funcall(Qnil, s_id_Rational, 2,
INT2NUM(sec_numerator), INT2NUM(sec_denominator));
INT2NUM(sec * 1000000 + nsec / 1000), INT2NUM(1000000));
} else {
sec_value = INT2NUM(sec);
}

if (tz_given) {
/* with timezone */
int gmt_offset;

gmt_offset = tz_hour * 3600 + tz_min * 60 + tz_sec;
if (tz_given == TZ_NEG)
{
gmt_offset = - gmt_offset;
}
gmt_offset_value = INT2NUM(gmt_offset);
} else {
/* without timezone */
Expand Down
14 changes: 14 additions & 0 deletions spec/pg/type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ def encode(value)
expect( textdec_timestamp.decode('1998-12-31 23:59:60.1234') ).
to be_within(0.000001).of( Time.new(1998,12,31, 23, 59, 60.1234) )
end
it 'decodes nanoseconds regardless of the year' do
expect( textdec_timestamputc.decode('1000-03-15 12:00:00.123456789').nsec ).to eq( 123456789 )
expect( textdec_timestamp.decode('1000-03-15 12:00:00.123456789').nsec ).to eq( 123456789 )
expect( textdec_timestamputc.decode('0044-03-15 12:00:00.123456789 BC').nsec ).to eq( 123456789 )
expect( textdec_timestamptz.decode('0044-03-15 12:00:00.123456789+02 BC').nsec ).to eq( 123456789 )
expect( textdec_timestamptz.decode('294276-12-31 23:58:59.123456789+03').nsec ).to eq( 123456789 )
end
it 'decodes timestamps around february, leap years and centuries' do
[[2000, 2, 29], [2016, 2, 15], [2024, 1, 31], [2024, 3, 1], [1900, 2, 28], [1900, 3, 1], [2100, 3, 1], [1600, 2, 29], [-43, 2, 28], [-43, 3, 1]].each do |year, month, day|
text = year > 0 ? format("%04d-%02d-%02d 12:34:56", year, month, day) : format("%04d-%02d-%02d 12:34:56 BC", 1 - year, month, day)
expect( textdec_timestamputc.decode(text) ).to eq( Time.utc(year, month, day, 12, 34, 56) )
expect( textdec_timestamptz.decode("#{text.delete_suffix(" BC")}+05#{" BC" if year <= 0}") ).to eq( Time.new(year, month, day, 12, 34, 56, "+05:00") )
end
end

def textdec_timestamptz_decode_should_fail(str)
expect(textdec_timestamptz.decode(str)).to eq(str)
Expand Down
Loading