pg 1.4.6 → 1.6.1

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 (92) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/{History.md → CHANGELOG.md} +185 -3
  4. data/Gemfile +12 -3
  5. data/README-Windows.rdoc +1 -1
  6. data/README.ja.md +75 -41
  7. data/README.md +86 -31
  8. data/Rakefile +95 -14
  9. data/certs/kanis@comcard.de.pem +20 -0
  10. data/certs/larskanis-2024.pem +24 -0
  11. data/ext/errorcodes.def +4 -5
  12. data/ext/errorcodes.txt +2 -5
  13. data/ext/extconf.rb +165 -14
  14. data/ext/gvl_wrappers.c +13 -2
  15. data/ext/gvl_wrappers.h +33 -0
  16. data/ext/pg.c +28 -35
  17. data/ext/pg.h +18 -14
  18. data/ext/pg_binary_decoder.c +231 -0
  19. data/ext/pg_binary_encoder.c +427 -0
  20. data/ext/pg_cancel_connection.c +360 -0
  21. data/ext/pg_coder.c +70 -12
  22. data/ext/pg_connection.c +473 -208
  23. data/ext/pg_copy_coder.c +316 -23
  24. data/ext/pg_record_coder.c +12 -11
  25. data/ext/pg_result.c +102 -30
  26. data/ext/pg_text_decoder.c +31 -10
  27. data/ext/pg_text_encoder.c +58 -26
  28. data/ext/pg_tuple.c +36 -33
  29. data/ext/pg_type_map.c +4 -3
  30. data/ext/pg_type_map_all_strings.c +3 -3
  31. data/ext/pg_type_map_by_class.c +6 -4
  32. data/ext/pg_type_map_by_column.c +9 -4
  33. data/ext/pg_type_map_by_mri_type.c +1 -1
  34. data/ext/pg_type_map_by_oid.c +10 -5
  35. data/ext/pg_type_map_in_ruby.c +6 -3
  36. data/lib/pg/basic_type_map_based_on_result.rb +21 -1
  37. data/lib/pg/basic_type_map_for_queries.rb +23 -10
  38. data/lib/pg/basic_type_map_for_results.rb +26 -3
  39. data/lib/pg/basic_type_registry.rb +46 -36
  40. data/lib/pg/binary_decoder/date.rb +9 -0
  41. data/lib/pg/binary_decoder/timestamp.rb +26 -0
  42. data/lib/pg/binary_encoder/timestamp.rb +20 -0
  43. data/lib/pg/cancel_connection.rb +53 -0
  44. data/lib/pg/coder.rb +18 -14
  45. data/lib/pg/connection.rb +387 -172
  46. data/lib/pg/exceptions.rb +6 -0
  47. data/lib/pg/text_decoder/date.rb +21 -0
  48. data/lib/pg/text_decoder/inet.rb +9 -0
  49. data/lib/pg/text_decoder/json.rb +17 -0
  50. data/lib/pg/text_decoder/numeric.rb +9 -0
  51. data/lib/pg/text_decoder/timestamp.rb +30 -0
  52. data/lib/pg/text_encoder/date.rb +13 -0
  53. data/lib/pg/text_encoder/inet.rb +31 -0
  54. data/lib/pg/text_encoder/json.rb +17 -0
  55. data/lib/pg/text_encoder/numeric.rb +9 -0
  56. data/lib/pg/text_encoder/timestamp.rb +24 -0
  57. data/lib/pg/version.rb +1 -1
  58. data/lib/pg.rb +78 -17
  59. data/misc/yugabyte/Dockerfile +9 -0
  60. data/misc/yugabyte/docker-compose.yml +28 -0
  61. data/misc/yugabyte/pg-test.rb +45 -0
  62. data/pg.gemspec +9 -5
  63. data/ports/patches/krb5/1.21.3/0001-Allow-static-linking-krb5-library.patch +30 -0
  64. data/ports/patches/openssl/3.5.1/0001-aarch64-mingw.patch +21 -0
  65. data/ports/patches/postgresql/17.5/0001-Use-workaround-of-__builtin_setjmp-only-on-MINGW-on-.patch +42 -0
  66. data/ports/patches/postgresql/17.5/0001-libpq-Process-buffered-SSL-read-bytes-to-support-rec.patch +52 -0
  67. data/rakelib/pg_gem_helper.rb +64 -0
  68. data.tar.gz.sig +0 -0
  69. metadata +61 -49
  70. metadata.gz.sig +0 -0
  71. data/.appveyor.yml +0 -42
  72. data/.gems +0 -6
  73. data/.gemtest +0 -0
  74. data/.github/workflows/binary-gems.yml +0 -117
  75. data/.github/workflows/source-gem.yml +0 -137
  76. data/.gitignore +0 -19
  77. data/.hgsigs +0 -34
  78. data/.hgtags +0 -41
  79. data/.irbrc +0 -23
  80. data/.pryrc +0 -23
  81. data/.tm_properties +0 -21
  82. data/.travis.yml +0 -49
  83. data/Manifest.txt +0 -72
  84. data/Rakefile.cross +0 -298
  85. data/lib/pg/binary_decoder.rb +0 -23
  86. data/lib/pg/constants.rb +0 -12
  87. data/lib/pg/text_decoder.rb +0 -46
  88. data/lib/pg/text_encoder.rb +0 -59
  89. data/translation/.po4a-version +0 -7
  90. data/translation/po/all.pot +0 -875
  91. data/translation/po/ja.po +0 -868
  92. data/translation/po4a.cfg +0 -9
data/lib/pg/connection.rb CHANGED
@@ -2,8 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'pg' unless defined?( PG )
5
- require 'uri'
6
- require 'io/wait'
5
+ require 'io/wait' unless ::IO.public_instance_methods(false).include?(:wait_readable) # for ruby < 3.0
7
6
  require 'socket'
8
7
 
9
8
  # The PostgreSQL connection class. The interface for this class is based on
@@ -31,8 +30,8 @@ require 'socket'
31
30
  class PG::Connection
32
31
 
33
32
  # The order the options are passed to the ::connect method.
34
- CONNECT_ARGUMENT_ORDER = %w[host port options tty dbname user password]
35
-
33
+ CONNECT_ARGUMENT_ORDER = %w[host port options tty dbname user password].freeze
34
+ private_constant :CONNECT_ARGUMENT_ORDER
36
35
 
37
36
  ### Quote a single +value+ for use in a connection-parameter string.
38
37
  def self.quote_connstr( value )
@@ -46,6 +45,10 @@ class PG::Connection
46
45
  hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
47
46
  end
48
47
 
48
+ # Shareable program name for Ractor
49
+ PROGRAM_NAME = $PROGRAM_NAME.dup.freeze
50
+ private_constant :PROGRAM_NAME
51
+
49
52
  # Parse the connection +args+ into a connection-parameter string.
50
53
  # See PG::Connection.new for valid arguments.
51
54
  #
@@ -63,8 +66,8 @@ class PG::Connection
63
66
  iopts = {}
64
67
 
65
68
  if args.length == 1
66
- case args.first
67
- when URI, /=/, /:\/\//
69
+ case args.first.to_s
70
+ when /=/, /:\/\//
68
71
  # Option or URL string style
69
72
  conn_string = args.first.to_s
70
73
  iopts = PG::Connection.conninfo_parse(conn_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
@@ -87,7 +90,7 @@ class PG::Connection
87
90
  iopts.merge!( hash_arg )
88
91
 
89
92
  if !iopts[:fallback_application_name]
90
- iopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
93
+ iopts[:fallback_application_name] = PROGRAM_NAME.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
91
94
  end
92
95
 
93
96
  return connect_hash_to_string(iopts)
@@ -114,6 +117,9 @@ class PG::Connection
114
117
  return str
115
118
  end
116
119
 
120
+ BinarySignature = "PGCOPY\n\377\r\n\0"
121
+ private_constant :BinarySignature
122
+
117
123
  # call-seq:
118
124
  # conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
119
125
  #
@@ -160,6 +166,17 @@ class PG::Connection
160
166
  # conn.put_copy_data ['more', 'data', 'to', 'copy']
161
167
  # end
162
168
  #
169
+ # All 4 CopyRow classes can take a type map to specify how the columns are mapped to and from the database format.
170
+ # For details see the particular CopyRow class description.
171
+ #
172
+ # PG::BinaryEncoder::CopyRow can be used to send data in binary format to the server.
173
+ # In this case copy_data generates the header and trailer data automatically:
174
+ # enco = PG::BinaryEncoder::CopyRow.new
175
+ # conn.copy_data "COPY my_table FROM STDIN (FORMAT binary)", enco do
176
+ # conn.put_copy_data ['some', 'data', 'to', 'copy']
177
+ # conn.put_copy_data ['more', 'data', 'to', 'copy']
178
+ # end
179
+ #
163
180
  # Example with CSV output format:
164
181
  # conn.copy_data "COPY my_table TO STDOUT CSV" do
165
182
  # while row=conn.get_copy_data
@@ -181,6 +198,18 @@ class PG::Connection
181
198
  # This receives all rows of +my_table+ as ruby array:
182
199
  # ["some", "data", "to", "copy"]
183
200
  # ["more", "data", "to", "copy"]
201
+ #
202
+ # Also PG::BinaryDecoder::CopyRow can be used to retrieve data in binary format from the server.
203
+ # In this case the header and trailer data is processed by the decoder and the remaining +nil+ from get_copy_data is processed by copy_data, so that binary data can be processed equally to text data:
204
+ # deco = PG::BinaryDecoder::CopyRow.new
205
+ # conn.copy_data "COPY my_table TO STDOUT (FORMAT binary)", deco do
206
+ # while row=conn.get_copy_data
207
+ # p row
208
+ # end
209
+ # end
210
+ # This receives all rows of +my_table+ as ruby array:
211
+ # ["some", "data", "to", "copy"]
212
+ # ["more", "data", "to", "copy"]
184
213
 
185
214
  def copy_data( sql, coder=nil )
186
215
  raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
@@ -189,10 +218,16 @@ class PG::Connection
189
218
  case res.result_status
190
219
  when PGRES_COPY_IN
191
220
  begin
221
+ if coder && res.binary_tuples == 1
222
+ # Binary file header (11 byte signature, 32 bit flags and 32 bit extension length)
223
+ put_copy_data(BinarySignature + ("\x00" * 8))
224
+ end
225
+
192
226
  if coder
193
227
  old_coder = self.encoder_for_put_copy_data
194
228
  self.encoder_for_put_copy_data = coder
195
229
  end
230
+
196
231
  yield res
197
232
  rescue Exception => err
198
233
  errmsg = "%s while copy data: %s" % [ err.class.name, err.message ]
@@ -205,6 +240,12 @@ class PG::Connection
205
240
  raise err
206
241
  else
207
242
  begin
243
+ self.encoder_for_put_copy_data = old_coder if coder
244
+
245
+ if coder && res.binary_tuples == 1
246
+ put_copy_data("\xFF\xFF") # Binary file trailer 16 bit "-1"
247
+ end
248
+
208
249
  put_copy_end
209
250
  rescue PG::Error => err
210
251
  raise PG::LostCopyState.new("#{err} (probably by executing another SQL query while running a COPY command)", connection: self)
@@ -226,6 +267,14 @@ class PG::Connection
226
267
  discard_results
227
268
  raise
228
269
  else
270
+ if coder && res.binary_tuples == 1
271
+ # There are two end markers in binary mode: file trailer and the final nil.
272
+ # The file trailer is expected to be processed by BinaryDecoder::CopyRow and already returns nil, so that the remaining NULL from PQgetCopyData is retrieved here:
273
+ if get_copy_data
274
+ discard_results
275
+ raise PG::NotAllCopyDataRetrieved.new("Not all binary COPY data retrieved", connection: self)
276
+ end
277
+ end
229
278
  res = get_last_result
230
279
  if !res
231
280
  discard_results
@@ -260,6 +309,11 @@ class PG::Connection
260
309
  rollback = false
261
310
  exec "BEGIN"
262
311
  yield(self)
312
+ rescue PG::RollbackTransaction
313
+ rollback = true
314
+ cancel if transaction_status == PG::PQTRANS_ACTIVE
315
+ block
316
+ exec "ROLLBACK"
263
317
  rescue Exception
264
318
  rollback = true
265
319
  cancel if transaction_status == PG::PQTRANS_ACTIVE
@@ -302,24 +356,38 @@ class PG::Connection
302
356
  end
303
357
  end
304
358
 
305
- # Method 'ssl_attribute' was introduced in PostgreSQL 9.5.
306
- if self.instance_methods.find{|m| m.to_sym == :ssl_attribute }
307
- # call-seq:
308
- # conn.ssl_attributes -> Hash<String,String>
309
- #
310
- # Returns SSL-related information about the connection as key/value pairs
311
- #
312
- # The available attributes varies depending on the SSL library being used,
313
- # and the type of connection.
314
- #
315
- # See also #ssl_attribute
316
- def ssl_attributes
317
- ssl_attribute_names.each.with_object({}) do |n,h|
318
- h[n] = ssl_attribute(n)
319
- end
359
+ # call-seq:
360
+ # conn.ssl_attributes -> Hash<String,String>
361
+ #
362
+ # Returns SSL-related information about the connection as key/value pairs
363
+ #
364
+ # The available attributes varies depending on the SSL library being used,
365
+ # and the type of connection.
366
+ #
367
+ # See also #ssl_attribute
368
+ def ssl_attributes
369
+ ssl_attribute_names.each.with_object({}) do |n,h|
370
+ h[n] = ssl_attribute(n)
320
371
  end
321
372
  end
322
373
 
374
+ # Read all pending socket input to internal memory and raise an exception in case of errors.
375
+ #
376
+ # This verifies that the connection socket is in a usable state and not aborted in any way.
377
+ # No communication is done with the server.
378
+ # Only pending data is read from the socket - the method doesn't wait for any outstanding server answers.
379
+ #
380
+ # Raises a kind of PG::Error if there was an error reading the data or if the socket is in a failure state.
381
+ #
382
+ # The method doesn't verify that the server is still responding.
383
+ # To verify that the communication to the server works, it is recommended to use something like <tt>conn.exec('')</tt> instead.
384
+ def check_socket
385
+ while socket_io.wait_readable(0)
386
+ consume_input
387
+ end
388
+ nil
389
+ end
390
+
323
391
  # call-seq:
324
392
  # conn.get_result() -> PG::Result
325
393
  # conn.get_result() {|pg_result| block }
@@ -430,7 +498,7 @@ class PG::Connection
430
498
  # See also #copy_data.
431
499
  #
432
500
  def put_copy_data(buffer, encoder=nil)
433
- # sync_put_copy_data does a non-blocking attept to flush data.
501
+ # sync_put_copy_data does a non-blocking attempt to flush data.
434
502
  until res=sync_put_copy_data(buffer, encoder)
435
503
  # It didn't flush immediately and allocation of more buffering memory failed.
436
504
  # Wait for all data sent by doing a blocking flush.
@@ -468,6 +536,25 @@ class PG::Connection
468
536
  end
469
537
  alias async_put_copy_end put_copy_end
470
538
 
539
+ if method_defined? :send_pipeline_sync
540
+ # call-seq:
541
+ # conn.pipeline_sync
542
+ #
543
+ # Marks a synchronization point in a pipeline by sending a sync message and flushing the send buffer.
544
+ # This serves as the delimiter of an implicit transaction and an error recovery point.
545
+ #
546
+ # See enter_pipeline_mode
547
+ #
548
+ # Raises PG::Error if the connection is not in pipeline mode or sending a sync message failed.
549
+ #
550
+ # Available since PostgreSQL-14
551
+ def pipeline_sync(*args)
552
+ send_pipeline_sync(*args)
553
+ flush
554
+ end
555
+ alias async_pipeline_sync pipeline_sync
556
+ end
557
+
471
558
  if method_defined? :sync_encrypt_password
472
559
  # call-seq:
473
560
  # conn.encrypt_password( password, username, algorithm=nil ) -> String
@@ -502,136 +589,210 @@ class PG::Connection
502
589
  # Resets the backend connection. This method closes the
503
590
  # backend connection and tries to re-connect.
504
591
  def reset
505
- reset_start
592
+ # Use connection options from PG::Connection.new to reconnect with the same options but with renewed DNS resolution.
593
+ # Use conninfo_hash as a fallback when connect_start was used to create the connection object.
594
+ iopts = @iopts_for_reset || conninfo_hash.compact
595
+ if iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
596
+ iopts = self.class.send(:resolve_hosts, iopts)
597
+ end
598
+ conninfo = self.class.parse_connect_args( iopts );
599
+ reset_start2(conninfo)
506
600
  async_connect_or_reset(:reset_poll)
507
601
  self
508
602
  end
509
603
  alias async_reset reset
510
604
 
511
- # call-seq:
512
- # conn.cancel() -> String
513
- #
514
- # Requests cancellation of the command currently being
515
- # processed.
516
- #
517
- # Returns +nil+ on success, or a string containing the
518
- # error message if a failure occurs.
519
- def cancel
520
- be_pid = backend_pid
521
- be_key = backend_key
522
- cancel_request = [0x10, 1234, 5678, be_pid, be_key].pack("NnnNN")
523
-
524
- if Fiber.respond_to?(:scheduler) && Fiber.scheduler && RUBY_PLATFORM =~ /mingw|mswin/
525
- # Ruby's nonblocking IO is not really supported on Windows.
526
- # We work around by using threads and explicit calls to wait_readable/wait_writable.
527
- cl = Thread.new(socket_io.remote_address) { |ra| ra.connect }.value
528
- begin
529
- cl.write_nonblock(cancel_request)
530
- rescue IO::WaitReadable, Errno::EINTR
531
- cl.wait_writable
532
- retry
533
- end
534
- begin
535
- cl.read_nonblock(1)
536
- rescue IO::WaitReadable, Errno::EINTR
537
- cl.wait_readable
538
- retry
539
- rescue EOFError
540
- end
541
- elsif RUBY_ENGINE == 'truffleruby'
542
- begin
543
- cl = socket_io.remote_address.connect
544
- rescue NotImplementedError
545
- # Workaround for truffleruby < 21.3.0
546
- cl2 = Socket.for_fd(socket_io.fileno)
547
- cl2.autoclose = false
548
- adr = cl2.remote_address
549
- if adr.ip?
550
- cl = TCPSocket.new(adr.ip_address, adr.ip_port)
551
- cl.autoclose = false
552
- else
553
- cl = UNIXSocket.new(adr.unix_path)
554
- cl.autoclose = false
605
+ if defined?(PG::CancelConnection)
606
+ # PostgreSQL-17+
607
+
608
+ def sync_cancel
609
+ cancon = PG::CancelConnection.new(self)
610
+ cancon.sync_cancel
611
+ rescue PG::Error => err
612
+ err.to_s
613
+ end
614
+
615
+ # call-seq:
616
+ # conn.cancel() -> String
617
+ #
618
+ # Requests cancellation of the command currently being
619
+ # processed.
620
+ #
621
+ # Returns +nil+ on success, or a string containing the
622
+ # error message if a failure occurs.
623
+ #
624
+ # On PostgreSQL-17+ client libaray the class PG::CancelConnection is used.
625
+ # On older client library a pure ruby implementation is used.
626
+ def cancel
627
+ cancon = PG::CancelConnection.new(self)
628
+ cancon.async_cancel
629
+ rescue PG::Error => err
630
+ err.to_s
631
+ end
632
+
633
+ else
634
+
635
+ # PostgreSQL < 17
636
+
637
+ def cancel
638
+ be_pid = backend_pid
639
+ be_key = backend_key
640
+ cancel_request = [0x10, 1234, 5678, be_pid, be_key].pack("NnnNN")
641
+
642
+ if Fiber.respond_to?(:scheduler) && Fiber.scheduler && RUBY_PLATFORM =~ /mingw|mswin/
643
+ # Ruby's nonblocking IO is not really supported on Windows.
644
+ # We work around by using threads and explicit calls to wait_readable/wait_writable.
645
+ cl = Thread.new(socket_io.remote_address) { |ra| ra.connect }.value
646
+ begin
647
+ cl.write_nonblock(cancel_request)
648
+ rescue IO::WaitReadable, Errno::EINTR
649
+ cl.wait_writable
650
+ retry
555
651
  end
652
+ begin
653
+ cl.read_nonblock(1)
654
+ rescue IO::WaitReadable, Errno::EINTR
655
+ cl.wait_readable
656
+ retry
657
+ rescue EOFError
658
+ end
659
+ else
660
+ cl = socket_io.remote_address.connect
661
+ # Send CANCEL_REQUEST_CODE and parameters
662
+ cl.write(cancel_request)
663
+ # Wait for the postmaster to close the connection, which indicates that it's processed the request.
664
+ cl.read(1)
556
665
  end
557
- cl.write(cancel_request)
558
- cl.read(1)
559
- else
560
- cl = socket_io.remote_address.connect
561
- # Send CANCEL_REQUEST_CODE and parameters
562
- cl.write(cancel_request)
563
- # Wait for the postmaster to close the connection, which indicates that it's processed the request.
564
- cl.read(1)
565
- end
566
666
 
567
- cl.close
568
- nil
569
- rescue SystemCallError => err
570
- err.to_s
667
+ cl.close
668
+ nil
669
+ rescue SystemCallError => err
670
+ err.to_s
671
+ end
571
672
  end
572
673
  alias async_cancel cancel
573
674
 
574
- private def async_connect_or_reset(poll_meth)
575
- # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
675
+ module Pollable
676
+ # Track the progress of the connection, waiting for the socket to become readable/writable before polling it.
677
+ #
678
+ # Connecting to multiple hosts is done like so:
679
+ # - All hosts are passed to PG::Connection.connect_start
680
+ # - As soon as the host is tried to connect the related host is removed from the hosts list
681
+ # - When the polling status changes to `PG::PGRES_POLLING_OK` the connection is returned and ready to use.
682
+ # - When the polling status changes to `PG::PGRES_POLLING_FAILED` connecting is aborted and a PG::ConnectionBad is raised with details to all connection attepts.
683
+ # - When a timeout occurs, connecting is restarted with the remaining hosts.
684
+ #
685
+ # The downside is that this connects only once to hosts which are listed twice when they timeout.
686
+ private def polling_loop(poll_meth)
687
+ connect_timeout = conninfo_hash[:connect_timeout]
688
+ if (timeo = connect_timeout.to_i) && timeo > 0
689
+ host_count = (conninfo_hash[:hostaddr].to_s.empty? ? conninfo_hash[:host] : conninfo_hash[:hostaddr]).to_s.count(",") + 1
690
+ stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
691
+ end
692
+ iopts = conninfo_hash.compact
693
+ connection_errors = []
694
+ poll_status = PG::PGRES_POLLING_WRITING
576
695
 
577
- if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
578
- # Lowest timeout is 2 seconds - like in libpq
579
- timeo = [timeo, 2].max
580
- host_count = conninfo_hash[:host].to_s.count(",") + 1
581
- stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
582
- end
696
+ until poll_status == PG::PGRES_POLLING_OK ||
697
+ poll_status == PG::PGRES_POLLING_FAILED
583
698
 
584
- poll_status = PG::PGRES_POLLING_WRITING
585
- until poll_status == PG::PGRES_POLLING_OK ||
586
- poll_status == PG::PGRES_POLLING_FAILED
587
-
588
- # Set single timeout to parameter "connect_timeout" but
589
- # don't exceed total connection time of number-of-hosts * connect_timeout.
590
- timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
591
- event = if !timeout || timeout >= 0
592
- # If the socket needs to read, wait 'til it becomes readable to poll again
593
- case poll_status
594
- when PG::PGRES_POLLING_READING
595
- if defined?(IO::READABLE) # ruby-3.0+
596
- socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
597
- else
598
- IO.select([socket_io], nil, [socket_io], timeout)
699
+ # Set single timeout to parameter "connect_timeout" but
700
+ # don't exceed total connection time of number-of-hosts * connect_timeout.
701
+ timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
702
+
703
+ hostcnt = remove_current_host(iopts)
704
+
705
+ event = if !timeout || timeout >= 0
706
+ # If the socket needs to read, wait 'til it becomes readable to poll again
707
+ case poll_status
708
+ when PG::PGRES_POLLING_READING
709
+ if defined?(IO::READABLE) # ruby-3.0+
710
+ socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
711
+ else
712
+ IO.select([socket_io], nil, [socket_io], timeout)
713
+ end
714
+
715
+ # ...and the same for when the socket needs to write
716
+ when PG::PGRES_POLLING_WRITING
717
+ if defined?(IO::WRITABLE) # ruby-3.0+
718
+ # Use wait instead of wait_readable, since connection errors are delivered as
719
+ # exceptional/priority events on Windows.
720
+ socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
721
+ else
722
+ # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
723
+ IO.select(nil, [socket_io], [socket_io], timeout)
724
+ end
599
725
  end
726
+ end
600
727
 
601
- # ...and the same for when the socket needs to write
602
- when PG::PGRES_POLLING_WRITING
603
- if defined?(IO::WRITABLE) # ruby-3.0+
604
- # Use wait instead of wait_readable, since connection errors are delivered as
605
- # exceptional/priority events on Windows.
606
- socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
728
+ # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
729
+ # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
730
+ unless event
731
+ connection_errors << (error_message + "timeout expired")
732
+ if hostcnt > 0
733
+ reset_start2(self.class.parse_connect_args(iopts))
734
+ # Restart polling with waiting for writable.
735
+ # Otherwise "not connected" error is raised on Windows.
736
+ poll_status = PG::PGRES_POLLING_WRITING
737
+ next
607
738
  else
608
- # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
609
- IO.select(nil, [socket_io], [socket_io], timeout)
739
+ finish
740
+ raise PG::ConnectionBad.new(connection_errors.join("\n").b, connection: self)
610
741
  end
611
742
  end
612
- end
613
- # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
614
- # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
615
- unless event
616
- if self.class.send(:host_is_named_pipe?, host)
617
- connhost = "on socket \"#{host}\""
618
- elsif respond_to?(:hostaddr)
619
- connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
620
- else
621
- connhost = "at \"#{host}\", port #{port}"
622
- end
623
- raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
743
+
744
+ # Check to see if it's finished or failed yet
745
+ poll_status = send( poll_meth )
624
746
  end
625
747
 
626
- # Check to see if it's finished or failed yet
627
- poll_status = send( poll_meth )
748
+ unless status == PG::CONNECTION_OK
749
+ msg = error_message
750
+ finish
751
+ raise PG::ConnectionBad.new(connection_errors.map{|e| e + "\n" }.join.b + msg, connection: self)
752
+ end
628
753
  end
629
754
 
630
- unless status == PG::CONNECTION_OK
631
- msg = error_message
632
- finish
633
- raise PG::ConnectionBad.new(msg, connection: self)
755
+ # Remove the host to which the connection is currently established from the option hash.
756
+ # Affected options are:
757
+ # - :host
758
+ # - :hostaddr
759
+ # - :port
760
+ #
761
+ # Return the number of remaining hosts.
762
+ private def remove_current_host(iopts)
763
+ ihosts = iopts[:host]&.split(",", -1)
764
+ ihostaddrs = iopts[:hostaddr]&.split(",", -1)
765
+ iports = iopts[:port]&.split(",", -1)
766
+ iports = iports * (ihosts || ihostaddrs || [1]).size if iports&.size == 1
767
+
768
+ idx = (ihosts || ihostaddrs || iports).index.with_index do |_, i|
769
+ (ihosts ? ihosts[i] == host : true) &&
770
+ (ihostaddrs && respond_to?(:hostaddr, true) ? ihostaddrs[i] == hostaddr : true) &&
771
+ (iports ? iports[i].to_i == port : true)
772
+ end
773
+
774
+ if idx
775
+ ihosts&.delete_at(idx)
776
+ ihostaddrs&.delete_at(idx)
777
+ iports&.delete_at(idx)
778
+
779
+ iopts.merge!(
780
+ host: ihosts.join(",")) if ihosts
781
+ iopts.merge!(
782
+ hostaddr: ihostaddrs.join(",")) if ihostaddrs
783
+ iopts.merge!(
784
+ port: iports.join(",")) if iports
785
+ end
786
+
787
+ (ihosts || ihostaddrs || iports).size
634
788
  end
789
+ end
790
+
791
+ include Pollable
792
+
793
+ private def async_connect_or_reset(poll_meth)
794
+ # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
795
+ polling_loop(poll_meth)
635
796
 
636
797
  # Set connection to nonblocking to handle all blocking states in ruby.
637
798
  # That way a fiber scheduler is able to handle IO requests.
@@ -647,7 +808,7 @@ class PG::Connection
647
808
  # PG::Connection.new(connection_string) -> conn
648
809
  # PG::Connection.new(host, port, options, tty, dbname, user, password) -> conn
649
810
  #
650
- # Create a connection to the specified server.
811
+ # === Create a connection to the specified server.
651
812
  #
652
813
  # +connection_hash+ must be a ruby Hash with connection parameters.
653
814
  # See the {list of valid parameters}[https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS] in the PostgreSQL documentation.
@@ -671,7 +832,13 @@ class PG::Connection
671
832
  # [+password+]
672
833
  # login password
673
834
  #
674
- # Examples:
835
+ #
836
+ # If the Ruby default internal encoding is set (i.e., <code>Encoding.default_internal != nil</code>), the
837
+ # connection will have its +client_encoding+ set accordingly.
838
+ #
839
+ # Raises a PG::Error if the connection fails.
840
+ #
841
+ # === Examples:
675
842
  #
676
843
  # # Connect using all defaults
677
844
  # PG::Connection.new
@@ -688,10 +855,18 @@ class PG::Connection
688
855
  # # As an URI
689
856
  # PG::Connection.new( "postgresql://user:pass@pgsql.example.com:5432/testdb?sslmode=require" )
690
857
  #
691
- # If the Ruby default internal encoding is set (i.e., <code>Encoding.default_internal != nil</code>), the
692
- # connection will have its +client_encoding+ set accordingly.
858
+ # === Specifying Multiple Hosts
859
+ #
860
+ # It is possible to specify multiple hosts to connect to, so that they are tried in the given order or optionally in random order.
861
+ # In the Keyword/Value format, the host, hostaddr, and port options accept comma-separated lists of values.
862
+ # The {details to libpq}[https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS] describe how it works, but there are two small differences how ruby-pg handles multiple hosts:
863
+ # - All hosts are resolved before the first connection is tried.
864
+ # This means that when +load_balance_hosts+ is set to +random+, then all resolved addresses are tried randomly in one level.
865
+ # When a host resolves to more than one address, it is therefore tried more often than a host that has only one address.
866
+ # - When a timeout occurs due to the value of +connect_timeout+, then the given +host+, +hostaddr+ and +port+ combination is not tried a second time, even if it's specified several times.
867
+ # It's still possible to do load balancing with +load_balance_hosts+ set to +random+ and to increase the number of connections a node gets, when the hostname is provided multiple times in the host string.
868
+ # This is because in non-timeout cases the host is tried multiple times.
693
869
  #
694
- # Raises a PG::Error if the connection fails.
695
870
  def new(*args)
696
871
  conn = connect_to_hosts(*args)
697
872
 
@@ -710,46 +885,59 @@ class PG::Connection
710
885
  alias setdb new
711
886
  alias setdblogin new
712
887
 
888
+ # Resolve DNS in Ruby to avoid blocking state while connecting.
889
+ # Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
890
+ # This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
891
+ private def resolve_hosts(iopts)
892
+ ihosts = iopts[:host].split(",", -1)
893
+ iports = iopts[:port].split(",", -1)
894
+ iports = [nil] if iports.size == 0
895
+ iports = iports * ihosts.size if iports.size == 1
896
+ raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
897
+
898
+ dests = ihosts.each_with_index.flat_map do |mhost, idx|
899
+ unless host_is_named_pipe?(mhost)
900
+ if Fiber.respond_to?(:scheduler) &&
901
+ Fiber.scheduler &&
902
+ RUBY_VERSION < '3.1.'
903
+
904
+ # Use a second thread to avoid blocking of the scheduler.
905
+ # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
906
+ hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
907
+ else
908
+ hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
909
+ end
910
+ else
911
+ # No hostname to resolve (UnixSocket)
912
+ hostaddrs = [nil]
913
+ end
914
+ hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
915
+ end
916
+ iopts.merge(
917
+ hostaddr: dests.map{|d| d[0] }.join(","),
918
+ host: dests.map{|d| d[1] }.join(","),
919
+ port: dests.map{|d| d[2] }.join(","))
920
+ end
921
+
713
922
  private def connect_to_hosts(*args)
714
923
  option_string = parse_connect_args(*args)
715
924
  iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
716
925
  iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
717
926
 
927
+ if PG::BUNDLED_LIBPQ_WITH_UNIXSOCKET && iopts[:host].to_s.empty? && iopts[:hostaddr].to_s.empty?
928
+ # Many distors patch the hardcoded default UnixSocket path in libpq to /var/run/postgresql instead of /tmp .
929
+ # We simply try them all.
930
+ iopts[:host] = "/var/run/postgresql" + # Ubuntu, Debian, Fedora, Opensuse
931
+ ",/run/postgresql" + # Alpine, Archlinux, Gentoo
932
+ ",/tmp" # Stock PostgreSQL
933
+ end
934
+
935
+ iopts_for_reset = iopts
718
936
  if iopts[:hostaddr]
719
937
  # hostaddr is provided -> no need to resolve hostnames
720
938
 
721
939
  elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
722
- # Resolve DNS in Ruby to avoid blocking state while connecting.
723
- # Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
724
- # This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
725
- ihosts = iopts[:host].split(",", -1)
726
- iports = iopts[:port].split(",", -1)
727
- iports = [nil] if iports.size == 0
728
- iports = iports * ihosts.size if iports.size == 1
729
- raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
730
-
731
- dests = ihosts.each_with_index.flat_map do |mhost, idx|
732
- unless host_is_named_pipe?(mhost)
733
- if Fiber.respond_to?(:scheduler) &&
734
- Fiber.scheduler &&
735
- RUBY_VERSION < '3.1.'
736
-
737
- # Use a second thread to avoid blocking of the scheduler.
738
- # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
739
- hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
740
- else
741
- hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
742
- end
743
- else
744
- # No hostname to resolve (UnixSocket)
745
- hostaddrs = [nil]
746
- end
747
- hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
748
- end
749
- iopts.merge!(
750
- hostaddr: dests.map{|d| d[0] }.join(","),
751
- host: dests.map{|d| d[1] }.join(","),
752
- port: dests.map{|d| d[2] }.join(","))
940
+ iopts = resolve_hosts(iopts)
753
941
  else
754
942
  # No host given
755
943
  end
@@ -758,6 +946,8 @@ class PG::Connection
758
946
 
759
947
  raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
760
948
 
949
+ # save the connection options for conn.reset
950
+ conn.instance_variable_set(:@iopts_for_reset, iopts_for_reset)
761
951
  conn.send(:async_connect_or_reset, :connect_poll)
762
952
  conn
763
953
  end
@@ -774,7 +964,10 @@ class PG::Connection
774
964
  # PG::Connection.ping(connection_string) -> Integer
775
965
  # PG::Connection.ping(host, port, options, tty, dbname, login, password) -> Integer
776
966
  #
777
- # Check server status.
967
+ # PQpingParams reports the status of the server.
968
+ #
969
+ # It accepts connection parameters identical to those of PQ::Connection.new .
970
+ # It is not necessary to supply correct user name, password, or database name values to obtain the server status; however, if incorrect values are provided, the server will log a failed connection attempt.
778
971
  #
779
972
  # See PG::Connection.new for a description of the parameters.
780
973
  #
@@ -787,6 +980,8 @@ class PG::Connection
787
980
  # could not establish connection
788
981
  # [+PQPING_NO_ATTEMPT+]
789
982
  # connection not attempted (bad params)
983
+ #
984
+ # See also check_socket for a way to check the connection without doing any server communication.
790
985
  def ping(*args)
791
986
  if Fiber.respond_to?(:scheduler) && Fiber.scheduler
792
987
  # Run PQping in a second thread to avoid blocking of the scheduler.
@@ -798,14 +993,15 @@ class PG::Connection
798
993
  end
799
994
  alias async_ping ping
800
995
 
801
- REDIRECT_CLASS_METHODS = {
996
+ REDIRECT_CLASS_METHODS = PG.make_shareable({
802
997
  :new => [:async_connect, :sync_connect],
803
998
  :connect => [:async_connect, :sync_connect],
804
999
  :open => [:async_connect, :sync_connect],
805
1000
  :setdb => [:async_connect, :sync_connect],
806
1001
  :setdblogin => [:async_connect, :sync_connect],
807
1002
  :ping => [:async_ping, :sync_ping],
808
- }
1003
+ })
1004
+ private_constant :REDIRECT_CLASS_METHODS
809
1005
 
810
1006
  # These methods are affected by PQsetnonblocking
811
1007
  REDIRECT_SEND_METHODS = {
@@ -815,6 +1011,22 @@ class PG::Connection
815
1011
  :put_copy_end => [:async_put_copy_end, :sync_put_copy_end],
816
1012
  :flush => [:async_flush, :sync_flush],
817
1013
  }
1014
+ private_constant :REDIRECT_SEND_METHODS
1015
+ if PG::Connection.instance_methods.include? :sync_pipeline_sync
1016
+ if PG::Connection.instance_methods.include? :send_pipeline_sync
1017
+ # PostgreSQL-17+
1018
+ REDIRECT_SEND_METHODS.merge!({
1019
+ :pipeline_sync => [:async_pipeline_sync, :sync_pipeline_sync],
1020
+ })
1021
+ else
1022
+ # PostgreSQL-14+
1023
+ REDIRECT_SEND_METHODS.merge!({
1024
+ :pipeline_sync => [:sync_pipeline_sync, :sync_pipeline_sync],
1025
+ })
1026
+ end
1027
+ end
1028
+ PG.make_shareable(REDIRECT_SEND_METHODS)
1029
+
818
1030
  REDIRECT_METHODS = {
819
1031
  :exec => [:async_exec, :sync_exec],
820
1032
  :query => [:async_exec, :sync_exec],
@@ -831,13 +1043,16 @@ class PG::Connection
831
1043
  :set_client_encoding => [:async_set_client_encoding, :sync_set_client_encoding],
832
1044
  :client_encoding= => [:async_set_client_encoding, :sync_set_client_encoding],
833
1045
  :cancel => [:async_cancel, :sync_cancel],
1046
+ :encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
834
1047
  }
835
-
836
- if PG::Connection.instance_methods.include? :async_encrypt_password
1048
+ private_constant :REDIRECT_METHODS
1049
+ if PG::Connection.instance_methods.include? :async_close_prepared
837
1050
  REDIRECT_METHODS.merge!({
838
- :encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
1051
+ :close_prepared => [:async_close_prepared, :sync_close_prepared],
1052
+ :close_portal => [:async_close_portal, :sync_close_portal],
839
1053
  })
840
1054
  end
1055
+ PG.make_shareable(REDIRECT_METHODS)
841
1056
 
842
1057
  def async_send_api=(enable)
843
1058
  REDIRECT_SEND_METHODS.each do |ali, (async, sync)|