Skip to content

Commit 2daa7f1

Browse files
authored
Merge pull request #758 from czj/perf/text-date-decoder-in-c
Decode text dates ~4x faster in C, with 1 allocation instead of 8
2 parents 74b79a2 + e35b468 commit 2daa7f1

3 files changed

Lines changed: 79 additions & 14 deletions

File tree

‎ext/pg_text_decoder.c‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ static ID s_id_utc;
4949
static ID s_id_getlocal;
5050
static ID s_id_BigDecimal;
5151

52+
static VALUE s_Date;
53+
static VALUE s_Date_GREGORIAN; /* Date::GREGORIAN */
5254
static VALUE s_IPAddr;
5355
static VALUE s_vmasks4;
5456
static VALUE s_vmasks6;
@@ -937,6 +939,51 @@ pg_text_dec_inet(t_pg_coder *conv, const char *val, int len, int tuple, int fiel
937939
return ip;
938940
}
939941

942+
/*
943+
* Document-class: PG::TextDecoder::Date < PG::SimpleDecoder
944+
*
945+
* This is a decoder class for conversion of PostgreSQL date type to Ruby Date values.
946+
*
947+
* Dates are expected in ISO format (+YYYY-MM-DD+), which is PostgreSQL's default DateStyle.
948+
* Other formats, BC dates, years beyond 9999 and the special values +infinity+ and +-infinity+
949+
* are returned as String.
950+
*
951+
* As soon as this class is used, it requires the ruby standard library 'date'.
952+
*/
953+
static VALUE
954+
pg_text_dec_date(t_pg_coder *conv, const char *val, int len, int tuple, int field, int enc_idx)
955+
{
956+
if (len == 10
957+
&& isdigit(val[0]) && isdigit(val[1]) && isdigit(val[2]) && isdigit(val[3])
958+
&& val[4] == '-' && isdigit(val[5]) && isdigit(val[6])
959+
&& val[7] == '-' && isdigit(val[8]) && isdigit(val[9])
960+
) {
961+
int year = str2_to_int(val) * 100 + str2_to_int(val + 2);
962+
int mon = str2_to_int(val + 5);
963+
int day = str2_to_int(val + 8);
964+
965+
return rb_funcall(s_Date, s_id_new, 4, INT2NUM(year), INT2NUM(mon), INT2NUM(day), s_Date_GREGORIAN);
966+
}
967+
968+
return pg_text_dec_string(conv, val, len, tuple, field, enc_idx);
969+
}
970+
971+
/* called per autoload when TextDecoder::Date is used */
972+
static VALUE
973+
init_pg_text_decoder_date(VALUE rb_mPG_TextDecoder)
974+
{
975+
rb_require("date");
976+
rb_gc_register_address(&s_Date);
977+
rb_gc_register_address(&s_Date_GREGORIAN);
978+
s_Date = rb_const_get(rb_cObject, rb_intern("Date"));
979+
s_Date_GREGORIAN = rb_const_get(s_Date, rb_intern("GREGORIAN"));
980+
981+
/* dummy = rb_define_class_under( rb_mPG_TextDecoder, "Date", rb_cPG_SimpleDecoder ); */
982+
pg_define_coder( "Date", pg_text_dec_date, rb_cPG_SimpleDecoder, rb_mPG_TextDecoder );
983+
984+
return Qnil;
985+
}
986+
940987
/* called per autoload when TextDecoder::Inet is used */
941988
static VALUE
942989
init_pg_text_decoder_inet(VALUE rb_mPG_TextDecoder)
@@ -982,6 +1029,7 @@ init_pg_text_decoder(void)
9821029

9831030
/* This module encapsulates all decoder classes with text input format */
9841031
rb_mPG_TextDecoder = rb_define_module_under( rb_mPG, "TextDecoder" );
1032+
rb_define_private_method(rb_singleton_class(rb_mPG_TextDecoder), "init_date", init_pg_text_decoder_date, 0);
9851033
rb_define_private_method(rb_singleton_class(rb_mPG_TextDecoder), "init_inet", init_pg_text_decoder_inet, 0);
9861034
rb_define_private_method(rb_singleton_class(rb_mPG_TextDecoder), "init_numeric", init_pg_text_decoder_numeric, 0);
9871035

‎lib/pg/text_decoder/date.rb‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
# -*- ruby -*-
22
# frozen_string_literal: true
33

4-
require 'date'
5-
64
module PG
75
module TextDecoder
8-
# This is a decoder class for conversion of PostgreSQL date type to Ruby Date values.
9-
#
10-
# As soon as this class is used, it requires the ruby standard library 'date'.
11-
class Date < SimpleDecoder
12-
def decode(string, tuple=nil, field=nil)
13-
if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/
14-
::Date.new $1.to_i, $2.to_i, $3.to_i, ::Date::GREGORIAN
15-
else
16-
string
17-
end
18-
end
19-
end
6+
# Init C part of the decoder
7+
init_date
208
end
219
end # module PG

‎spec/pg/type_spec.rb‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,35 @@ def textdec_timestamptz_decode_should_fail(str)
234234
end
235235
end
236236

237+
context 'dates' do
238+
let(:textdec_date) { PG::TextDecoder::Date.new }
239+
240+
it 'decodes ISO dates' do
241+
expect( textdec_date.decode('2016-01-02') ).to eq( Date.new(2016, 1, 2) )
242+
expect( textdec_date.decode('0001-12-31') ).to eq( Date.new(1, 12, 31, Date::GREGORIAN) )
243+
end
244+
it 'decodes dates in the proleptic gregorian calendar' do
245+
expect( textdec_date.decode('1581-12-15').start ).to eq( Date::GREGORIAN )
246+
end
247+
it 'returns special values, BC dates and years beyond 9999 as string' do
248+
%w[infinity -infinity].each do |value|
249+
expect( textdec_date.decode(value) ).to eq( value )
250+
end
251+
expect( textdec_date.decode('0044-03-15 BC') ).to eq( '0044-03-15 BC' )
252+
expect( textdec_date.decode('10000-01-01') ).to eq( '10000-01-01' )
253+
end
254+
it 'returns dates in other formats as string' do
255+
%w[01/02/2016 02.01.2016 2016-1-02 2016-01-2 2016/01/02 2016-01ª0].each do |value|
256+
expect( textdec_date.decode(value) ).to eq( value )
257+
end
258+
expect( textdec_date.decode(' 2016-01-02') ).to eq( ' 2016-01-02' )
259+
expect( textdec_date.decode('2016-01-02 ') ).to eq( '2016-01-02 ' )
260+
end
261+
it 'raises on invalid dates' do
262+
expect{ textdec_date.decode('2016-02-30') }.to raise_error( Date::Error )
263+
end
264+
end
265+
237266
context 'identifier quotation' do
238267
it 'should build an array out of an quoted identifier string' do
239268
quoted_type = PG::TextDecoder::Identifier.new

0 commit comments

Comments
 (0)