Skip to content

Commit

Permalink
Merge pull request #515 from larskanis/bin-date
Browse files Browse the repository at this point in the history
Add PG::BinaryEncoder::Date and BinaryDecoder::Date
  • Loading branch information
larskanis authored Mar 28, 2023
2 parents 86485ce + 6f37dd4 commit 430dcb6
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 20 deletions.
6 changes: 4 additions & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ Binary Encoder、BD = Binary Decoder)。
[UTC](rdoc-ref:PG::BinaryEncoder::TimestampUtc)
* BD:
[現地時間](rdoc-ref:PG::BinaryDecoder::TimestampLocal)[UTC](rdoc-ref:PG::BinaryDecoder::TimestampUtc)[UTCから現地時間へ](rdoc-ref:PG::BinaryDecoder::TimestampUtcToLocal)
* Date:
[TE](rdoc-ref:PG::TextEncoder::Date)[TD](rdoc-ref:PG::TextDecoder::Date)
* Date: [TE](rdoc-ref:PG::TextEncoder::Date),
[TD](rdoc-ref:PG::TextDecoder::Date),
[BE](rdoc-ref:PG::BinaryEncoder::Date),
[BD](rdoc-ref:PG::BinaryDecoder::Date)
* JSONとJSONB:
[TE](rdoc-ref:PG::TextEncoder::JSON)[TD](rdoc-ref:PG::TextDecoder::JSON)
* Inet:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ The following PostgreSQL column types are supported by ruby-pg (TE = Text Encode
* TD: [local](rdoc-ref:PG::TextDecoder::TimestampLocal), [UTC](rdoc-ref:PG::TextDecoder::TimestampUtc), [UTC-to-local](rdoc-ref:PG::TextDecoder::TimestampUtcToLocal)
* BE: [local](rdoc-ref:PG::BinaryEncoder::TimestampLocal), [UTC](rdoc-ref:PG::BinaryEncoder::TimestampUtc)
* BD: [local](rdoc-ref:PG::BinaryDecoder::TimestampLocal), [UTC](rdoc-ref:PG::BinaryDecoder::TimestampUtc), [UTC-to-local](rdoc-ref:PG::BinaryDecoder::TimestampUtcToLocal)
* Date: [TE](rdoc-ref:PG::TextEncoder::Date), [TD](rdoc-ref:PG::TextDecoder::Date)
* Date: [TE](rdoc-ref:PG::TextEncoder::Date), [TD](rdoc-ref:PG::TextDecoder::Date), [BE](rdoc-ref:PG::BinaryEncoder::Date), [BD](rdoc-ref:PG::BinaryDecoder::Date)
* JSON and JSONB: [TE](rdoc-ref:PG::TextEncoder::JSON), [TD](rdoc-ref:PG::TextDecoder::JSON)
* Inet: [TE](rdoc-ref:PG::TextEncoder::Inet), [TD](rdoc-ref:PG::TextDecoder::Inet)
* Array: [TE](rdoc-ref:PG::TextEncoder::Array), [TD](rdoc-ref:PG::TextDecoder::Array)
Expand Down
79 changes: 79 additions & 0 deletions ext/pg_binary_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#endif

VALUE rb_mPG_BinaryDecoder;
static VALUE s_Date;
static ID s_id_new;


/*
Expand Down Expand Up @@ -195,6 +197,82 @@ pg_bin_dec_timestamp(t_pg_coder *conv, const char *val, int len, int tuple, int
}
}

#define PG_INT32_MIN (-0x7FFFFFFF-1)
#define PG_INT32_MAX (0x7FFFFFFF)
#define POSTGRES_EPOCH_JDATE 2451545 /* == date2j(2000, 1, 1) */
#define MONTHS_PER_YEAR 12

/* taken from PostgreSQL sources at src/backend/utils/adt/datetime.c */
void
j2date(int jd, int *year, int *month, int *day)
{
unsigned int julian;
unsigned int quad;
unsigned int extra;
int y;

julian = jd;
julian += 32044;
quad = julian / 146097;
extra = (julian - quad * 146097) * 4 + 3;
julian += 60 + quad * 3 + extra / 146097;
quad = julian / 1461;
julian -= quad * 1461;
y = julian * 4 / 1461;
julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366))
+ 123;
y += quad * 4;
*year = y - 4800;
quad = julian * 2141 / 65536;
*day = julian - 7834 * quad / 256;
*month = (quad + 10) % MONTHS_PER_YEAR + 1;
} /* j2date() */

/*
* Document-class: PG::BinaryDecoder::Date < PG::SimpleDecoder
*
* This is a decoder class for conversion of PostgreSQL binary date
* to Ruby Date objects.
*/
static VALUE
pg_bin_dec_date(t_pg_coder *conv, const char *val, int len, int tuple, int field, int enc_idx)
{
int year, month, day;
int date;

if (len != 4) {
rb_raise(rb_eTypeError, "unexpected date format != 4 bytes");
}

date = read_nbo32(val);
switch(date){
case PG_INT32_MAX:
return rb_str_new2("infinity");
case PG_INT32_MIN:
return rb_str_new2("-infinity");
default:
j2date(date + POSTGRES_EPOCH_JDATE, &year, &month, &day);

return rb_funcall(s_Date, s_id_new, 3, INT2NUM(year), INT2NUM(month), INT2NUM(day));
}
}

/* called per autoload when BinaryDecoder::Date is used */
static VALUE
init_pg_bin_decoder_date(VALUE rb_mPG_BinaryDecoder)
{
rb_require("date");
s_Date = rb_const_get(rb_cObject, rb_intern("Date"));
rb_gc_register_mark_object(s_Date);
s_id_new = rb_intern("new");

/* dummy = rb_define_class_under( rb_mPG_BinaryDecoder, "Date", rb_cPG_SimpleDecoder ); */
pg_define_coder( "Date", pg_bin_dec_date, rb_cPG_SimpleDecoder, rb_mPG_BinaryDecoder );

return Qnil;
}


/*
* Document-class: PG::BinaryDecoder::String < PG::SimpleDecoder
*
Expand All @@ -209,6 +287,7 @@ init_pg_binary_decoder(void)
{
/* This module encapsulates all decoder classes with binary input format */
rb_mPG_BinaryDecoder = rb_define_module_under( rb_mPG, "BinaryDecoder" );
rb_define_private_method(rb_singleton_class(rb_mPG_BinaryDecoder), "init_date", init_pg_bin_decoder_date, 0);

/* Make RDoc aware of the decoder classes... */
/* dummy = rb_define_class_under( rb_mPG_BinaryDecoder, "Boolean", rb_cPG_SimpleDecoder ); */
Expand Down
93 changes: 93 additions & 0 deletions ext/pg_binary_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
#endif

VALUE rb_mPG_BinaryEncoder;
static ID s_id_year;
static ID s_id_month;
static ID s_id_day;


/*
Expand Down Expand Up @@ -139,6 +142,8 @@ pg_bin_enc_float8(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
return 8;
}

#define PG_INT32_MIN (-0x7FFFFFFF-1)
#define PG_INT32_MAX (0x7FFFFFFF)
#define PG_INT64_MIN (-0x7FFFFFFFFFFFFFFFL - 1)
#define PG_INT64_MAX 0x7FFFFFFFFFFFFFFFL

Expand Down Expand Up @@ -218,6 +223,88 @@ pg_bin_enc_timestamp(t_pg_coder *this, VALUE value, char *out, VALUE *intermedia
return 8;
}

#define POSTGRES_EPOCH_JDATE 2451545 /* == date2j(2000, 1, 1) */
int
date2j(int year, int month, int day)
{
int julian;
int century;

if (month > 2)
{
month += 1;
year += 4800;
}
else
{
month += 13;
year += 4799;
}

century = year / 100;
julian = year * 365 - 32167;
julian += year / 4 - century + century / 4;
julian += 7834 * month / 256 + day;

return julian;
} /* date2j() */

/*
* Document-class: PG::BinaryEncoder::Date < PG::SimpleEncoder
*
* This is a encoder class for conversion of Ruby Date objects to PostgreSQL binary date.
*
* String values are expected to contain a binary data with a length of 4 byte.
*
*/
static int
pg_bin_enc_date(t_pg_coder *this, VALUE value, char *out, VALUE *intermediate, int enc_idx)
{
if(out){
/* second call -> write data to *out */
switch(TYPE(*intermediate)){
case T_STRING:
return pg_coder_enc_to_s(this, value, out, intermediate, enc_idx);
case T_TRUE:
write_nbo32(PG_INT32_MAX, out);
return 4;
case T_FALSE:
write_nbo32(PG_INT32_MIN, out);
return 4;
}

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);
int jday = date2j(NUM2INT(year), NUM2INT(month), NUM2INT(day)) - POSTGRES_EPOCH_JDATE;
write_nbo32(jday, out);

}else{
/* first call -> determine the required length */
if(TYPE(value) == T_STRING){
char *pstr = RSTRING_PTR(value);
if(RSTRING_LEN(value) >= 1){
switch(pstr[0]) {
case 'I':
case 'i':
*intermediate = Qtrue;
return 4;
case '-':
if (RSTRING_LEN(value) >= 2 && (pstr[1] == 'I' || pstr[1] == 'i')) {
*intermediate = Qfalse;
return 4;
}
}
}

return pg_coder_enc_to_s(this, value, out, intermediate, enc_idx);
}

*intermediate = value;
}
return 4;
}

/*
* Document-class: PG::BinaryEncoder::FromBase64 < PG::CompositeEncoder
*
Expand Down Expand Up @@ -266,6 +353,10 @@ pg_bin_enc_from_base64(t_pg_coder *conv, VALUE value, char *out, VALUE *intermed
void
init_pg_binary_encoder(void)
{
s_id_year = rb_intern("year");
s_id_month = rb_intern("month");
s_id_day = rb_intern("day");

/* This module encapsulates all encoder classes with binary output format */
rb_mPG_BinaryEncoder = rb_define_module_under( rb_mPG, "BinaryEncoder" );

Expand All @@ -288,6 +379,8 @@ init_pg_binary_encoder(void)
pg_define_coder( "Bytea", pg_coder_enc_to_s, rb_cPG_SimpleEncoder, rb_mPG_BinaryEncoder );
/* dummy = rb_define_class_under( rb_mPG_BinaryEncoder, "Timestamp", rb_cPG_SimpleEncoder ); */
pg_define_coder( "Timestamp", pg_bin_enc_timestamp, rb_cPG_SimpleEncoder, rb_mPG_BinaryEncoder );
/* dummy = rb_define_class_under( rb_mPG_BinaryEncoder, "Date", rb_cPG_SimpleEncoder ); */
pg_define_coder( "Date", pg_bin_enc_date, rb_cPG_SimpleEncoder, rb_mPG_BinaryEncoder );

/* dummy = rb_define_class_under( rb_mPG_BinaryEncoder, "FromBase64", rb_cPG_CompositeEncoder ); */
pg_define_coder( "FromBase64", pg_bin_enc_from_base64, rb_cPG_CompositeEncoder, rb_mPG_BinaryEncoder );
Expand Down
1 change: 1 addition & 0 deletions lib/pg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module BinaryDecoder
%i[ TimestampUtc TimestampUtcToLocal TimestampLocal ].each do |klass|
autoload klass, 'pg/binary_decoder/timestamp'
end
autoload :Date, 'pg/binary_decoder/date'
end
module BinaryEncoder
%i[ TimestampUtc TimestampLocal ].each do |klass|
Expand Down
1 change: 1 addition & 0 deletions lib/pg/basic_type_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ def register_default_types
register_type 1, 'float8', PG::BinaryEncoder::Float8, PG::BinaryDecoder::Float
register_type 1, 'timestamp', PG::BinaryEncoder::TimestampUtc, PG::BinaryDecoder::TimestampUtc
register_type 1, 'timestamptz', PG::BinaryEncoder::TimestampUtc, PG::BinaryDecoder::TimestampUtcToLocal
register_type 1, 'date', PG::BinaryEncoder::Date, PG::BinaryDecoder::Date

self
end
Expand Down
9 changes: 9 additions & 0 deletions lib/pg/binary_decoder/date.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# -*- ruby -*-
# frozen_string_literal: true

module PG
module BinaryDecoder
# Init C part of the decoder
init_date
end
end # module PG
10 changes: 5 additions & 5 deletions spec/pg/basic_type_map_based_on_result_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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)" )
@conn.exec( "CREATE TEMP TABLE copytable (b bytea, i INT, ts1 timestamp, ts2 timestamp, f4 float4, f8 float8, d1 date, d2 date)" )

# Retrieve table OIDs per empty result set.
res = @conn.exec_params( "SELECT * FROM copytable LIMIT 0", [], format )
Expand All @@ -101,13 +101,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]
@conn.put_copy_data [" xyz ", -444, "Infinity", "-infinity", -Float::INFINITY, Float::NAN]
@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"]
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"],
["\\x202078797a2020", "-444", "infinity", "-infinity", "-Infinity", "NaN"]
["\\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"]
] )
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/pg/basic_type_map_for_results_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@
end
end

it "should do date type conversions" do
[0].each do |format|
[0, 1].each do |format|
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('infinity' AS DATE),
Expand Down
6 changes: 4 additions & 2 deletions translation/po/all.pot
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Pg 1.4.6\n"
"POT-Creation-Date: 2023-03-24 13:35+0100\n"
"POT-Creation-Date: 2023-03-28 08:27+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -448,7 +448,9 @@ msgstr ""
#, markdown-text
msgid ""
"Date: [TE](rdoc-ref:PG::TextEncoder::Date), "
"[TD](rdoc-ref:PG::TextDecoder::Date)"
"[TD](rdoc-ref:PG::TextDecoder::Date), "
"[BE](rdoc-ref:PG::BinaryEncoder::Date), "
"[BD](rdoc-ref:PG::BinaryDecoder::Date)"
msgstr ""

#. type: Bullet: '* '
Expand Down
22 changes: 14 additions & 8 deletions translation/po/ja.po
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Pg 1.4.5\n"
"POT-Creation-Date: 2023-03-24 13:35+0100\n"
"POT-Creation-Date: 2023-03-28 08:27+0200\n"
"PO-Revision-Date: 2023-02-28 21:24+0900\n"
"Last-Translator: gemmaro <gemmaro.dev@gmail.com>\n"
"Language-Team: none\n"
Expand Down Expand Up @@ -534,12 +534,19 @@ msgstr ""

#. type: Bullet: '* '
#: ../README.md:146
#, fuzzy
#| msgid ""
#| "Bytea: [TE](rdoc-ref:PG::TextEncoder::Bytea), [TD](rdoc-ref:PG::"
#| "TextDecoder::Bytea), [BE](rdoc-ref:PG::BinaryEncoder::Bytea), [BD](rdoc-"
#| "ref:PG::BinaryDecoder::Bytea)"
msgid ""
"Date: [TE](rdoc-ref:PG::TextEncoder::Date), [TD](rdoc-ref:PG::TextDecoder::"
"Date)"
"Date), [BE](rdoc-ref:PG::BinaryEncoder::Date), [BD](rdoc-ref:PG::"
"BinaryDecoder::Date)"
msgstr ""
"Date: [TE](rdoc-ref:PG::TextEncoder::Date)、[TD](rdoc-ref:PG::TextDecoder::"
"Date)"
"Bytea: [TE](rdoc-ref:PG::TextEncoder::Bytea)、[TD](rdoc-ref:PG::TextDecoder::"
"Bytea)、[BE](rdoc-ref:PG::BinaryEncoder::Bytea)、[BD](rdoc-ref:PG::"
"BinaryDecoder::Bytea)"

#. type: Bullet: '* '
#: ../README.md:146
Expand Down Expand Up @@ -596,10 +603,9 @@ msgid ""
"(rdoc-ref:PG::TextDecoder::CopyRow), [BE](rdoc-ref:PG::BinaryEncoder::"
"CopyRow), [BD](rdoc-ref:PG::BinaryDecoder::CopyRow)"
msgstr ""
"COPYの入出力データ:[TE](rdoc-ref:PG::TextEncoder::CopyRow)、[TD](rdoc-"
"ref:PG::TextDecoder::CopyRow), "
"[BE](rdoc-ref:PG::BinaryEncoder::CopyRow), "
"[BD](rdoc-ref:PG::BinaryDecoder::CopyRow)"
"COPYの入出力データ:[TE](rdoc-ref:PG::TextEncoder::CopyRow)、[TD](rdoc-ref:"
"PG::TextDecoder::CopyRow), [BE](rdoc-ref:PG::BinaryEncoder::CopyRow), [BD]"
"(rdoc-ref:PG::BinaryDecoder::CopyRow)"

#. type: Bullet: '* '
#: ../README.md:152
Expand Down

0 comments on commit 430dcb6

Please sign in to comment.