Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add possibility to define the number of array dimensions to be encoded #622

Merged
merged 1 commit into from
Jan 10, 2025
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: 1 addition & 0 deletions ext/pg.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ typedef struct {
t_pg_coder comp;
t_pg_coder *elem;
int needs_quotation;
int dimensions;
char delimiter;
} t_pg_composite_coder;

Expand Down
9 changes: 8 additions & 1 deletion ext/pg_binary_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ pg_bin_enc_date(t_pg_coder *this, VALUE value, char *out, VALUE *intermediate, i
* This encoder expects an Array of values or sub-arrays as input.
* Other values are passed through as byte string without interpretation.
*
* It is possible to enforce a number of dimensions to be encoded by #dimensions= .
* Deeper nested arrays are then passed to the elements encoder and less nested arrays raise an ArgumentError.
*
* The accessors needs_quotation and delimiter are ignored for binary encoding.
*
*/
Expand All @@ -346,7 +349,8 @@ pg_bin_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
dim_sizes[ndim-1] = RARRAY_LENINT(el1);
nitems *= dim_sizes[ndim-1];
el2 = rb_ary_entry(el1, 0);
if (TYPE(el2) == T_ARRAY) {
if ( (this->dimensions < 0 || ndim < this->dimensions) &&
TYPE(el2) == T_ARRAY) {
ndim++;
if (ndim > MAXDIM)
rb_raise( rb_eArgError, "unsupported number of array dimensions: >%d", ndim );
Expand All @@ -356,6 +360,9 @@ pg_bin_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
el1 = el2;
}
}
if( this->dimensions >= 0 && (ndim==0 ? 1 : ndim) != this->dimensions ){
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", ndim, this->dimensions);
}

if(out){
/* Second encoder pass -> write data to `out` */
Expand Down
49 changes: 49 additions & 0 deletions ext/pg_coder.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ pg_composite_encoder_allocate( VALUE klass )
this->elem = NULL;
this->needs_quotation = 1;
this->delimiter = ',';
this->dimensions = -1;
rb_iv_set( self, "@elements_type", Qnil );
return self;
}
Expand All @@ -157,6 +158,7 @@ pg_composite_decoder_allocate( VALUE klass )
this->elem = NULL;
this->needs_quotation = 1;
this->delimiter = ',';
this->dimensions = -1;
rb_iv_set( self, "@elements_type", Qnil );
return self;
}
Expand Down Expand Up @@ -421,6 +423,49 @@ pg_coder_delimiter_get(VALUE self)
return rb_str_new(&this->delimiter, 1);
}

/*
* call-seq:
* coder.dimensions = Integer
* coder.dimensions = nil
*
* Set number of array dimensions to be encoded.
*
* This property ensures, that this number of dimensions is always encoded.
* If less dimensions than this number are in the given value, an ArgumentError is raised.
* If more dimensions than this number are in the value, the Array value is passed to the next encoder.
*
* Setting dimensions is especially useful, when a Record shall be encoded into an Array, since the Array encoder can not distinguish if the array shall be encoded as a higher dimension or as a record otherwise.
*
* The default is +nil+.
*
* See #dimensions
*/
static VALUE
pg_coder_dimensions_set(VALUE self, VALUE dimensions)
{
t_pg_composite_coder *this = RTYPEDDATA_DATA(self);
rb_check_frozen(self);
if(!NIL_P(dimensions) && NUM2INT(dimensions) < 0)
rb_raise( rb_eArgError, "dimensions must be nil or >= 0");
this->dimensions = NIL_P(dimensions) ? -1 : NUM2INT(dimensions);
return dimensions;
}

/*
* call-seq:
* coder.dimensions -> Integer | nil
*
* Get number of enforced array dimensions or +nil+ if not set.
*
* See #dimensions=
*/
static VALUE
pg_coder_dimensions_get(VALUE self)
{
t_pg_composite_coder *this = RTYPEDDATA_DATA(self);
return this->dimensions < 0 ? Qnil : INT2NUM(this->dimensions);
}

/*
* call-seq:
* coder.elements_type = coder
Expand Down Expand Up @@ -602,6 +647,8 @@ init_pg_coder(void)
*
* This is the base class for all type cast classes of PostgreSQL types,
* that are made up of some sub type.
*
* See PG::TextEncoder::Array, PG::TextDecoder::Array, PG::BinaryEncoder::Array, PG::BinaryDecoder::Array, etc.
*/
rb_cPG_CompositeCoder = rb_define_class_under( rb_mPG, "CompositeCoder", rb_cPG_Coder );
rb_define_method( rb_cPG_CompositeCoder, "elements_type=", pg_coder_elements_type_set, 1 );
Expand All @@ -610,6 +657,8 @@ init_pg_coder(void)
rb_define_method( rb_cPG_CompositeCoder, "needs_quotation?", pg_coder_needs_quotation_get, 0 );
rb_define_method( rb_cPG_CompositeCoder, "delimiter=", pg_coder_delimiter_set, 1 );
rb_define_method( rb_cPG_CompositeCoder, "delimiter", pg_coder_delimiter_get, 0 );
rb_define_method( rb_cPG_CompositeCoder, "dimensions=", pg_coder_dimensions_set, 1 );
rb_define_method( rb_cPG_CompositeCoder, "dimensions", pg_coder_dimensions_get, 0 );

/* Document-class: PG::CompositeEncoder < PG::CompositeCoder */
rb_cPG_CompositeEncoder = rb_define_class_under( rb_mPG, "CompositeEncoder", rb_cPG_CompositeCoder );
Expand Down
23 changes: 18 additions & 5 deletions ext/pg_text_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -537,14 +537,18 @@ quote_string(t_pg_coder *this, VALUE value, VALUE string, char *current_out, int
}

static char *
write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE string, int quote, int enc_idx)
write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE string, int quote, int enc_idx, int dimension)
{
int i;

/* size of "{}" */
current_out = pg_rb_str_ensure_capa( string, 2, current_out, NULL );
*current_out++ = '{';

if( RARRAY_LEN(value) == 0 && this->dimensions >= 0 && dimension != this->dimensions ){
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", dimension, this->dimensions);
}

for( i=0; i<RARRAY_LEN(value); i++){
VALUE entry = rb_ary_entry(value, i);

Expand All @@ -554,17 +558,26 @@ write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE st
}

switch(TYPE(entry)){
case T_ARRAY:
current_out = write_array(this, entry, current_out, string, quote, enc_idx);
break;
case T_NIL:
if( this->dimensions >= 0 && dimension != this->dimensions ){
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", dimension, this->dimensions);
}
current_out = pg_rb_str_ensure_capa( string, 4, current_out, NULL );
*current_out++ = 'N';
*current_out++ = 'U';
*current_out++ = 'L';
*current_out++ = 'L';
break;
case T_ARRAY:
if( this->dimensions < 0 || dimension < this->dimensions ){
current_out = write_array(this, entry, current_out, string, quote, enc_idx, dimension+1);
break;
}
/* Number of dimensions reached -> handle array as normal value */
default:
if( this->dimensions >= 0 && dimension != this->dimensions ){
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", dimension, this->dimensions);
}
current_out = quote_string( this->elem, entry, string, current_out, quote, quote_array_buffer, this, enc_idx );
}
}
Expand Down Expand Up @@ -596,7 +609,7 @@ pg_text_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
VALUE out_str = rb_str_new(NULL, 0);
PG_ENCODING_SET_NOCHECK(out_str, enc_idx);

end_ptr = write_array(this, value, RSTRING_PTR(out_str), out_str, this->needs_quotation, enc_idx);
end_ptr = write_array(this, value, RSTRING_PTR(out_str), out_str, this->needs_quotation, enc_idx, 1);

rb_str_set_len( out_str, end_ptr - RSTRING_PTR(out_str) );
*intermediate = out_str;
Expand Down
3 changes: 2 additions & 1 deletion lib/pg/coder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ def to_h
elements_type: elements_type,
needs_quotation: needs_quotation?,
delimiter: delimiter,
dimensions: dimensions,
}
end

def inspect
str = super
str[-1,0] = " elements_type=#{elements_type.inspect} #{needs_quotation? ? 'needs' : 'no'} quotation"
str[-1,0] = " elements_type=#{elements_type.inspect} #{needs_quotation? ? 'needs' : 'no'} quotation#{dimensions && " #{dimensions} dimensions"}"
str
end
end
Expand Down
98 changes: 97 additions & 1 deletion spec/pg/type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -956,13 +956,75 @@ def expect_deprecated_coder_init

expect( binaryenc_text_array.encode([[[5,6]],[["6\"",7]],[[nil,5]]]) ).to eq( exp )
end

let!(:binaryenc_array_array) { PG::BinaryEncoder::Array.new elements_type: PG::BinaryEncoder::Array.new(elements_type: PG::BinaryEncoder::Int4.new(oid: 0x17), dimensions: 1), dimensions: 2 }

it 'encodes an array in an array of int4' do
exp = ["00000002" + "00000001" + "00000000" +
"00000003" + "00000001" + "00000001" + "00000001" +

"00000024" +
"00000001" + "00000001" + "00000017" +
"00000002" + "00000001" +
"00000004" + "00000005" +
"00000004" + "00000006" +

"00000024" +
"00000001" + "00000001" + "00000017" +
"00000002" + "00000001" +
"00000004" + "00000006" +
"00000004" + "00000007" +

"00000020" +
"00000001" + "00000001" + "00000017" +
"00000002" + "00000001" +
"ffffffff" +
"00000004" + "00000005"
].pack("H*")

expect( binaryenc_array_array.encode([[[5,6]],[[6,7]],[[nil,5]]]) ).to eq( exp )
end
end

context 'two dimensional arrays' do
it 'encodes an array of timestamps with sub arrays' do
expect( textenc_timestamp_array.encode([Time.new(2014,12,31),[nil, Time.new(2016,01,02, 23, 23, 59.99)]]) ).
to eq( %[{2014-12-31 00:00:00.000000000,{NULL,2016-01-02 23:23:59.990000000}}] )
end

context 'with dimensions' do
let!(:textenc_array_2dim) { textenc_string_array.dup.tap{|a| a.dimensions = 2} }
let!(:binaryenc_array_2dim) { binaryenc_array.dup.tap{|a| a.dimensions = 2} }

it 'encodes binary int array' do
binaryenc_array_2dim.encode([[1]])
end
it 'encodes text int array' do
expect( textenc_array_2dim.encode([[1]]) ).to eq( "{{1}}" )
end
it 'encodes empty array' do
binaryenc_array_2dim.encode([[]])
end
it 'encodes text empty array' do
expect( textenc_array_2dim.encode([[]]) ).to eq( "{{}}" )
end
it 'raises an error on 1 dim binary array input to int4' do
expect{ binaryenc_array_2dim.encode([1]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
end
it 'raises an error on 1 dim text array input to int4' do
expect{ textenc_array_2dim.encode([1]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
end

it 'raises an error on 0 dim array input to int4' do
expect{ binaryenc_array_2dim.encode([]) }.to raise_error( ArgumentError, /less array dimensions.*0.*2/)
end
it 'raises an error on 0 dim text array input to int4' do
expect{ textenc_array_2dim.encode([]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
end
it 'raises an error on 1 dim text array nil input' do
expect{ textenc_array_2dim.encode([nil]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
end
end
end

context 'one dimensional array' do
Expand All @@ -986,6 +1048,37 @@ def expect_deprecated_coder_init

expect( binaryenc_array.encode([nil, "6\""]) ).to eq( exp )
end

context 'with dimensions' do
let!(:textenc_array_1dim) { textenc_int_array.dup.tap{|a| a.dimensions = 1} }
let!(:binaryenc_array_1dim) { binaryenc_array.dup.tap{|a| a.dimensions = 1} }

it 'encodes an array' do
exp =["00000001" + "00000001" + "00000000" +
"00000002" + "00000001" +
"ffffffff" +
"00000002" + "3622"
].pack("H*")

expect( binaryenc_array_1dim.encode([nil, "6\""]) ).to eq( exp )
end
it 'encodes an empty binary array' do
exp =["00000000" + "00000001" + "00000000"
].pack("H*")
expect( binaryenc_array_1dim.encode([]) ).to eq( exp )
end
it 'encodes an empty text array' do
expect( textenc_array_1dim.encode([]) ).to eq( "{}" )
end

let!(:binaryenc_int4_array_1dim) { PG::BinaryEncoder::Array.new elements_type: PG::BinaryEncoder::Int4.new, dimensions: 1 }
it 'raises an error on binary array input to int4' do
expect{ binaryenc_int4_array_1dim.encode([[1]]) }.to raise_error( NoMethodError, /to_i/)
end
it 'raises an error on text array input to int4' do
expect{ textenc_array_1dim.encode([[1]]) }.to raise_error( NoMethodError, /to_i/)
end
end
end

context 'other dimensional array' do
Expand Down Expand Up @@ -1091,7 +1184,8 @@ def expect_deprecated_coder_init
it "should respond to to_h" do
expect( textenc_int_array.to_h ).to eq( {
name: nil, oid: 0, format: 0, flags: 0,
elements_type: textenc_int, needs_quotation: false, delimiter: ','
elements_type: textenc_int, needs_quotation: false, delimiter: ',',
dimensions: nil
} )
end

Expand All @@ -1107,6 +1201,7 @@ def expect_deprecated_coder_init
expect( t.needs_quotation? ).to eq( true )
expect( t.delimiter ).to eq( ',' )
expect( t.elements_type ).to be_nil
expect( t.dimensions ).to be_nil
end

it "should deny changes when frozen" do
Expand All @@ -1117,6 +1212,7 @@ def expect_deprecated_coder_init
expect{ t.needs_quotation = true }.to raise_error(FrozenError)
expect{ t.delimiter = "," }.to raise_error(FrozenError)
expect{ t.elements_type = nil }.to raise_error(FrozenError)
expect{ t.dimensions = 1 }.to raise_error(FrozenError)
end

it "should be shareable for Ractor", :ractor do
Expand Down
Loading