Skip to content

Commit f9d05f5

Browse files
authored
Merge pull request ged#622 from ged/add-array-dimensions
Add possibility to define the number of array dimensions to be encoded
2 parents ab21474 + a974f31 commit f9d05f5

File tree

6 files changed

+175
-8
lines changed

6 files changed

+175
-8
lines changed

ext/pg.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ typedef struct {
206206
t_pg_coder comp;
207207
t_pg_coder *elem;
208208
int needs_quotation;
209+
int dimensions;
209210
char delimiter;
210211
} t_pg_composite_coder;
211212

ext/pg_binary_encoder.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ pg_bin_enc_date(t_pg_coder *this, VALUE value, char *out, VALUE *intermediate, i
320320
* This encoder expects an Array of values or sub-arrays as input.
321321
* Other values are passed through as byte string without interpretation.
322322
*
323+
* It is possible to enforce a number of dimensions to be encoded by #dimensions= .
324+
* Deeper nested arrays are then passed to the elements encoder and less nested arrays raise an ArgumentError.
325+
*
323326
* The accessors needs_quotation and delimiter are ignored for binary encoding.
324327
*
325328
*/
@@ -346,7 +349,8 @@ pg_bin_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
346349
dim_sizes[ndim-1] = RARRAY_LENINT(el1);
347350
nitems *= dim_sizes[ndim-1];
348351
el2 = rb_ary_entry(el1, 0);
349-
if (TYPE(el2) == T_ARRAY) {
352+
if ( (this->dimensions < 0 || ndim < this->dimensions) &&
353+
TYPE(el2) == T_ARRAY) {
350354
ndim++;
351355
if (ndim > MAXDIM)
352356
rb_raise( rb_eArgError, "unsupported number of array dimensions: >%d", ndim );
@@ -356,6 +360,9 @@ pg_bin_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
356360
el1 = el2;
357361
}
358362
}
363+
if( this->dimensions >= 0 && (ndim==0 ? 1 : ndim) != this->dimensions ){
364+
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", ndim, this->dimensions);
365+
}
359366

360367
if(out){
361368
/* Second encoder pass -> write data to `out` */

ext/pg_coder.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ pg_composite_encoder_allocate( VALUE klass )
135135
this->elem = NULL;
136136
this->needs_quotation = 1;
137137
this->delimiter = ',';
138+
this->dimensions = -1;
138139
rb_iv_set( self, "@elements_type", Qnil );
139140
return self;
140141
}
@@ -157,6 +158,7 @@ pg_composite_decoder_allocate( VALUE klass )
157158
this->elem = NULL;
158159
this->needs_quotation = 1;
159160
this->delimiter = ',';
161+
this->dimensions = -1;
160162
rb_iv_set( self, "@elements_type", Qnil );
161163
return self;
162164
}
@@ -421,6 +423,49 @@ pg_coder_delimiter_get(VALUE self)
421423
return rb_str_new(&this->delimiter, 1);
422424
}
423425

426+
/*
427+
* call-seq:
428+
* coder.dimensions = Integer
429+
* coder.dimensions = nil
430+
*
431+
* Set number of array dimensions to be encoded.
432+
*
433+
* This property ensures, that this number of dimensions is always encoded.
434+
* If less dimensions than this number are in the given value, an ArgumentError is raised.
435+
* If more dimensions than this number are in the value, the Array value is passed to the next encoder.
436+
*
437+
* 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.
438+
*
439+
* The default is +nil+.
440+
*
441+
* See #dimensions
442+
*/
443+
static VALUE
444+
pg_coder_dimensions_set(VALUE self, VALUE dimensions)
445+
{
446+
t_pg_composite_coder *this = RTYPEDDATA_DATA(self);
447+
rb_check_frozen(self);
448+
if(!NIL_P(dimensions) && NUM2INT(dimensions) < 0)
449+
rb_raise( rb_eArgError, "dimensions must be nil or >= 0");
450+
this->dimensions = NIL_P(dimensions) ? -1 : NUM2INT(dimensions);
451+
return dimensions;
452+
}
453+
454+
/*
455+
* call-seq:
456+
* coder.dimensions -> Integer | nil
457+
*
458+
* Get number of enforced array dimensions or +nil+ if not set.
459+
*
460+
* See #dimensions=
461+
*/
462+
static VALUE
463+
pg_coder_dimensions_get(VALUE self)
464+
{
465+
t_pg_composite_coder *this = RTYPEDDATA_DATA(self);
466+
return this->dimensions < 0 ? Qnil : INT2NUM(this->dimensions);
467+
}
468+
424469
/*
425470
* call-seq:
426471
* coder.elements_type = coder
@@ -602,6 +647,8 @@ init_pg_coder(void)
602647
*
603648
* This is the base class for all type cast classes of PostgreSQL types,
604649
* that are made up of some sub type.
650+
*
651+
* See PG::TextEncoder::Array, PG::TextDecoder::Array, PG::BinaryEncoder::Array, PG::BinaryDecoder::Array, etc.
605652
*/
606653
rb_cPG_CompositeCoder = rb_define_class_under( rb_mPG, "CompositeCoder", rb_cPG_Coder );
607654
rb_define_method( rb_cPG_CompositeCoder, "elements_type=", pg_coder_elements_type_set, 1 );
@@ -610,6 +657,8 @@ init_pg_coder(void)
610657
rb_define_method( rb_cPG_CompositeCoder, "needs_quotation?", pg_coder_needs_quotation_get, 0 );
611658
rb_define_method( rb_cPG_CompositeCoder, "delimiter=", pg_coder_delimiter_set, 1 );
612659
rb_define_method( rb_cPG_CompositeCoder, "delimiter", pg_coder_delimiter_get, 0 );
660+
rb_define_method( rb_cPG_CompositeCoder, "dimensions=", pg_coder_dimensions_set, 1 );
661+
rb_define_method( rb_cPG_CompositeCoder, "dimensions", pg_coder_dimensions_get, 0 );
613662

614663
/* Document-class: PG::CompositeEncoder < PG::CompositeCoder */
615664
rb_cPG_CompositeEncoder = rb_define_class_under( rb_mPG, "CompositeEncoder", rb_cPG_CompositeCoder );

ext/pg_text_encoder.c

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,14 +537,18 @@ quote_string(t_pg_coder *this, VALUE value, VALUE string, char *current_out, int
537537
}
538538

539539
static char *
540-
write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE string, int quote, int enc_idx)
540+
write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE string, int quote, int enc_idx, int dimension)
541541
{
542542
int i;
543543

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

548+
if( RARRAY_LEN(value) == 0 && this->dimensions >= 0 && dimension != this->dimensions ){
549+
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", dimension, this->dimensions);
550+
}
551+
548552
for( i=0; i<RARRAY_LEN(value); i++){
549553
VALUE entry = rb_ary_entry(value, i);
550554

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

556560
switch(TYPE(entry)){
557-
case T_ARRAY:
558-
current_out = write_array(this, entry, current_out, string, quote, enc_idx);
559-
break;
560561
case T_NIL:
562+
if( this->dimensions >= 0 && dimension != this->dimensions ){
563+
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", dimension, this->dimensions);
564+
}
561565
current_out = pg_rb_str_ensure_capa( string, 4, current_out, NULL );
562566
*current_out++ = 'N';
563567
*current_out++ = 'U';
564568
*current_out++ = 'L';
565569
*current_out++ = 'L';
566570
break;
571+
case T_ARRAY:
572+
if( this->dimensions < 0 || dimension < this->dimensions ){
573+
current_out = write_array(this, entry, current_out, string, quote, enc_idx, dimension+1);
574+
break;
575+
}
576+
/* Number of dimensions reached -> handle array as normal value */
567577
default:
578+
if( this->dimensions >= 0 && dimension != this->dimensions ){
579+
rb_raise(rb_eArgError, "less array dimensions to encode (%d) than expected (%d)", dimension, this->dimensions);
580+
}
568581
current_out = quote_string( this->elem, entry, string, current_out, quote, quote_array_buffer, this, enc_idx );
569582
}
570583
}
@@ -596,7 +609,7 @@ pg_text_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
596609
VALUE out_str = rb_str_new(NULL, 0);
597610
PG_ENCODING_SET_NOCHECK(out_str, enc_idx);
598611

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

601614
rb_str_set_len( out_str, end_ptr - RSTRING_PTR(out_str) );
602615
*intermediate = out_str;

lib/pg/coder.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ def to_h
7676
elements_type: elements_type,
7777
needs_quotation: needs_quotation?,
7878
delimiter: delimiter,
79+
dimensions: dimensions,
7980
}
8081
end
8182

8283
def inspect
8384
str = super
84-
str[-1,0] = " elements_type=#{elements_type.inspect} #{needs_quotation? ? 'needs' : 'no'} quotation"
85+
str[-1,0] = " elements_type=#{elements_type.inspect} #{needs_quotation? ? 'needs' : 'no'} quotation#{dimensions && " #{dimensions} dimensions"}"
8586
str
8687
end
8788
end

spec/pg/type_spec.rb

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,13 +956,75 @@ def expect_deprecated_coder_init
956956

957957
expect( binaryenc_text_array.encode([[[5,6]],[["6\"",7]],[[nil,5]]]) ).to eq( exp )
958958
end
959+
960+
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 }
961+
962+
it 'encodes an array in an array of int4' do
963+
exp = ["00000002" + "00000001" + "00000000" +
964+
"00000003" + "00000001" + "00000001" + "00000001" +
965+
966+
"00000024" +
967+
"00000001" + "00000001" + "00000017" +
968+
"00000002" + "00000001" +
969+
"00000004" + "00000005" +
970+
"00000004" + "00000006" +
971+
972+
"00000024" +
973+
"00000001" + "00000001" + "00000017" +
974+
"00000002" + "00000001" +
975+
"00000004" + "00000006" +
976+
"00000004" + "00000007" +
977+
978+
"00000020" +
979+
"00000001" + "00000001" + "00000017" +
980+
"00000002" + "00000001" +
981+
"ffffffff" +
982+
"00000004" + "00000005"
983+
].pack("H*")
984+
985+
expect( binaryenc_array_array.encode([[[5,6]],[[6,7]],[[nil,5]]]) ).to eq( exp )
986+
end
959987
end
960988

961989
context 'two dimensional arrays' do
962990
it 'encodes an array of timestamps with sub arrays' do
963991
expect( textenc_timestamp_array.encode([Time.new(2014,12,31),[nil, Time.new(2016,01,02, 23, 23, 59.99)]]) ).
964992
to eq( %[{2014-12-31 00:00:00.000000000,{NULL,2016-01-02 23:23:59.990000000}}] )
965993
end
994+
995+
context 'with dimensions' do
996+
let!(:textenc_array_2dim) { textenc_string_array.dup.tap{|a| a.dimensions = 2} }
997+
let!(:binaryenc_array_2dim) { binaryenc_array.dup.tap{|a| a.dimensions = 2} }
998+
999+
it 'encodes binary int array' do
1000+
binaryenc_array_2dim.encode([[1]])
1001+
end
1002+
it 'encodes text int array' do
1003+
expect( textenc_array_2dim.encode([[1]]) ).to eq( "{{1}}" )
1004+
end
1005+
it 'encodes empty array' do
1006+
binaryenc_array_2dim.encode([[]])
1007+
end
1008+
it 'encodes text empty array' do
1009+
expect( textenc_array_2dim.encode([[]]) ).to eq( "{{}}" )
1010+
end
1011+
it 'raises an error on 1 dim binary array input to int4' do
1012+
expect{ binaryenc_array_2dim.encode([1]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
1013+
end
1014+
it 'raises an error on 1 dim text array input to int4' do
1015+
expect{ textenc_array_2dim.encode([1]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
1016+
end
1017+
1018+
it 'raises an error on 0 dim array input to int4' do
1019+
expect{ binaryenc_array_2dim.encode([]) }.to raise_error( ArgumentError, /less array dimensions.*0.*2/)
1020+
end
1021+
it 'raises an error on 0 dim text array input to int4' do
1022+
expect{ textenc_array_2dim.encode([]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
1023+
end
1024+
it 'raises an error on 1 dim text array nil input' do
1025+
expect{ textenc_array_2dim.encode([nil]) }.to raise_error( ArgumentError, /less array dimensions.*1.*2/)
1026+
end
1027+
end
9661028
end
9671029

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

9871049
expect( binaryenc_array.encode([nil, "6\""]) ).to eq( exp )
9881050
end
1051+
1052+
context 'with dimensions' do
1053+
let!(:textenc_array_1dim) { textenc_int_array.dup.tap{|a| a.dimensions = 1} }
1054+
let!(:binaryenc_array_1dim) { binaryenc_array.dup.tap{|a| a.dimensions = 1} }
1055+
1056+
it 'encodes an array' do
1057+
exp =["00000001" + "00000001" + "00000000" +
1058+
"00000002" + "00000001" +
1059+
"ffffffff" +
1060+
"00000002" + "3622"
1061+
].pack("H*")
1062+
1063+
expect( binaryenc_array_1dim.encode([nil, "6\""]) ).to eq( exp )
1064+
end
1065+
it 'encodes an empty binary array' do
1066+
exp =["00000000" + "00000001" + "00000000"
1067+
].pack("H*")
1068+
expect( binaryenc_array_1dim.encode([]) ).to eq( exp )
1069+
end
1070+
it 'encodes an empty text array' do
1071+
expect( textenc_array_1dim.encode([]) ).to eq( "{}" )
1072+
end
1073+
1074+
let!(:binaryenc_int4_array_1dim) { PG::BinaryEncoder::Array.new elements_type: PG::BinaryEncoder::Int4.new, dimensions: 1 }
1075+
it 'raises an error on binary array input to int4' do
1076+
expect{ binaryenc_int4_array_1dim.encode([[1]]) }.to raise_error( NoMethodError, /to_i/)
1077+
end
1078+
it 'raises an error on text array input to int4' do
1079+
expect{ textenc_array_1dim.encode([[1]]) }.to raise_error( NoMethodError, /to_i/)
1080+
end
1081+
end
9891082
end
9901083

9911084
context 'other dimensional array' do
@@ -1091,7 +1184,8 @@ def expect_deprecated_coder_init
10911184
it "should respond to to_h" do
10921185
expect( textenc_int_array.to_h ).to eq( {
10931186
name: nil, oid: 0, format: 0, flags: 0,
1094-
elements_type: textenc_int, needs_quotation: false, delimiter: ','
1187+
elements_type: textenc_int, needs_quotation: false, delimiter: ',',
1188+
dimensions: nil
10951189
} )
10961190
end
10971191

@@ -1107,6 +1201,7 @@ def expect_deprecated_coder_init
11071201
expect( t.needs_quotation? ).to eq( true )
11081202
expect( t.delimiter ).to eq( ',' )
11091203
expect( t.elements_type ).to be_nil
1204+
expect( t.dimensions ).to be_nil
11101205
end
11111206

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

11221218
it "should be shareable for Ractor", :ractor do

0 commit comments

Comments
 (0)