diff --git a/ext/pg_binary_decoder.c b/ext/pg_binary_decoder.c index 6b6c06e30..18f07d08d 100644 --- a/ext/pg_binary_decoder.c +++ b/ext/pg_binary_decoder.c @@ -13,6 +13,7 @@ VALUE rb_mPG_BinaryDecoder; static VALUE s_Date; +static VALUE s_Date_GREGORIAN; /* Date::GREGORIAN */ static ID s_id_new; @@ -403,7 +404,7 @@ pg_bin_dec_date(t_pg_coder *conv, const char *val, int len, int tuple, int field default: j2date(date + POSTGRES_EPOCH_JDATE, &year, &month, &day); - return rb_funcall(s_Date, s_id_new, 3, INT2NUM(year), INT2NUM(month), INT2NUM(day)); + return rb_funcall(s_Date, s_id_new, 4, INT2NUM(year), INT2NUM(month), INT2NUM(day), s_Date_GREGORIAN); } } @@ -412,8 +413,10 @@ static VALUE init_pg_bin_decoder_date(VALUE rb_mPG_BinaryDecoder) { rb_require("date"); + rb_gc_register_address(&s_Date); + rb_gc_register_address(&s_Date_GREGORIAN); s_Date = rb_const_get(rb_cObject, rb_intern("Date")); - rb_gc_register_mark_object(s_Date); + s_Date_GREGORIAN = rb_const_get(s_Date, rb_intern("GREGORIAN")); s_id_new = rb_intern("new"); /* dummy = rb_define_class_under( rb_mPG_BinaryDecoder, "Date", rb_cPG_SimpleDecoder ); */ diff --git a/ext/pg_binary_encoder.c b/ext/pg_binary_encoder.c index 9417fe8b3..df8ba854b 100644 --- a/ext/pg_binary_encoder.c +++ b/ext/pg_binary_encoder.c @@ -11,6 +11,8 @@ #endif VALUE rb_mPG_BinaryEncoder; +static ID s_id_gregorianP; +static ID s_id_gregorian; static ID s_id_year; static ID s_id_month; static ID s_id_day; @@ -272,6 +274,9 @@ pg_bin_enc_date(t_pg_coder *this, VALUE value, char *out, VALUE *intermediate, i write_nbo32(PG_INT32_MIN, out); return 4; } { + /* Only create a new gregorian Date object if necessary */ + if( rb_funcall(value, s_id_gregorianP, 0) != Qtrue ) + value = rb_funcall(value, s_id_gregorian, 0); VALUE year = rb_funcall(value, s_id_year, 0); VALUE month = rb_funcall(value, s_id_month, 0); VALUE day = rb_funcall(value, s_id_day, 0); @@ -554,6 +559,8 @@ pg_bin_enc_from_base64(t_pg_coder *conv, VALUE value, char *out, VALUE *intermed void init_pg_binary_encoder(void) { + s_id_gregorianP = rb_intern("gregorian?"); + s_id_gregorian = rb_intern("gregorian"); s_id_year = rb_intern("year"); s_id_month = rb_intern("month"); s_id_day = rb_intern("day"); diff --git a/lib/pg/basic_type_map_for_queries.rb b/lib/pg/basic_type_map_for_queries.rb index 3b0d492d9..d7e9fc326 100644 --- a/lib/pg/basic_type_map_for_queries.rb +++ b/lib/pg/basic_type_map_for_queries.rb @@ -184,6 +184,7 @@ def get_array_type(value) Integer => [0, 'int8'], Float => [0, 'float8'], Time => [0, 'timestamptz'], + Date => [0, 'date'], # We use text format and no type OID for IPAddr, because setting the OID can lead # to unnecessary inet/cidr conversions on the server side. IPAddr => [0, 'inet'], @@ -200,6 +201,7 @@ def get_array_type(value) String => [0, '_text'], Float => [0, '_float8'], Time => [0, '_timestamptz'], + Date => [0, '_date'], IPAddr => [0, '_inet'], }.merge(has_bigdecimal ? {BigDecimal => [0, '_numeric']} : {})) private_constant :DEFAULT_ARRAY_TYPE_MAP diff --git a/lib/pg/text_decoder/date.rb b/lib/pg/text_decoder/date.rb index 75d0b6100..6fc4cf4a0 100644 --- a/lib/pg/text_decoder/date.rb +++ b/lib/pg/text_decoder/date.rb @@ -11,7 +11,7 @@ module TextDecoder class Date < SimpleDecoder def decode(string, tuple=nil, field=nil) if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/ - ::Date.new $1.to_i, $2.to_i, $3.to_i + ::Date.new $1.to_i, $2.to_i, $3.to_i, ::Date::GREGORIAN else string end diff --git a/lib/pg/text_encoder/date.rb b/lib/pg/text_encoder/date.rb index fa8b0d62c..a2f827560 100644 --- a/lib/pg/text_encoder/date.rb +++ b/lib/pg/text_encoder/date.rb @@ -6,7 +6,13 @@ module TextEncoder # This is a encoder class for conversion of Ruby Date values to PostgreSQL date type. class Date < SimpleEncoder def encode(value) - value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value + if value.respond_to?(:gregorian?) + # Only create a new gregorian Date object if necessary + value = value.gregorian unless value.gregorian? + value.strftime("%Y-%m-%d") + else + value + end end end end diff --git a/spec/pg/basic_type_map_based_on_result_spec.rb b/spec/pg/basic_type_map_based_on_result_spec.rb index 0d0666f24..6e10554b9 100644 --- a/spec/pg/basic_type_map_based_on_result_spec.rb +++ b/spec/pg/basic_type_map_based_on_result_spec.rb @@ -127,7 +127,7 @@ [1, 0].each do |format| it "can type cast #copy_data input with encoder to format #{format}" do - @conn.exec( "CREATE TEMP TABLE copytable (b bytea, i INT, ts1 timestamp, ts2 timestamp, f4 float4, f8 float8, d1 date, d2 date)" ) + @conn.exec( "CREATE TEMP TABLE copytable (b bytea, i INT, ts1 timestamp, ts2 timestamp, f4 float4, f8 float8, d1 date, d2 date, d3 date)" ) # Retrieve table OIDs per empty result set. res = @conn.exec_params( "SELECT * FROM copytable LIMIT 0", [], format ) @@ -136,13 +136,13 @@ row_encoder = nsp::CopyRow.new type_map: tm @conn.copy_data( "COPY copytable FROM STDIN WITH (FORMAT #{ format==1 ? "binary" : "text" })", row_encoder ) do |res| - @conn.put_copy_data ["\xff\x00\n\r'", 123, Time.utc(2023, 3, 17, 3, 4, 5.6789123), Time.new(1990, 12, 17, 18, 44, 45, "+03:30").utc, 12.345, -12.345e167, Date.new(2055, 12, 31), Date.new(1234, 8, 31)] - @conn.put_copy_data [" xyz ", -444, "Infinity", "-infinity", -Float::INFINITY, Float::NAN, "infinity", "-infinity"] + @conn.put_copy_data ["\xff\x00\n\r'", 123, Time.utc(2023, 3, 17, 3, 4, 5.6789123), Time.new(1990, 12, 17, 18, 44, 45, "+03:30").utc, 12.345, -12.345e167, Date.new(2055, 12, 31), Date.new(1234, 8, 15), Date.new(1234, 8, 15, ::Date::GREGORIAN)] + @conn.put_copy_data [" xyz ", -444, "Infinity", "-infinity", -Float::INFINITY, Float::NAN, "infinity", "-infinity", nil] end res = @conn.exec( "SELECT * FROM copytable" ) expect( res.values ).to eq( [ - ["\\xff000a0d27", "123", "2023-03-17 03:04:05.678912", "1990-12-17 15:14:45", "12.345", "-1.2345e+168", "2055-12-31", "1234-08-31"], - ["\\x202078797a2020", "-444", "infinity", "-infinity", "-Infinity", "NaN", "infinity", "-infinity"] + ["\\xff000a0d27", "123", "2023-03-17 03:04:05.678912", "1990-12-17 15:14:45", "12.345", "-1.2345e+168", "2055-12-31", "1234-08-22", "1234-08-15"], + ["\\x202078797a2020", "-444", "infinity", "-infinity", "-Infinity", "NaN", "infinity", "-infinity", nil] ] ) end end diff --git a/spec/pg/basic_type_map_for_results_spec.rb b/spec/pg/basic_type_map_for_results_spec.rb index d3176c43c..d1cbe9e07 100644 --- a/spec/pg/basic_type_map_for_results_spec.rb +++ b/spec/pg/basic_type_map_for_results_spec.rb @@ -244,12 +244,15 @@ it "should do format #{format} date type conversions" do res = @conn.exec_params( "SELECT CAST('2113-12-31' AS DATE), CAST('1913-12-31' AS DATE), + CAST('1581-12-15' AS DATE), CAST('infinity' AS DATE), CAST('-infinity' AS DATE)", [], format ) expect( res.getvalue(0,0) ).to eq( Date.new(2113, 12, 31) ) expect( res.getvalue(0,1) ).to eq( Date.new(1913, 12, 31) ) - expect( res.getvalue(0,2) ).to eq( 'infinity' ) - expect( res.getvalue(0,3) ).to eq( '-infinity' ) + expect( res.getvalue(0,2) ).to eq( Date.new(1581, 12, 15, Date::GREGORIAN) ) + expect( res.getvalue(0,2) ).to eq( Date.new(1581, 12, 5, Date::JULIAN) ) + expect( res.getvalue(0,3) ).to eq( 'infinity' ) + expect( res.getvalue(0,4) ).to eq( '-infinity' ) end end