pg 1.5.9-x64-mingw32 → 1.6.0.rc2-x64-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/{History.md → CHANGELOG.md} +54 -0
  4. data/Gemfile +7 -4
  5. data/README-Windows.rdoc +1 -1
  6. data/README.ja.md +4 -4
  7. data/README.md +6 -5
  8. data/Rakefile +80 -13
  9. data/ext/extconf.rb +158 -14
  10. data/ext/gvl_wrappers.c +13 -2
  11. data/ext/gvl_wrappers.h +33 -0
  12. data/ext/pg.c +16 -5
  13. data/ext/pg.h +9 -9
  14. data/ext/pg_binary_decoder.c +150 -0
  15. data/ext/pg_binary_encoder.c +210 -7
  16. data/ext/pg_cancel_connection.c +360 -0
  17. data/ext/pg_coder.c +52 -5
  18. data/ext/pg_connection.c +368 -158
  19. data/ext/pg_copy_coder.c +2 -2
  20. data/ext/pg_record_coder.c +1 -1
  21. data/ext/pg_result.c +9 -11
  22. data/ext/pg_text_encoder.c +20 -7
  23. data/ext/pg_tuple.c +2 -2
  24. data/ext/pg_type_map.c +1 -1
  25. data/ext/pg_type_map_all_strings.c +1 -1
  26. data/ext/pg_type_map_by_class.c +1 -1
  27. data/ext/pg_type_map_by_column.c +2 -1
  28. data/ext/pg_type_map_by_mri_type.c +1 -1
  29. data/ext/pg_type_map_by_oid.c +3 -1
  30. data/ext/pg_type_map_in_ruby.c +1 -1
  31. data/lib/2.7/pg_ext.so +0 -0
  32. data/lib/3.0/pg_ext.so +0 -0
  33. data/lib/pg/basic_type_map_for_queries.rb +7 -3
  34. data/lib/pg/basic_type_registry.rb +2 -2
  35. data/lib/pg/cancel_connection.rb +53 -0
  36. data/lib/pg/coder.rb +2 -1
  37. data/lib/pg/connection.rb +252 -131
  38. data/lib/pg/version.rb +1 -1
  39. data/lib/pg.rb +13 -8
  40. data/misc/yugabyte/Dockerfile +9 -0
  41. data/misc/yugabyte/docker-compose.yml +28 -0
  42. data/misc/yugabyte/pg-test.rb +45 -0
  43. data/pg.gemspec +5 -3
  44. data/ports/patches/krb5/1.21.3/0001-Allow-static-linking-krb5-library.patch +30 -0
  45. data/ports/patches/openssl/3.5.1/0001-aarch64-mingw.patch +21 -0
  46. data/ports/patches/postgresql/17.5/0001-Use-workaround-of-__builtin_setjmp-only-on-MINGW-on-.patch +42 -0
  47. data/ports/patches/postgresql/17.5/0001-libpq-Process-buffered-SSL-read-bytes-to-support-rec.patch +52 -0
  48. data/{lib/x64-mingw32 → ports/x64-mingw32/lib}/libpq.dll +0 -0
  49. data/rakelib/pg_gem_helper.rb +64 -0
  50. data.tar.gz.sig +0 -0
  51. metadata +33 -27
  52. metadata.gz.sig +0 -0
  53. data/Manifest.txt +0 -72
  54. data/Rakefile.cross +0 -303
  55. data/lib/2.5/pg_ext.so +0 -0
  56. data/lib/2.6/pg_ext.so +0 -0
data/ext/pg_copy_coder.c CHANGED
@@ -51,7 +51,7 @@ static const rb_data_type_t pg_copycoder_type = {
51
51
  pg_copycoder_mark,
52
52
  RUBY_TYPED_DEFAULT_FREE,
53
53
  pg_copycoder_memsize,
54
- pg_compact_callback(pg_copycoder_compact),
54
+ pg_copycoder_compact,
55
55
  },
56
56
  &pg_coder_type,
57
57
  0,
@@ -831,7 +831,6 @@ pg_bin_dec_copy_row(t_pg_coder *conv, const char *input_line, int len, int _tupl
831
831
 
832
832
  for( fieldno = 0; fieldno < nfields; fieldno++){
833
833
  long input_len;
834
- VALUE field_value;
835
834
 
836
835
  /* read field size */
837
836
  if (line_end_ptr - cur_ptr < 4 ) goto length_error;
@@ -843,6 +842,7 @@ pg_bin_dec_copy_row(t_pg_coder *conv, const char *input_line, int len, int _tupl
843
842
  /* NULL indicator */
844
843
  rb_ary_push(array, Qnil);
845
844
  } else {
845
+ VALUE field_value;
846
846
  if (line_end_ptr - cur_ptr < input_len ) goto length_error;
847
847
 
848
848
  /* copy input data to field_str */
@@ -43,7 +43,7 @@ static const rb_data_type_t pg_recordcoder_type = {
43
43
  pg_recordcoder_mark,
44
44
  RUBY_TYPED_DEFAULT_FREE,
45
45
  pg_recordcoder_memsize,
46
- pg_compact_callback(pg_recordcoder_compact),
46
+ pg_recordcoder_compact,
47
47
  },
48
48
  &pg_coder_type,
49
49
  0,
data/ext/pg_result.c CHANGED
@@ -147,9 +147,7 @@ pgresult_clear( void *_this )
147
147
  t_pg_result *this = (t_pg_result *)_this;
148
148
  if( this->pgresult && !this->autoclear ){
149
149
  PQclear(this->pgresult);
150
- #ifdef HAVE_RB_GC_ADJUST_MEMORY_USAGE
151
150
  rb_gc_adjust_memory_usage(-this->result_size);
152
- #endif
153
151
  }
154
152
  this->result_size = 0;
155
153
  this->nfields = -1;
@@ -180,7 +178,7 @@ static const rb_data_type_t pgresult_type = {
180
178
  pgresult_gc_mark,
181
179
  pgresult_gc_free,
182
180
  pgresult_memsize,
183
- pg_compact_callback(pgresult_gc_compact),
181
+ pgresult_gc_compact,
184
182
  },
185
183
  0, 0,
186
184
  RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | PG_RUBY_TYPED_FROZEN_SHAREABLE,
@@ -253,9 +251,7 @@ pg_new_result(PGresult *result, VALUE rb_pgconn)
253
251
  */
254
252
  this->result_size = pgresult_approx_size(result);
255
253
 
256
- #ifdef HAVE_RB_GC_ADJUST_MEMORY_USAGE
257
254
  rb_gc_adjust_memory_usage(this->result_size);
258
- #endif
259
255
 
260
256
  return self;
261
257
  }
@@ -323,6 +319,9 @@ pg_result_check( VALUE self )
323
319
  case PGRES_COMMAND_OK:
324
320
  #ifdef HAVE_PQENTERPIPELINEMODE
325
321
  case PGRES_PIPELINE_SYNC:
322
+ #endif
323
+ #ifdef HAVE_PQSETCHUNKEDROWSMODE
324
+ case PGRES_TUPLES_CHUNK:
326
325
  #endif
327
326
  return self;
328
327
  case PGRES_BAD_RESPONSE:
@@ -549,6 +548,7 @@ static void pgresult_init_fnames(VALUE self)
549
548
  * * +PGRES_FATAL_ERROR+
550
549
  * * +PGRES_COPY_BOTH+
551
550
  * * +PGRES_SINGLE_TUPLE+
551
+ * * +PGRES_TUPLES_CHUNK+
552
552
  * * +PGRES_PIPELINE_SYNC+
553
553
  * * +PGRES_PIPELINE_ABORTED+
554
554
  *
@@ -613,14 +613,12 @@ pgresult_error_message(VALUE self)
613
613
  return ret;
614
614
  }
615
615
 
616
- #ifdef HAVE_PQRESULTVERBOSEERRORMESSAGE
617
616
  /*
618
617
  * call-seq:
619
618
  * res.verbose_error_message( verbosity, show_context ) -> String
620
619
  *
621
620
  * Returns a reformatted version of the error message associated with a PGresult object.
622
621
  *
623
- * Available since PostgreSQL-9.6
624
622
  */
625
623
  static VALUE
626
624
  pgresult_verbose_error_message(VALUE self, VALUE verbosity, VALUE show_context)
@@ -639,7 +637,6 @@ pgresult_verbose_error_message(VALUE self, VALUE verbosity, VALUE show_context)
639
637
 
640
638
  return ret;
641
639
  }
642
- #endif
643
640
 
644
641
  /*
645
642
  * call-seq:
@@ -1528,6 +1525,9 @@ pgresult_stream_any(VALUE self, int (*yielder)(VALUE, int, int, void*), void* da
1528
1525
  return self;
1529
1526
  rb_raise( rb_eInvalidResultStatus, "PG::Result is not in single row mode");
1530
1527
  case PGRES_SINGLE_TUPLE:
1528
+ #ifdef HAVE_PQSETCHUNKEDROWSMODE
1529
+ case PGRES_TUPLES_CHUNK:
1530
+ #endif
1531
1531
  break;
1532
1532
  default:
1533
1533
  pg_result_check( self );
@@ -1572,7 +1572,7 @@ pgresult_stream_any(VALUE self, int (*yielder)(VALUE, int, int, void*), void* da
1572
1572
  * wrapping each row into a dedicated result object, it delivers data in nearly
1573
1573
  * the same speed as with ordinary results.
1574
1574
  *
1575
- * The base result must be in status PGRES_SINGLE_TUPLE.
1575
+ * The base result must be in status PGRES_SINGLE_TUPLE or PGRES_TUPLES_CHUNK.
1576
1576
  * It iterates over all tuples until the status changes to PGRES_TUPLES_OK.
1577
1577
  * A PG::Error is raised for any errors from the server.
1578
1578
  *
@@ -1707,10 +1707,8 @@ init_pg_result(void)
1707
1707
  rb_define_singleton_method(rb_cPGresult, "res_status", pgresult_s_res_status, 1);
1708
1708
  rb_define_method(rb_cPGresult, "error_message", pgresult_error_message, 0);
1709
1709
  rb_define_alias( rb_cPGresult, "result_error_message", "error_message");
1710
- #ifdef HAVE_PQRESULTVERBOSEERRORMESSAGE
1711
1710
  rb_define_method(rb_cPGresult, "verbose_error_message", pgresult_verbose_error_message, 2);
1712
1711
  rb_define_alias( rb_cPGresult, "result_verbose_error_message", "verbose_error_message");
1713
- #endif
1714
1712
  rb_define_method(rb_cPGresult, "error_field", pgresult_error_field, 1);
1715
1713
  rb_define_alias( rb_cPGresult, "result_error_field", "error_field" );
1716
1714
  rb_define_method(rb_cPGresult, "clear", pg_result_clear, 0);
@@ -231,7 +231,7 @@ pg_text_enc_integer(t_pg_coder *this, VALUE value, char *out, VALUE *intermediat
231
231
  *
232
232
  */
233
233
  static int
234
- pg_text_enc_float(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate, int enc_idx)
234
+ pg_text_enc_float(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate1, int enc_idx)
235
235
  {
236
236
  if(out){
237
237
  double dvalue = NUM2DBL(value);
@@ -239,7 +239,6 @@ pg_text_enc_float(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
239
239
  int neg = 0;
240
240
  int exp2i, exp10i, i;
241
241
  unsigned long long ll, remainder, oldval;
242
- VALUE intermediate;
243
242
 
244
243
  /* Cast to the same strings as value.to_s . */
245
244
  if( isinf(dvalue) ){
@@ -283,6 +282,7 @@ pg_text_enc_float(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
283
282
 
284
283
  if( exp10i <= -5 || exp10i >= 15 ) {
285
284
  /* Write the float in exponent format (1.23e45) */
285
+ VALUE intermediate;
286
286
 
287
287
  /* write fraction digits from right to left */
288
288
  for( i = MAX_DOUBLE_DIGITS; i > 1; i--){
@@ -537,7 +537,7 @@ quote_string(t_pg_coder *this, VALUE value, VALUE string, char *current_out, int
537
537
  }
538
538
 
539
539
  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)
541
541
  {
542
542
  int i;
543
543
 
@@ -545,6 +545,10 @@ write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE st
545
545
  current_out = pg_rb_str_ensure_capa( string, 2, current_out, NULL );
546
546
  *current_out++ = '{';
547
547
 
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
+
548
552
  for( i=0; i<RARRAY_LEN(value); i++){
549
553
  VALUE entry = rb_ary_entry(value, i);
550
554
 
@@ -554,17 +558,26 @@ write_array(t_pg_composite_coder *this, VALUE value, char *current_out, VALUE st
554
558
  }
555
559
 
556
560
  switch(TYPE(entry)){
557
- case T_ARRAY:
558
- current_out = write_array(this, entry, current_out, string, quote, enc_idx);
559
- break;
560
561
  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
+ }
561
565
  current_out = pg_rb_str_ensure_capa( string, 4, current_out, NULL );
562
566
  *current_out++ = 'N';
563
567
  *current_out++ = 'U';
564
568
  *current_out++ = 'L';
565
569
  *current_out++ = 'L';
566
570
  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 */
567
577
  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
+ }
568
581
  current_out = quote_string( this->elem, entry, string, current_out, quote, quote_array_buffer, this, enc_idx );
569
582
  }
570
583
  }
@@ -596,7 +609,7 @@ pg_text_enc_array(t_pg_coder *conv, VALUE value, char *out, VALUE *intermediate,
596
609
  VALUE out_str = rb_str_new(NULL, 0);
597
610
  PG_ENCODING_SET_NOCHECK(out_str, enc_idx);
598
611
 
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);
600
613
 
601
614
  rb_str_set_len( out_str, end_ptr - RSTRING_PTR(out_str) );
602
615
  *intermediate = out_str;
data/ext/pg_tuple.c CHANGED
@@ -125,7 +125,7 @@ static const rb_data_type_t pg_tuple_type = {
125
125
  pg_tuple_gc_mark,
126
126
  pg_tuple_gc_free,
127
127
  pg_tuple_memsize,
128
- pg_compact_callback(pg_tuple_gc_compact),
128
+ pg_tuple_gc_compact,
129
129
  },
130
130
  0, 0,
131
131
  RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | PG_RUBY_TYPED_FROZEN_SHAREABLE,
@@ -135,7 +135,7 @@ static const rb_data_type_t pg_tuple_type = {
135
135
  * Document-method: allocate
136
136
  *
137
137
  * call-seq:
138
- * PG::VeryTuple.allocate -> obj
138
+ * PG::Tuple.allocate -> obj
139
139
  */
140
140
  static VALUE
141
141
  pg_tuple_s_allocate( VALUE klass )
data/ext/pg_type_map.c CHANGED
@@ -33,7 +33,7 @@ const rb_data_type_t pg_typemap_type = {
33
33
  pg_typemap_mark,
34
34
  RUBY_TYPED_DEFAULT_FREE,
35
35
  pg_typemap_memsize,
36
- pg_compact_callback(pg_typemap_compact),
36
+ pg_typemap_compact,
37
37
  },
38
38
  0,
39
39
  0,
@@ -14,7 +14,7 @@ static const rb_data_type_t pg_tmas_type = {
14
14
  pg_typemap_mark,
15
15
  RUBY_TYPED_DEFAULT_FREE,
16
16
  pg_typemap_memsize,
17
- pg_compact_callback(pg_typemap_compact),
17
+ pg_typemap_compact,
18
18
  },
19
19
  &pg_typemap_type,
20
20
  0,
@@ -153,7 +153,7 @@ static const rb_data_type_t pg_tmbk_type = {
153
153
  pg_tmbk_mark,
154
154
  RUBY_TYPED_DEFAULT_FREE,
155
155
  pg_tmbk_memsize,
156
- pg_compact_callback(pg_tmbk_compact),
156
+ pg_tmbk_compact,
157
157
  },
158
158
  &pg_typemap_type,
159
159
  0,
@@ -54,6 +54,7 @@ pg_tmbc_fit_to_query( VALUE self, VALUE params )
54
54
  t_tmbc *this = RTYPEDDATA_DATA( self );
55
55
  t_typemap *default_tm;
56
56
 
57
+ Check_Type(params, T_ARRAY);
57
58
  nfields = (int)RARRAY_LEN( params );
58
59
  if ( this->nfields != nfields ) {
59
60
  rb_raise( rb_eArgError, "number of result fields (%d) does not match number of mapped columns (%d)",
@@ -228,7 +229,7 @@ static const rb_data_type_t pg_tmbc_type = {
228
229
  pg_tmbc_mark,
229
230
  pg_tmbc_free,
230
231
  pg_tmbc_memsize,
231
- pg_compact_callback(pg_tmbc_compact),
232
+ pg_tmbc_compact,
232
233
  },
233
234
  &pg_typemap_type,
234
235
  0,
@@ -130,7 +130,7 @@ static const rb_data_type_t pg_tmbmt_type = {
130
130
  pg_tmbmt_mark,
131
131
  RUBY_TYPED_DEFAULT_FREE,
132
132
  pg_tmbmt_memsize,
133
- pg_compact_callback(pg_tmbmt_compact),
133
+ pg_tmbmt_compact,
134
134
  },
135
135
  &pg_typemap_type,
136
136
  0,
@@ -190,7 +190,7 @@ static const rb_data_type_t pg_tmbo_type = {
190
190
  pg_tmbo_mark,
191
191
  RUBY_TYPED_DEFAULT_FREE,
192
192
  pg_tmbo_memsize,
193
- pg_compact_callback(pg_tmbo_compact),
193
+ pg_tmbo_compact,
194
194
  },
195
195
  &pg_typemap_type,
196
196
  0,
@@ -315,6 +315,8 @@ pg_tmbo_coders( VALUE self )
315
315
  * The type map will do Hash lookups for each result value, if the number of rows
316
316
  * is below or equal +number+.
317
317
  *
318
+ * Default is 10.
319
+ *
318
320
  */
319
321
  static VALUE
320
322
  pg_tmbo_max_rows_for_online_lookup_set( VALUE self, VALUE value )
@@ -40,7 +40,7 @@ static const rb_data_type_t pg_tmir_type = {
40
40
  pg_typemap_mark,
41
41
  RUBY_TYPED_DEFAULT_FREE,
42
42
  pg_tmir_memsize,
43
- pg_compact_callback(pg_tmir_compact),
43
+ pg_tmir_compact,
44
44
  },
45
45
  &pg_typemap_type,
46
46
  0,
data/lib/2.7/pg_ext.so CHANGED
Binary file
data/lib/3.0/pg_ext.so CHANGED
Binary file
@@ -53,14 +53,18 @@ class PG::BasicTypeMapForQueries < PG::TypeMapByClass
53
53
  @coder_maps = build_coder_maps(connection_or_coder_maps, registry: registry)
54
54
  @array_encoders_by_klass = array_encoders_by_klass
55
55
  @encode_array_as = :array
56
- @if_undefined = if_undefined || method(:raise_undefined_type).to_proc
56
+ @if_undefined = if_undefined || UndefinedDefault
57
57
  init_encoders
58
58
  end
59
59
 
60
- private def raise_undefined_type(oid_name, format)
61
- raise UndefinedEncoder, "no encoder defined for type #{oid_name.inspect} format #{format}"
60
+ class UndefinedDefault
61
+ def self.call(oid_name, format)
62
+ raise UndefinedEncoder, "no encoder defined for type #{oid_name.inspect} format #{format}"
63
+ end
62
64
  end
63
65
 
66
+ private_constant :UndefinedDefault
67
+
64
68
  # Change the mechanism that is used to encode ruby array values
65
69
  #
66
70
  # Possible values:
@@ -127,8 +127,8 @@ class PG::BasicTypeRegistry
127
127
  @maps = [
128
128
  [0, :encoder, PG::TextEncoder::Array],
129
129
  [0, :decoder, PG::TextDecoder::Array],
130
- [1, :encoder, nil],
131
- [1, :decoder, nil],
130
+ [1, :encoder, PG::BinaryEncoder::Array],
131
+ [1, :decoder, PG::BinaryDecoder::Array],
132
132
  ].inject([]) do |h, (format, direction, arraycoder)|
133
133
  coders = registry.coders_for(format, direction) || {}
134
134
  h[format] ||= {}
@@ -0,0 +1,53 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'pg' unless defined?( PG )
5
+
6
+ if defined?(PG::CancelConnection)
7
+ class PG::CancelConnection
8
+ include PG::Connection::Pollable
9
+
10
+ alias c_initialize initialize
11
+
12
+ def initialize(conn)
13
+ c_initialize(conn)
14
+
15
+ # A cancel connection is always to one destination server only.
16
+ # Prepare conninfo_hash with just enough information to allow a shared polling_loop.
17
+ @host = conn.host
18
+ @hostaddr = conn.hostaddr
19
+ @port = conn.port
20
+
21
+ @conninfo_hash = {
22
+ host: @host,
23
+ hostaddr: @hostaddr,
24
+ port: @port.to_s,
25
+ connect_timeout: conn.conninfo_hash[:connect_timeout],
26
+ }
27
+ end
28
+
29
+ # call-seq:
30
+ # conn.cancel
31
+ #
32
+ # Requests that the server abandons processing of the current command in a blocking manner.
33
+ #
34
+ # If the cancel request wasn't successfully dispatched an error message is raised.
35
+ #
36
+ # Successful dispatch of the cancellation is no guarantee that the request will have any effect, however.
37
+ # If the cancellation is effective, the command being canceled will terminate early and raises an error.
38
+ # If the cancellation fails (say, because the server was already done processing the command), then there will be no visible result at all.
39
+ #
40
+ def cancel
41
+ start
42
+ polling_loop(:poll)
43
+ end
44
+ alias async_cancel cancel
45
+
46
+ # These private methods are there to allow a shared polling_loop.
47
+ private
48
+ attr_reader :host
49
+ attr_reader :hostaddr
50
+ attr_reader :port
51
+ attr_reader :conninfo_hash
52
+ end
53
+ end
data/lib/pg/coder.rb CHANGED
@@ -76,12 +76,13 @@ module PG
76
76
  elements_type: elements_type,
77
77
  needs_quotation: needs_quotation?,
78
78
  delimiter: delimiter,
79
+ dimensions: dimensions,
79
80
  }
80
81
  end
81
82
 
82
83
  def inspect
83
84
  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"}"
85
86
  str
86
87
  end
87
88
  end