pg 1.3.5 → 1.4.5

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.
data/ext/pg_copy_coder.c CHANGED
@@ -592,7 +592,7 @@ pg_text_dec_copy_row(t_pg_coder *conv, const char *input_line, int len, int _tup
592
592
 
593
593
 
594
594
  void
595
- init_pg_copycoder()
595
+ init_pg_copycoder(void)
596
596
  {
597
597
  /* Document-class: PG::CopyCoder < PG::Coder
598
598
  *
data/ext/pg_errors.c CHANGED
@@ -70,7 +70,7 @@ lookup_error_class(const char *sqlstate)
70
70
  }
71
71
 
72
72
  void
73
- init_pg_errors()
73
+ init_pg_errors(void)
74
74
  {
75
75
  rb_hErrors = rb_hash_new();
76
76
  rb_define_const( rb_mPG, "ERROR_CLASSES", rb_hErrors );
@@ -494,7 +494,7 @@ pg_text_dec_record(t_pg_coder *conv, char *input_line, int len, int _tuple, int
494
494
 
495
495
 
496
496
  void
497
- init_pg_recordcoder()
497
+ init_pg_recordcoder(void)
498
498
  {
499
499
  /* Document-class: PG::RecordCoder < PG::Coder
500
500
  *
data/ext/pg_result.c CHANGED
@@ -1382,21 +1382,20 @@ pgresult_type_map_get(VALUE self)
1382
1382
  }
1383
1383
 
1384
1384
 
1385
- static void
1385
+ static int
1386
1386
  yield_hash(VALUE self, int ntuples, int nfields, void *data)
1387
1387
  {
1388
1388
  int tuple_num;
1389
- t_pg_result *this = pgresult_get_this(self);
1390
1389
  UNUSED(nfields);
1391
1390
 
1392
1391
  for(tuple_num = 0; tuple_num < ntuples; tuple_num++) {
1393
1392
  rb_yield(pgresult_aref(self, INT2NUM(tuple_num)));
1394
1393
  }
1395
1394
 
1396
- pgresult_clear( this );
1395
+ return 1; /* clear the result */
1397
1396
  }
1398
1397
 
1399
- static void
1398
+ static int
1400
1399
  yield_array(VALUE self, int ntuples, int nfields, void *data)
1401
1400
  {
1402
1401
  int row;
@@ -1413,10 +1412,10 @@ yield_array(VALUE self, int ntuples, int nfields, void *data)
1413
1412
  rb_yield( rb_ary_new4( nfields, row_values ));
1414
1413
  }
1415
1414
 
1416
- pgresult_clear( this );
1415
+ return 1; /* clear the result */
1417
1416
  }
1418
1417
 
1419
- static void
1418
+ static int
1420
1419
  yield_tuple(VALUE self, int ntuples, int nfields, void *data)
1421
1420
  {
1422
1421
  int tuple_num;
@@ -1434,11 +1433,12 @@ yield_tuple(VALUE self, int ntuples, int nfields, void *data)
1434
1433
  VALUE tuple = pgresult_tuple(copy, INT2FIX(tuple_num));
1435
1434
  rb_yield( tuple );
1436
1435
  }
1436
+ return 0; /* don't clear the result */
1437
1437
  }
1438
1438
 
1439
1439
  /* Non-static, and data pointer for use by sequel_pg */
1440
1440
  VALUE
1441
- pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* data)
1441
+ pgresult_stream_any(VALUE self, int (*yielder)(VALUE, int, int, void*), void* data)
1442
1442
  {
1443
1443
  t_pg_result *this;
1444
1444
  int nfields;
@@ -1467,7 +1467,9 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* d
1467
1467
  pg_result_check( self );
1468
1468
  }
1469
1469
 
1470
- yielder( self, ntuples, nfields, data );
1470
+ if( yielder( self, ntuples, nfields, data ) ){
1471
+ pgresult_clear( this );
1472
+ }
1471
1473
 
1472
1474
  if( gvl_PQisBusy(pgconn) ){
1473
1475
  /* wait for input (without blocking) before reading each result */
@@ -1476,10 +1478,10 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* d
1476
1478
 
1477
1479
  pgresult = gvl_PQgetResult(pgconn);
1478
1480
  if( pgresult == NULL )
1479
- rb_raise( rb_eNoResultError, "no result received - possibly an intersection with another result retrieval");
1481
+ rb_raise( rb_eNoResultError, "no result received - possibly an intersection with another query");
1480
1482
 
1481
1483
  if( nfields != PQnfields(pgresult) )
1482
- rb_raise( rb_eInvalidChangeOfResultFields, "number of fields must not change in single row mode");
1484
+ rb_raise( rb_eInvalidChangeOfResultFields, "number of fields changed in single row mode from %d to %d - this is a sign for intersection with another query", nfields, PQnfields(pgresult));
1483
1485
 
1484
1486
  this->pgresult = pgresult;
1485
1487
  }
@@ -1617,7 +1619,7 @@ pgresult_field_name_type_get(VALUE self)
1617
1619
  }
1618
1620
 
1619
1621
  void
1620
- init_pg_result()
1622
+ init_pg_result(void)
1621
1623
  {
1622
1624
  sym_string = ID2SYM(rb_intern("string"));
1623
1625
  sym_symbol = ID2SYM(rb_intern("symbol"));
@@ -923,7 +923,7 @@ pg_text_dec_inet(t_pg_coder *conv, const char *val, int len, int tuple, int fiel
923
923
  }
924
924
 
925
925
  void
926
- init_pg_text_decoder()
926
+ init_pg_text_decoder(void)
927
927
  {
928
928
  rb_require("ipaddr");
929
929
  s_IPAddr = rb_funcall(rb_cObject, rb_intern("const_get"), 1, rb_str_new2("IPAddr"));
@@ -775,7 +775,7 @@ pg_text_enc_to_base64(t_pg_coder *conv, VALUE value, char *out, VALUE *intermedi
775
775
 
776
776
 
777
777
  void
778
- init_pg_text_encoder()
778
+ init_pg_text_encoder(void)
779
779
  {
780
780
  s_id_encode = rb_intern("encode");
781
781
  s_id_to_i = rb_intern("to_i");
data/ext/pg_tuple.c CHANGED
@@ -545,7 +545,7 @@ pg_tuple_load(VALUE self, VALUE a)
545
545
  }
546
546
 
547
547
  void
548
- init_pg_tuple()
548
+ init_pg_tuple(void)
549
549
  {
550
550
  rb_cPG_Tuple = rb_define_class_under( rb_mPG, "Tuple", rb_cObject );
551
551
  rb_define_alloc_func( rb_cPG_Tuple, pg_tuple_s_allocate );
data/ext/pg_type_map.c CHANGED
@@ -176,7 +176,7 @@ pg_typemap_with_default_type_map(VALUE self, VALUE typemap)
176
176
  }
177
177
 
178
178
  void
179
- init_pg_type_map()
179
+ init_pg_type_map(void)
180
180
  {
181
181
  s_id_fit_to_query = rb_intern("fit_to_query");
182
182
  s_id_fit_to_result = rb_intern("fit_to_result");
@@ -105,7 +105,7 @@ pg_tmas_s_allocate( VALUE klass )
105
105
 
106
106
 
107
107
  void
108
- init_pg_type_map_all_strings()
108
+ init_pg_type_map_all_strings(void)
109
109
  {
110
110
  /*
111
111
  * Document-class: PG::TypeMapAllStrings < PG::TypeMap
@@ -247,7 +247,7 @@ pg_tmbk_coders( VALUE self )
247
247
  }
248
248
 
249
249
  void
250
- init_pg_type_map_by_class()
250
+ init_pg_type_map_by_class(void)
251
251
  {
252
252
  /*
253
253
  * Document-class: PG::TypeMapByClass < PG::TypeMap
@@ -243,7 +243,7 @@ pg_tmbc_s_allocate( VALUE klass )
243
243
  }
244
244
 
245
245
  VALUE
246
- pg_tmbc_allocate()
246
+ pg_tmbc_allocate(void)
247
247
  {
248
248
  return pg_tmbc_s_allocate(rb_cTypeMapByColumn);
249
249
  }
@@ -320,7 +320,7 @@ pg_tmbc_coders(VALUE self)
320
320
  }
321
321
 
322
322
  void
323
- init_pg_type_map_by_column()
323
+ init_pg_type_map_by_column(void)
324
324
  {
325
325
  s_id_decode = rb_intern("decode");
326
326
  s_id_encode = rb_intern("encode");
@@ -286,7 +286,7 @@ pg_tmbmt_coders( VALUE self )
286
286
  }
287
287
 
288
288
  void
289
- init_pg_type_map_by_mri_type()
289
+ init_pg_type_map_by_mri_type(void)
290
290
  {
291
291
  /*
292
292
  * Document-class: PG::TypeMapByMriType < PG::TypeMap
@@ -356,7 +356,7 @@ pg_tmbo_build_column_map( VALUE self, VALUE result )
356
356
 
357
357
 
358
358
  void
359
- init_pg_type_map_by_oid()
359
+ init_pg_type_map_by_oid(void)
360
360
  {
361
361
  s_id_decode = rb_intern("decode");
362
362
 
@@ -299,7 +299,7 @@ pg_tmir_s_allocate( VALUE klass )
299
299
 
300
300
 
301
301
  void
302
- init_pg_type_map_in_ruby()
302
+ init_pg_type_map_in_ruby(void)
303
303
  {
304
304
  s_id_fit_to_result = rb_intern("fit_to_result");
305
305
  s_id_fit_to_query = rb_intern("fit_to_query");
data/lib/pg/connection.rb CHANGED
@@ -46,37 +46,6 @@ class PG::Connection
46
46
  hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
47
47
  end
48
48
 
49
- # Decode a connection string to Hash options
50
- #
51
- # Value are properly unquoted and unescaped.
52
- def self.connect_string_to_hash( str )
53
- options = {}
54
- key = nil
55
- value = String.new
56
- str.scan(/\G\s*(?>([^\s\\\']+)\s*=\s*|([^\s\\\']+)|'((?:[^\'\\]|\\.)*)'|(\\.?)|(\S))(\s|\z)?/m) do
57
- |k, word, sq, esc, garbage, sep|
58
- raise ArgumentError, "unterminated quoted string in connection info string: #{str.inspect}" if garbage
59
- if k
60
- key = k
61
- else
62
- value << (word || (sq || esc).gsub(/\\(.)/, '\\1'))
63
- end
64
- if sep
65
- raise ArgumentError, "missing = after #{value.inspect}" unless key
66
- options[key.to_sym] = value
67
- key = nil
68
- value = String.new
69
- end
70
- end
71
- options
72
- end
73
-
74
- # URI defined in RFC3986
75
- # This regexp is modified to allow host to specify multiple comma separated components captured as <hostports> and to disallow comma in hostnames.
76
- # Taken from: https://github.com/ruby/ruby/blob/be04006c7d2f9aeb7e9d8d09d945b3a9c7850202/lib/uri/rfc3986_parser.rb#L6
77
- HOST_AND_PORT = /(?<hostport>(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[-\.!$&-+0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)/
78
- POSTGRESQL_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<hostports>#{HOST_AND_PORT}(?:,\g<hostport>)*))(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
79
-
80
49
  # Parse the connection +args+ into a connection-parameter string.
81
50
  # See PG::Connection.new for valid arguments.
82
51
  #
@@ -87,91 +56,64 @@ class PG::Connection
87
56
  # * URI object
88
57
  # * positional arguments
89
58
  #
90
- # The method adds the option "hostaddr" and "fallback_application_name" if they aren't already set.
91
- # The URI and the options string is passed through and "hostaddr" as well as "fallback_application_name"
92
- # are added to the end.
93
- def self::parse_connect_args( *args )
59
+ # The method adds the option "fallback_application_name" if it isn't already set.
60
+ # It returns a connection string with "key=value" pairs.
61
+ def self.parse_connect_args( *args )
94
62
  hash_arg = args.last.is_a?( Hash ) ? args.pop.transform_keys(&:to_sym) : {}
95
- option_string = ""
96
63
  iopts = {}
97
64
 
98
65
  if args.length == 1
99
66
  case args.first
100
- when URI, POSTGRESQL_URI
101
- uri = args.first.to_s
102
- uri_match = POSTGRESQL_URI.match(uri)
103
- if uri_match['query']
104
- iopts = URI.decode_www_form(uri_match['query']).to_h.transform_keys(&:to_sym)
105
- end
106
- # extract "host1,host2" from "host1:5432,host2:5432"
107
- iopts[:host] = uri_match['hostports'].split(',', -1).map do |hostport|
108
- hostmatch = /\A#{HOST_AND_PORT}\z/.match(hostport)
109
- hostmatch['IPv6address'] || hostmatch['IPv4address'] || hostmatch['reg-name']&.gsub(/%(\h\h)/){ $1.hex.chr }
110
- end.join(',')
111
- oopts = {}
112
- when /=/
113
- # Option string style
114
- option_string = args.first.to_s
115
- iopts = connect_string_to_hash(option_string)
116
- oopts = {}
67
+ when URI, /=/, /:\/\//
68
+ # Option or URL string style
69
+ conn_string = args.first.to_s
70
+ iopts = PG::Connection.conninfo_parse(conn_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
117
71
  else
118
72
  # Positional parameters (only host given)
119
73
  iopts[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
120
- oopts = iopts.dup
121
74
  end
122
75
  else
123
- # Positional parameters
76
+ # Positional parameters with host and more
124
77
  max = CONNECT_ARGUMENT_ORDER.length
125
78
  raise ArgumentError,
126
- "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
79
+ "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
127
80
 
128
81
  CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
129
82
  iopts[ k.to_sym ] = v if v
130
83
  end
131
84
  iopts.delete(:tty) # ignore obsolete tty parameter
132
- oopts = iopts.dup
133
85
  end
134
86
 
135
87
  iopts.merge!( hash_arg )
136
- oopts.merge!( hash_arg )
137
-
138
- # Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
139
- if (host=iopts[:host]) && !iopts[:hostaddr]
140
- hostaddrs = host.split(",", -1).map do |mhost|
141
- if !mhost.empty? && !mhost.start_with?("/") && # isn't UnixSocket
142
- # isn't a path on Windows
143
- (RUBY_PLATFORM !~ /mingw|mswin/ || mhost !~ /\A\w:[\/\\]/)
144
-
145
- if Fiber.respond_to?(:scheduler) &&
146
- Fiber.scheduler &&
147
- RUBY_VERSION < '3.1.'
148
-
149
- # Use a second thread to avoid blocking of the scheduler.
150
- # `IPSocket.getaddress` isn't fiber aware before ruby-3.1.
151
- Thread.new{ IPSocket.getaddress(mhost) rescue '' }.value
152
- else
153
- IPSocket.getaddress(mhost) rescue ''
154
- end
155
- end
156
- end
157
- oopts[:hostaddr] = hostaddrs.join(",") if hostaddrs.any?
158
- end
159
88
 
160
89
  if !iopts[:fallback_application_name]
161
- oopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
90
+ iopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
162
91
  end
163
92
 
164
- if uri
165
- uri += uri_match['query'] ? "&" : "?"
166
- uri += URI.encode_www_form( oopts )
167
- return uri
93
+ return connect_hash_to_string(iopts)
94
+ end
95
+
96
+ # Return a String representation of the object suitable for debugging.
97
+ def inspect
98
+ str = self.to_s
99
+ str[-1,0] = if finished?
100
+ " finished"
168
101
  else
169
- option_string += ' ' unless option_string.empty? && oopts.empty?
170
- return option_string + connect_hash_to_string(oopts)
102
+ stats = []
103
+ stats << " status=#{ PG.constants.grep(/CONNECTION_/).find{|c| PG.const_get(c) == status} }" if status != CONNECTION_OK
104
+ stats << " transaction_status=#{ PG.constants.grep(/PQTRANS_/).find{|c| PG.const_get(c) == transaction_status} }" if transaction_status != PG::PQTRANS_IDLE
105
+ stats << " nonblocking=#{ isnonblocking }" if isnonblocking
106
+ stats << " pipeline_status=#{ PG.constants.grep(/PQ_PIPELINE_/).find{|c| PG.const_get(c) == pipeline_status} }" if respond_to?(:pipeline_status) && pipeline_status != PG::PQ_PIPELINE_OFF
107
+ stats << " client_encoding=#{ get_client_encoding }" if get_client_encoding != "UTF8"
108
+ stats << " type_map_for_results=#{ type_map_for_results.to_s }" unless type_map_for_results.is_a?(PG::TypeMapAllStrings)
109
+ stats << " type_map_for_queries=#{ type_map_for_queries.to_s }" unless type_map_for_queries.is_a?(PG::TypeMapAllStrings)
110
+ stats << " encoder_for_put_copy_data=#{ encoder_for_put_copy_data.to_s }" if encoder_for_put_copy_data
111
+ stats << " decoder_for_get_copy_data=#{ decoder_for_get_copy_data.to_s }" if decoder_for_get_copy_data
112
+ " host=#{host} port=#{port} user=#{user}#{stats.join}"
171
113
  end
114
+ return str
172
115
  end
173
116
 
174
-
175
117
  # call-seq:
176
118
  # conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
177
119
  #
@@ -241,7 +183,7 @@ class PG::Connection
241
183
  # ["more", "data", "to", "copy"]
242
184
 
243
185
  def copy_data( sql, coder=nil )
244
- raise PG::NotInBlockingMode, "copy_data can not be used in nonblocking mode" if nonblocking?
186
+ raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
245
187
  res = exec( sql )
246
188
 
247
189
  case res.result_status
@@ -273,11 +215,15 @@ class PG::Connection
273
215
  yield res
274
216
  rescue Exception => err
275
217
  cancel
276
- while get_copy_data
218
+ begin
219
+ while get_copy_data
220
+ end
221
+ rescue PG::Error
222
+ # Ignore error in cleanup to avoid losing original exception
277
223
  end
278
224
  while get_result
279
225
  end
280
- raise
226
+ raise err
281
227
  else
282
228
  res = get_last_result
283
229
  if !res || res.result_status != PGRES_COMMAND_OK
@@ -285,7 +231,7 @@ class PG::Connection
285
231
  end
286
232
  while get_result
287
233
  end
288
- raise PG::NotAllCopyDataRetrieved, "Not all COPY data retrieved"
234
+ raise PG::NotAllCopyDataRetrieved.new("Not all COPY data retrieved", connection: self)
289
235
  end
290
236
  res
291
237
  ensure
@@ -310,16 +256,17 @@ class PG::Connection
310
256
  # and a +COMMIT+ at the end of the block, or
311
257
  # +ROLLBACK+ if any exception occurs.
312
258
  def transaction
259
+ rollback = false
313
260
  exec "BEGIN"
314
- res = yield(self)
261
+ yield(self)
315
262
  rescue Exception
263
+ rollback = true
316
264
  cancel if transaction_status == PG::PQTRANS_ACTIVE
317
265
  block
318
266
  exec "ROLLBACK"
319
267
  raise
320
- else
321
- exec "COMMIT"
322
- res
268
+ ensure
269
+ exec "COMMIT" unless rollback
323
270
  end
324
271
 
325
272
  ### Returns an array of Hashes with connection defaults. See ::conndefaults
@@ -482,10 +429,20 @@ class PG::Connection
482
429
  # See also #copy_data.
483
430
  #
484
431
  def put_copy_data(buffer, encoder=nil)
485
- until sync_put_copy_data(buffer, encoder)
486
- flush
432
+ # sync_put_copy_data does a non-blocking attept to flush data.
433
+ until res=sync_put_copy_data(buffer, encoder)
434
+ # It didn't flush immediately and allocation of more buffering memory failed.
435
+ # Wait for all data sent by doing a blocking flush.
436
+ res = flush
487
437
  end
488
- flush
438
+
439
+ # And do a blocking flush every 100 calls.
440
+ # This is to avoid memory bloat, when sending the data is slower than calls to put_copy_data happen.
441
+ if (@calls_to_put_copy_data += 1) > 100
442
+ @calls_to_put_copy_data = 0
443
+ res = flush
444
+ end
445
+ res
489
446
  end
490
447
  alias async_put_copy_data put_copy_data
491
448
 
@@ -505,6 +462,7 @@ class PG::Connection
505
462
  until sync_put_copy_end(*args)
506
463
  flush
507
464
  end
465
+ @calls_to_put_copy_data = 0
508
466
  flush
509
467
  end
510
468
  alias async_put_copy_end put_copy_end
@@ -545,6 +503,7 @@ class PG::Connection
545
503
  def reset
546
504
  reset_start
547
505
  async_connect_or_reset(:reset_poll)
506
+ self
548
507
  end
549
508
  alias async_reset reset
550
509
 
@@ -613,18 +572,54 @@ class PG::Connection
613
572
 
614
573
  private def async_connect_or_reset(poll_meth)
615
574
  # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
575
+
576
+ if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
577
+ # Lowest timeout is 2 seconds - like in libpq
578
+ timeo = [timeo, 2].max
579
+ host_count = conninfo_hash[:host].to_s.count(",") + 1
580
+ stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
581
+ end
582
+
616
583
  poll_status = PG::PGRES_POLLING_WRITING
617
584
  until poll_status == PG::PGRES_POLLING_OK ||
618
585
  poll_status == PG::PGRES_POLLING_FAILED
619
586
 
620
- # If the socket needs to read, wait 'til it becomes readable to poll again
621
- case poll_status
622
- when PG::PGRES_POLLING_READING
623
- socket_io.wait_readable
587
+ # Set single timeout to parameter "connect_timeout" but
588
+ # don't exceed total connection time of number-of-hosts * connect_timeout.
589
+ timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
590
+ event = if !timeout || timeout >= 0
591
+ # If the socket needs to read, wait 'til it becomes readable to poll again
592
+ case poll_status
593
+ when PG::PGRES_POLLING_READING
594
+ if defined?(IO::READABLE) # ruby-3.0+
595
+ socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
596
+ else
597
+ IO.select([socket_io], nil, [socket_io], timeout)
598
+ end
624
599
 
625
- # ...and the same for when the socket needs to write
626
- when PG::PGRES_POLLING_WRITING
627
- socket_io.wait_writable
600
+ # ...and the same for when the socket needs to write
601
+ when PG::PGRES_POLLING_WRITING
602
+ if defined?(IO::WRITABLE) # ruby-3.0+
603
+ # Use wait instead of wait_readable, since connection errors are delivered as
604
+ # exceptional/priority events on Windows.
605
+ socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
606
+ else
607
+ # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
608
+ IO.select(nil, [socket_io], [socket_io], timeout)
609
+ end
610
+ end
611
+ end
612
+ # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
613
+ # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
614
+ unless event
615
+ if self.class.send(:host_is_named_pipe?, host)
616
+ connhost = "on socket \"#{host}\""
617
+ elsif respond_to?(:hostaddr)
618
+ connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
619
+ else
620
+ connhost = "at \"#{host}\", port #{port}"
621
+ end
622
+ raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
628
623
  end
629
624
 
630
625
  # Check to see if it's finished or failed yet
@@ -634,7 +629,7 @@ class PG::Connection
634
629
  unless status == PG::CONNECTION_OK
635
630
  msg = error_message
636
631
  finish
637
- raise PG::ConnectionBad, msg
632
+ raise PG::ConnectionBad.new(msg, connection: self)
638
633
  end
639
634
 
640
635
  # Set connection to nonblocking to handle all blocking states in ruby.
@@ -642,8 +637,6 @@ class PG::Connection
642
637
  sync_setnonblocking(true)
643
638
  self.flush_data = true
644
639
  set_default_encoding
645
-
646
- self
647
640
  end
648
641
 
649
642
  class << self
@@ -698,13 +691,17 @@ class PG::Connection
698
691
  # connection will have its +client_encoding+ set accordingly.
699
692
  #
700
693
  # Raises a PG::Error if the connection fails.
701
- def new(*args, **kwargs)
702
- conn = self.connect_start(*args, **kwargs ) or
703
- raise(PG::Error, "Unable to create a new connection")
704
-
705
- raise(PG::ConnectionBad, conn.error_message) if conn.status == PG::CONNECTION_BAD
706
-
707
- conn.send(:async_connect_or_reset, :connect_poll)
694
+ def new(*args)
695
+ conn = connect_to_hosts(*args)
696
+
697
+ if block_given?
698
+ begin
699
+ return yield conn
700
+ ensure
701
+ conn.finish
702
+ end
703
+ end
704
+ conn
708
705
  end
709
706
  alias async_connect new
710
707
  alias connect new
@@ -712,6 +709,65 @@ class PG::Connection
712
709
  alias setdb new
713
710
  alias setdblogin new
714
711
 
712
+ private def connect_to_hosts(*args)
713
+ option_string = parse_connect_args(*args)
714
+ iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
715
+ iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
716
+
717
+ if iopts[:hostaddr]
718
+ # hostaddr is provided -> no need to resolve hostnames
719
+
720
+ elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
721
+ # Resolve DNS in Ruby to avoid blocking state while connecting.
722
+ # Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
723
+ # This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
724
+ ihosts = iopts[:host].split(",", -1)
725
+ iports = iopts[:port].split(",", -1)
726
+ iports = [nil] if iports.size == 0
727
+ iports = iports * ihosts.size if iports.size == 1
728
+ raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
729
+
730
+ dests = ihosts.each_with_index.flat_map do |mhost, idx|
731
+ unless host_is_named_pipe?(mhost)
732
+ if Fiber.respond_to?(:scheduler) &&
733
+ Fiber.scheduler &&
734
+ RUBY_VERSION < '3.1.'
735
+
736
+ # Use a second thread to avoid blocking of the scheduler.
737
+ # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
738
+ hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
739
+ else
740
+ hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
741
+ end
742
+ else
743
+ # No hostname to resolve (UnixSocket)
744
+ hostaddrs = [nil]
745
+ end
746
+ hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
747
+ end
748
+ iopts.merge!(
749
+ hostaddr: dests.map{|d| d[0] }.join(","),
750
+ host: dests.map{|d| d[1] }.join(","),
751
+ port: dests.map{|d| d[2] }.join(","))
752
+ else
753
+ # No host given
754
+ end
755
+ conn = self.connect_start(iopts) or
756
+ raise(PG::Error, "Unable to create a new connection")
757
+
758
+ raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
759
+
760
+ conn.send(:async_connect_or_reset, :connect_poll)
761
+ conn
762
+ end
763
+
764
+ private def host_is_named_pipe?(host_string)
765
+ host_string.empty? || host_string.start_with?("/") || # it's UnixSocket?
766
+ host_string.start_with?("@") || # it's UnixSocket in the abstract namespace?
767
+ # it's a path on Windows?
768
+ (RUBY_PLATFORM =~ /mingw|mswin/ && host_string =~ /\A([\/\\]|\w:[\/\\])/)
769
+ end
770
+
715
771
  # call-seq:
716
772
  # PG::Connection.ping(connection_hash) -> Integer
717
773
  # PG::Connection.ping(connection_string) -> Integer
data/lib/pg/exceptions.rb CHANGED
@@ -6,7 +6,13 @@ require 'pg' unless defined?( PG )
6
6
 
7
7
  module PG
8
8
 
9
- class Error < StandardError; end
9
+ class Error < StandardError
10
+ def initialize(msg=nil, connection: nil, result: nil)
11
+ @connection = connection
12
+ @result = result
13
+ super(msg)
14
+ end
15
+ end
10
16
 
11
17
  end # module PG
12
18
 
data/lib/pg/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module PG
2
2
  # Library version
3
- VERSION = '1.3.5'
3
+ VERSION = '1.4.5'
4
4
  end