pg 1.5.4 → 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 (78) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/{History.md → CHANGELOG.md} +106 -4
  4. data/Gemfile +12 -3
  5. data/README-Windows.rdoc +1 -1
  6. data/README.ja.md +4 -4
  7. data/README.md +58 -17
  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 +161 -14
  14. data/ext/gvl_wrappers.c +13 -2
  15. data/ext/gvl_wrappers.h +33 -0
  16. data/ext/pg.c +17 -6
  17. data/ext/pg.h +9 -9
  18. data/ext/pg_binary_decoder.c +152 -0
  19. data/ext/pg_binary_encoder.c +211 -8
  20. data/ext/pg_cancel_connection.c +360 -0
  21. data/ext/pg_coder.c +54 -5
  22. data/ext/pg_connection.c +409 -167
  23. data/ext/pg_copy_coder.c +19 -15
  24. data/ext/pg_record_coder.c +7 -7
  25. data/ext/pg_result.c +11 -13
  26. data/ext/pg_text_decoder.c +4 -1
  27. data/ext/pg_text_encoder.c +37 -18
  28. data/ext/pg_tuple.c +2 -2
  29. data/ext/pg_type_map.c +1 -1
  30. data/ext/pg_type_map_all_strings.c +1 -1
  31. data/ext/pg_type_map_by_class.c +1 -1
  32. data/ext/pg_type_map_by_column.c +2 -1
  33. data/ext/pg_type_map_by_mri_type.c +1 -1
  34. data/ext/pg_type_map_by_oid.c +3 -1
  35. data/ext/pg_type_map_in_ruby.c +1 -1
  36. data/lib/pg/basic_type_map_for_queries.rb +15 -7
  37. data/lib/pg/basic_type_registry.rb +16 -4
  38. data/lib/pg/cancel_connection.rb +53 -0
  39. data/lib/pg/coder.rb +4 -2
  40. data/lib/pg/connection.rb +310 -167
  41. data/lib/pg/exceptions.rb +6 -0
  42. data/lib/pg/text_decoder/date.rb +3 -0
  43. data/lib/pg/text_decoder/json.rb +3 -0
  44. data/lib/pg/text_encoder/date.rb +1 -0
  45. data/lib/pg/text_encoder/inet.rb +3 -0
  46. data/lib/pg/text_encoder/json.rb +3 -0
  47. data/lib/pg/version.rb +1 -1
  48. data/lib/pg.rb +23 -8
  49. data/misc/yugabyte/Dockerfile +9 -0
  50. data/misc/yugabyte/docker-compose.yml +28 -0
  51. data/misc/yugabyte/pg-test.rb +45 -0
  52. data/pg.gemspec +8 -4
  53. data/ports/patches/krb5/1.21.3/0001-Allow-static-linking-krb5-library.patch +30 -0
  54. data/ports/patches/openssl/3.5.1/0001-aarch64-mingw.patch +21 -0
  55. data/ports/patches/postgresql/17.5/0001-Use-workaround-of-__builtin_setjmp-only-on-MINGW-on-.patch +42 -0
  56. data/ports/patches/postgresql/17.5/0001-libpq-Process-buffered-SSL-read-bytes-to-support-rec.patch +52 -0
  57. data/rakelib/pg_gem_helper.rb +64 -0
  58. data.tar.gz.sig +0 -0
  59. metadata +45 -47
  60. metadata.gz.sig +0 -0
  61. data/.appveyor.yml +0 -42
  62. data/.gems +0 -6
  63. data/.gemtest +0 -0
  64. data/.github/workflows/binary-gems.yml +0 -117
  65. data/.github/workflows/source-gem.yml +0 -141
  66. data/.gitignore +0 -22
  67. data/.hgsigs +0 -34
  68. data/.hgtags +0 -41
  69. data/.irbrc +0 -23
  70. data/.pryrc +0 -23
  71. data/.tm_properties +0 -21
  72. data/.travis.yml +0 -49
  73. data/Manifest.txt +0 -72
  74. data/Rakefile.cross +0 -298
  75. data/translation/.po4a-version +0 -7
  76. data/translation/po/all.pot +0 -936
  77. data/translation/po/ja.po +0 -1036
  78. data/translation/po4a.cfg +0 -12
data/lib/pg/connection.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'pg' unless defined?( PG )
5
- require 'io/wait' unless ::IO.public_instance_methods(false).include?(:wait_readable)
5
+ require 'io/wait' unless ::IO.public_instance_methods(false).include?(:wait_readable) # for ruby < 3.0
6
6
  require 'socket'
7
7
 
8
8
  # The PostgreSQL connection class. The interface for this class is based on
@@ -117,7 +117,7 @@ class PG::Connection
117
117
  return str
118
118
  end
119
119
 
120
- BinarySignature = "PGCOPY\n\377\r\n\0".b
120
+ BinarySignature = "PGCOPY\n\377\r\n\0"
121
121
  private_constant :BinarySignature
122
122
 
123
123
  # call-seq:
@@ -166,7 +166,10 @@ class PG::Connection
166
166
  # conn.put_copy_data ['more', 'data', 'to', 'copy']
167
167
  # end
168
168
  #
169
- # Also PG::BinaryEncoder::CopyRow can be used to send data in binary format to the server.
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.
170
173
  # In this case copy_data generates the header and trailer data automatically:
171
174
  # enco = PG::BinaryEncoder::CopyRow.new
172
175
  # conn.copy_data "COPY my_table FROM STDIN (FORMAT binary)", enco do
@@ -306,6 +309,11 @@ class PG::Connection
306
309
  rollback = false
307
310
  exec "BEGIN"
308
311
  yield(self)
312
+ rescue PG::RollbackTransaction
313
+ rollback = true
314
+ cancel if transaction_status == PG::PQTRANS_ACTIVE
315
+ block
316
+ exec "ROLLBACK"
309
317
  rescue Exception
310
318
  rollback = true
311
319
  cancel if transaction_status == PG::PQTRANS_ACTIVE
@@ -348,21 +356,18 @@ class PG::Connection
348
356
  end
349
357
  end
350
358
 
351
- # Method 'ssl_attribute' was introduced in PostgreSQL 9.5.
352
- if self.instance_methods.find{|m| m.to_sym == :ssl_attribute }
353
- # call-seq:
354
- # conn.ssl_attributes -> Hash<String,String>
355
- #
356
- # Returns SSL-related information about the connection as key/value pairs
357
- #
358
- # The available attributes varies depending on the SSL library being used,
359
- # and the type of connection.
360
- #
361
- # See also #ssl_attribute
362
- def ssl_attributes
363
- ssl_attribute_names.each.with_object({}) do |n,h|
364
- h[n] = ssl_attribute(n)
365
- 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)
366
371
  end
367
372
  end
368
373
 
@@ -493,7 +498,7 @@ class PG::Connection
493
498
  # See also #copy_data.
494
499
  #
495
500
  def put_copy_data(buffer, encoder=nil)
496
- # 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.
497
502
  until res=sync_put_copy_data(buffer, encoder)
498
503
  # It didn't flush immediately and allocation of more buffering memory failed.
499
504
  # Wait for all data sent by doing a blocking flush.
@@ -531,6 +536,25 @@ class PG::Connection
531
536
  end
532
537
  alias async_put_copy_end put_copy_end
533
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
+
534
558
  if method_defined? :sync_encrypt_password
535
559
  # call-seq:
536
560
  # conn.encrypt_password( password, username, algorithm=nil ) -> String
@@ -565,136 +589,210 @@ class PG::Connection
565
589
  # Resets the backend connection. This method closes the
566
590
  # backend connection and tries to re-connect.
567
591
  def reset
568
- 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)
569
600
  async_connect_or_reset(:reset_poll)
570
601
  self
571
602
  end
572
603
  alias async_reset reset
573
604
 
574
- # call-seq:
575
- # conn.cancel() -> String
576
- #
577
- # Requests cancellation of the command currently being
578
- # processed.
579
- #
580
- # Returns +nil+ on success, or a string containing the
581
- # error message if a failure occurs.
582
- def cancel
583
- be_pid = backend_pid
584
- be_key = backend_key
585
- cancel_request = [0x10, 1234, 5678, be_pid, be_key].pack("NnnNN")
586
-
587
- if Fiber.respond_to?(:scheduler) && Fiber.scheduler && RUBY_PLATFORM =~ /mingw|mswin/
588
- # Ruby's nonblocking IO is not really supported on Windows.
589
- # We work around by using threads and explicit calls to wait_readable/wait_writable.
590
- cl = Thread.new(socket_io.remote_address) { |ra| ra.connect }.value
591
- begin
592
- cl.write_nonblock(cancel_request)
593
- rescue IO::WaitReadable, Errno::EINTR
594
- cl.wait_writable
595
- retry
596
- end
597
- begin
598
- cl.read_nonblock(1)
599
- rescue IO::WaitReadable, Errno::EINTR
600
- cl.wait_readable
601
- retry
602
- rescue EOFError
603
- end
604
- elsif RUBY_ENGINE == 'truffleruby'
605
- begin
606
- cl = socket_io.remote_address.connect
607
- rescue NotImplementedError
608
- # Workaround for truffleruby < 21.3.0
609
- cl2 = Socket.for_fd(socket_io.fileno)
610
- cl2.autoclose = false
611
- adr = cl2.remote_address
612
- if adr.ip?
613
- cl = TCPSocket.new(adr.ip_address, adr.ip_port)
614
- cl.autoclose = false
615
- else
616
- cl = UNIXSocket.new(adr.unix_path)
617
- 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
618
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)
619
665
  end
620
- cl.write(cancel_request)
621
- cl.read(1)
622
- else
623
- cl = socket_io.remote_address.connect
624
- # Send CANCEL_REQUEST_CODE and parameters
625
- cl.write(cancel_request)
626
- # Wait for the postmaster to close the connection, which indicates that it's processed the request.
627
- cl.read(1)
628
- end
629
666
 
630
- cl.close
631
- nil
632
- rescue SystemCallError => err
633
- err.to_s
667
+ cl.close
668
+ nil
669
+ rescue SystemCallError => err
670
+ err.to_s
671
+ end
634
672
  end
635
673
  alias async_cancel cancel
636
674
 
637
- private def async_connect_or_reset(poll_meth)
638
- # 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
639
695
 
640
- if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
641
- # Lowest timeout is 2 seconds - like in libpq
642
- timeo = [timeo, 2].max
643
- host_count = conninfo_hash[:host].to_s.count(",") + 1
644
- stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
645
- end
696
+ until poll_status == PG::PGRES_POLLING_OK ||
697
+ poll_status == PG::PGRES_POLLING_FAILED
646
698
 
647
- poll_status = PG::PGRES_POLLING_WRITING
648
- until poll_status == PG::PGRES_POLLING_OK ||
649
- poll_status == PG::PGRES_POLLING_FAILED
650
-
651
- # Set single timeout to parameter "connect_timeout" but
652
- # don't exceed total connection time of number-of-hosts * connect_timeout.
653
- timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
654
- event = if !timeout || timeout >= 0
655
- # If the socket needs to read, wait 'til it becomes readable to poll again
656
- case poll_status
657
- when PG::PGRES_POLLING_READING
658
- if defined?(IO::READABLE) # ruby-3.0+
659
- socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
660
- else
661
- 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
662
725
  end
726
+ end
663
727
 
664
- # ...and the same for when the socket needs to write
665
- when PG::PGRES_POLLING_WRITING
666
- if defined?(IO::WRITABLE) # ruby-3.0+
667
- # Use wait instead of wait_readable, since connection errors are delivered as
668
- # exceptional/priority events on Windows.
669
- 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
670
738
  else
671
- # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
672
- IO.select(nil, [socket_io], [socket_io], timeout)
739
+ finish
740
+ raise PG::ConnectionBad.new(connection_errors.join("\n").b, connection: self)
673
741
  end
674
742
  end
675
- end
676
- # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
677
- # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
678
- unless event
679
- if self.class.send(:host_is_named_pipe?, host)
680
- connhost = "on socket \"#{host}\""
681
- elsif respond_to?(:hostaddr)
682
- connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
683
- else
684
- connhost = "at \"#{host}\", port #{port}"
685
- end
686
- 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 )
687
746
  end
688
747
 
689
- # Check to see if it's finished or failed yet
690
- 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
691
753
  end
692
754
 
693
- unless status == PG::CONNECTION_OK
694
- msg = error_message
695
- finish
696
- 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
697
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)
698
796
 
699
797
  # Set connection to nonblocking to handle all blocking states in ruby.
700
798
  # That way a fiber scheduler is able to handle IO requests.
@@ -710,7 +808,7 @@ class PG::Connection
710
808
  # PG::Connection.new(connection_string) -> conn
711
809
  # PG::Connection.new(host, port, options, tty, dbname, user, password) -> conn
712
810
  #
713
- # Create a connection to the specified server.
811
+ # === Create a connection to the specified server.
714
812
  #
715
813
  # +connection_hash+ must be a ruby Hash with connection parameters.
716
814
  # See the {list of valid parameters}[https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS] in the PostgreSQL documentation.
@@ -734,7 +832,13 @@ class PG::Connection
734
832
  # [+password+]
735
833
  # login password
736
834
  #
737
- # 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:
738
842
  #
739
843
  # # Connect using all defaults
740
844
  # PG::Connection.new
@@ -751,10 +855,18 @@ class PG::Connection
751
855
  # # As an URI
752
856
  # PG::Connection.new( "postgresql://user:pass@pgsql.example.com:5432/testdb?sslmode=require" )
753
857
  #
754
- # If the Ruby default internal encoding is set (i.e., <code>Encoding.default_internal != nil</code>), the
755
- # 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.
756
869
  #
757
- # Raises a PG::Error if the connection fails.
758
870
  def new(*args)
759
871
  conn = connect_to_hosts(*args)
760
872
 
@@ -773,46 +885,59 @@ class PG::Connection
773
885
  alias setdb new
774
886
  alias setdblogin new
775
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
+
776
922
  private def connect_to_hosts(*args)
777
923
  option_string = parse_connect_args(*args)
778
924
  iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
779
925
  iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
780
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
781
936
  if iopts[:hostaddr]
782
937
  # hostaddr is provided -> no need to resolve hostnames
783
938
 
784
939
  elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
785
- # Resolve DNS in Ruby to avoid blocking state while connecting.
786
- # Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
787
- # This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
788
- ihosts = iopts[:host].split(",", -1)
789
- iports = iopts[:port].split(",", -1)
790
- iports = [nil] if iports.size == 0
791
- iports = iports * ihosts.size if iports.size == 1
792
- raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
793
-
794
- dests = ihosts.each_with_index.flat_map do |mhost, idx|
795
- unless host_is_named_pipe?(mhost)
796
- if Fiber.respond_to?(:scheduler) &&
797
- Fiber.scheduler &&
798
- RUBY_VERSION < '3.1.'
799
-
800
- # Use a second thread to avoid blocking of the scheduler.
801
- # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
802
- hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
803
- else
804
- hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
805
- end
806
- else
807
- # No hostname to resolve (UnixSocket)
808
- hostaddrs = [nil]
809
- end
810
- hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
811
- end
812
- iopts.merge!(
813
- hostaddr: dests.map{|d| d[0] }.join(","),
814
- host: dests.map{|d| d[1] }.join(","),
815
- port: dests.map{|d| d[2] }.join(","))
940
+ iopts = resolve_hosts(iopts)
816
941
  else
817
942
  # No host given
818
943
  end
@@ -821,6 +946,8 @@ class PG::Connection
821
946
 
822
947
  raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
823
948
 
949
+ # save the connection options for conn.reset
950
+ conn.instance_variable_set(:@iopts_for_reset, iopts_for_reset)
824
951
  conn.send(:async_connect_or_reset, :connect_poll)
825
952
  conn
826
953
  end
@@ -877,14 +1004,29 @@ class PG::Connection
877
1004
  private_constant :REDIRECT_CLASS_METHODS
878
1005
 
879
1006
  # These methods are affected by PQsetnonblocking
880
- REDIRECT_SEND_METHODS = PG.make_shareable({
1007
+ REDIRECT_SEND_METHODS = {
881
1008
  :isnonblocking => [:async_isnonblocking, :sync_isnonblocking],
882
1009
  :nonblocking? => [:async_isnonblocking, :sync_isnonblocking],
883
1010
  :put_copy_data => [:async_put_copy_data, :sync_put_copy_data],
884
1011
  :put_copy_end => [:async_put_copy_end, :sync_put_copy_end],
885
1012
  :flush => [:async_flush, :sync_flush],
886
- })
1013
+ }
887
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
+
888
1030
  REDIRECT_METHODS = {
889
1031
  :exec => [:async_exec, :sync_exec],
890
1032
  :query => [:async_exec, :sync_exec],
@@ -901,12 +1043,13 @@ class PG::Connection
901
1043
  :set_client_encoding => [:async_set_client_encoding, :sync_set_client_encoding],
902
1044
  :client_encoding= => [:async_set_client_encoding, :sync_set_client_encoding],
903
1045
  :cancel => [:async_cancel, :sync_cancel],
1046
+ :encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
904
1047
  }
905
1048
  private_constant :REDIRECT_METHODS
906
-
907
- if PG::Connection.instance_methods.include? :async_encrypt_password
1049
+ if PG::Connection.instance_methods.include? :async_close_prepared
908
1050
  REDIRECT_METHODS.merge!({
909
- :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],
910
1053
  })
911
1054
  end
912
1055
  PG.make_shareable(REDIRECT_METHODS)
data/lib/pg/exceptions.rb CHANGED
@@ -21,5 +21,11 @@ module PG
21
21
  class NotInBlockingMode < PG::Error
22
22
  end
23
23
 
24
+ # PG::Connection#transaction uses this exception to distinguish a deliberate rollback from other exceptional situations.
25
+ # Normally, raising an exception will cause the .transaction method to rollback the database transaction and pass on the exception.
26
+ # But if you raise an PG::RollbackTransaction exception, then the database transaction will be rolled back, without passing on the exception.
27
+ class RollbackTransaction < StandardError
28
+ end
29
+
24
30
  end # module PG
25
31
 
@@ -5,6 +5,9 @@ require 'date'
5
5
 
6
6
  module PG
7
7
  module TextDecoder
8
+ # This is a decoder class for conversion of PostgreSQL date type to Ruby Date values.
9
+ #
10
+ # As soon as this class is used, it requires the ruby standard library 'date'.
8
11
  class Date < SimpleDecoder
9
12
  def decode(string, tuple=nil, field=nil)
10
13
  if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/
@@ -5,6 +5,9 @@ require 'json'
5
5
 
6
6
  module PG
7
7
  module TextDecoder
8
+ # This is a decoder class for conversion of PostgreSQL JSON/JSONB type to Ruby Hash, Array, String, Numeric, nil values.
9
+ #
10
+ # As soon as this class is used, it requires the ruby standard library 'json'.
8
11
  class JSON < SimpleDecoder
9
12
  def decode(string, tuple=nil, field=nil)
10
13
  ::JSON.parse(string, quirks_mode: true)
@@ -3,6 +3,7 @@
3
3
 
4
4
  module PG
5
5
  module TextEncoder
6
+ # This is a encoder class for conversion of Ruby Date values to PostgreSQL date type.
6
7
  class Date < SimpleEncoder
7
8
  def encode(value)
8
9
  value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value