pg 1.5.9 → 1.7.0

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 (67) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/{History.md → CHANGELOG.md} +126 -0
  4. data/Gemfile +11 -7
  5. data/README-Windows.rdoc +1 -1
  6. data/README.ja.md +83 -32
  7. data/README.md +79 -29
  8. data/Rakefile +84 -19
  9. data/certs/larskanis-2026.pem +24 -0
  10. data/ext/errorcodes.def +9 -0
  11. data/ext/errorcodes.rb +1 -1
  12. data/ext/errorcodes.txt +8 -1
  13. data/ext/extconf.rb +194 -30
  14. data/ext/gvl_wrappers.c +13 -2
  15. data/ext/gvl_wrappers.h +47 -134
  16. data/ext/pg.c +23 -16
  17. data/ext/pg.h +23 -24
  18. data/ext/pg_binary_decoder.c +157 -3
  19. data/ext/pg_binary_encoder.c +219 -9
  20. data/ext/pg_cancel_connection.c +360 -0
  21. data/ext/pg_coder.c +58 -9
  22. data/ext/pg_connection.c +558 -291
  23. data/ext/pg_copy_coder.c +36 -23
  24. data/ext/pg_record_coder.c +11 -7
  25. data/ext/pg_result.c +144 -76
  26. data/ext/pg_text_decoder.c +7 -7
  27. data/ext/pg_text_encoder.c +29 -11
  28. data/ext/pg_tuple.c +9 -9
  29. data/ext/pg_type_map.c +45 -3
  30. data/ext/pg_type_map_all_strings.c +3 -3
  31. data/ext/pg_type_map_by_class.c +2 -2
  32. data/ext/pg_type_map_by_column.c +4 -3
  33. data/ext/pg_type_map_by_mri_type.c +1 -1
  34. data/ext/pg_type_map_by_oid.c +4 -2
  35. data/ext/pg_type_map_in_ruby.c +2 -2
  36. data/ext/pg_util.c +2 -2
  37. data/ext/pg_util.h +2 -2
  38. data/lib/pg/basic_type_map_based_on_result.rb +5 -0
  39. data/lib/pg/basic_type_map_for_queries.rb +16 -5
  40. data/lib/pg/basic_type_map_for_results.rb +5 -0
  41. data/lib/pg/basic_type_registry.rb +2 -2
  42. data/lib/pg/cancel_connection.rb +53 -0
  43. data/lib/pg/coder.rb +4 -2
  44. data/lib/pg/connection.rb +393 -180
  45. data/lib/pg/text_decoder/date.rb +1 -1
  46. data/lib/pg/text_decoder/json.rb +1 -1
  47. data/lib/pg/text_encoder/date.rb +7 -1
  48. data/lib/pg/text_encoder/json.rb +1 -1
  49. data/lib/pg/version.rb +2 -1
  50. data/lib/pg.rb +156 -130
  51. data/misc/glibc/Dockerfile +20 -0
  52. data/misc/glibc/docker-compose.yml +9 -0
  53. data/misc/glibc/glibc_spec.rb +5 -0
  54. data/misc/yugabyte/Dockerfile +9 -0
  55. data/misc/yugabyte/docker-compose.yml +28 -0
  56. data/misc/yugabyte/pg-test.rb +45 -0
  57. data/pg.gemspec +5 -3
  58. data/ports/patches/krb5/1.22.2/0001-Allow-static-linking-krb5-library.patch +30 -0
  59. data/ports/patches/krb5/1.22.2/0002-unknown-command-line-option-on-clang.patch +12 -0
  60. data/ports/patches/postgresql/18.6/0001-Use-workaround-of-__builtin_setjmp-only-on-MINGW-on-.patch +42 -0
  61. data/rakelib/pg_gem_helper.rb +64 -0
  62. data/sample/async_mixed.rb +1 -1
  63. data.tar.gz.sig +0 -0
  64. metadata +36 -22
  65. metadata.gz.sig +0 -0
  66. data/Manifest.txt +0 -72
  67. data/Rakefile.cross +0 -303
data/lib/pg/connection.rb CHANGED
@@ -2,7 +2,6 @@
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) # for ruby < 3.0
6
5
  require 'socket'
7
6
 
8
7
  # The PostgreSQL connection class. The interface for this class is based on
@@ -13,7 +12,7 @@ require 'socket'
13
12
  # For example, to send query to the database on the localhost:
14
13
  #
15
14
  # require 'pg'
16
- # conn = PG::Connection.open(:dbname => 'test')
15
+ # conn = PG::Connection.open(dbname: 'test')
17
16
  # res = conn.exec_params('SELECT $1 AS a, $2 AS b, $3 AS c', [1, 2, nil])
18
17
  # # Equivalent to:
19
18
  # # res = conn.exec('SELECT 1 AS a, 2 AS b, NULL AS c')
@@ -306,22 +305,19 @@ class PG::Connection
306
305
  # and a +COMMIT+ at the end of the block, or
307
306
  # +ROLLBACK+ if any exception occurs.
308
307
  def transaction
309
- rollback = false
310
308
  exec "BEGIN"
311
309
  yield(self)
312
- rescue PG::RollbackTransaction
313
- rollback = true
314
- cancel if transaction_status == PG::PQTRANS_ACTIVE
315
- block
316
- exec "ROLLBACK"
317
- rescue Exception
318
- rollback = true
319
- cancel if transaction_status == PG::PQTRANS_ACTIVE
320
- block
321
- exec "ROLLBACK"
310
+ rescue PG::RollbackTransaction => error
311
+ rescue Exception => error
322
312
  raise
323
313
  ensure
324
- exec "COMMIT" unless rollback
314
+ if error || Thread.current.status == "aborting"
315
+ cancel if transaction_status == PG::PQTRANS_ACTIVE
316
+ block
317
+ exec("ROLLBACK")
318
+ else
319
+ exec("COMMIT")
320
+ end
325
321
  end
326
322
 
327
323
  ### Returns an array of Hashes with connection defaults. See ::conndefaults
@@ -356,21 +352,18 @@ class PG::Connection
356
352
  end
357
353
  end
358
354
 
359
- # Method 'ssl_attribute' was introduced in PostgreSQL 9.5.
360
- if self.instance_methods.find{|m| m.to_sym == :ssl_attribute }
361
- # call-seq:
362
- # conn.ssl_attributes -> Hash<String,String>
363
- #
364
- # Returns SSL-related information about the connection as key/value pairs
365
- #
366
- # The available attributes varies depending on the SSL library being used,
367
- # and the type of connection.
368
- #
369
- # See also #ssl_attribute
370
- def ssl_attributes
371
- ssl_attribute_names.each.with_object({}) do |n,h|
372
- h[n] = ssl_attribute(n)
373
- end
355
+ # call-seq:
356
+ # conn.ssl_attributes -> Hash<String,String>
357
+ #
358
+ # Returns SSL-related information about the connection as key/value pairs
359
+ #
360
+ # The available attributes varies depending on the SSL library being used,
361
+ # and the type of connection.
362
+ #
363
+ # See also #ssl_attribute
364
+ def ssl_attributes
365
+ ssl_attribute_names.each.with_object({}) do |n,h|
366
+ h[n] = ssl_attribute(n)
374
367
  end
375
368
  end
376
369
 
@@ -539,34 +532,50 @@ class PG::Connection
539
532
  end
540
533
  alias async_put_copy_end put_copy_end
541
534
 
542
- if method_defined? :sync_encrypt_password
535
+ if method_defined? :send_pipeline_sync
543
536
  # call-seq:
544
- # conn.encrypt_password( password, username, algorithm=nil ) -> String
545
- #
546
- # This function is intended to be used by client applications that wish to send commands like <tt>ALTER USER joe PASSWORD 'pwd'</tt>.
547
- # It is good practice not to send the original cleartext password in such a command, because it might be exposed in command logs, activity displays, and so on.
548
- # Instead, use this function to convert the password to encrypted form before it is sent.
549
- #
550
- # The +password+ and +username+ arguments are the cleartext password, and the SQL name of the user it is for.
551
- # +algorithm+ specifies the encryption algorithm to use to encrypt the password.
552
- # Currently supported algorithms are +md5+ and +scram-sha-256+ (+on+ and +off+ are also accepted as aliases for +md5+, for compatibility with older server versions).
553
- # Note that support for +scram-sha-256+ was introduced in PostgreSQL version 10, and will not work correctly with older server versions.
554
- # If algorithm is omitted or +nil+, this function will query the server for the current value of the +password_encryption+ setting.
555
- # That can block, and will fail if the current transaction is aborted, or if the connection is busy executing another query.
556
- # If you wish to use the default algorithm for the server but want to avoid blocking, query +password_encryption+ yourself before calling #encrypt_password, and pass that value as the algorithm.
557
- #
558
- # Return value is the encrypted password.
559
- # The caller can assume the string doesn't contain any special characters that would require escaping.
560
- #
561
- # Available since PostgreSQL-10.
562
- # See also corresponding {libpq function}[https://www.postgresql.org/docs/current/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN].
563
- def encrypt_password( password, username, algorithm=nil )
564
- algorithm ||= exec("SHOW password_encryption").getvalue(0,0)
565
- sync_encrypt_password(password, username, algorithm)
566
- end
567
- alias async_encrypt_password encrypt_password
537
+ # conn.pipeline_sync
538
+ #
539
+ # Marks a synchronization point in a pipeline by sending a sync message and flushing the send buffer.
540
+ # This serves as the delimiter of an implicit transaction and an error recovery point.
541
+ #
542
+ # See enter_pipeline_mode
543
+ #
544
+ # Raises PG::Error if the connection is not in pipeline mode or sending a sync message failed.
545
+ #
546
+ # Available since PostgreSQL-14
547
+ def pipeline_sync(*args)
548
+ send_pipeline_sync(*args)
549
+ flush
550
+ end
551
+ alias async_pipeline_sync pipeline_sync
568
552
  end
569
553
 
554
+ # call-seq:
555
+ # conn.encrypt_password( password, username, algorithm=nil ) -> String
556
+ #
557
+ # This function is intended to be used by client applications that wish to send commands like <tt>ALTER USER joe PASSWORD 'pwd'</tt>.
558
+ # It is good practice not to send the original cleartext password in such a command, because it might be exposed in command logs, activity displays, and so on.
559
+ # Instead, use this function to convert the password to encrypted form before it is sent.
560
+ #
561
+ # The +password+ and +username+ arguments are the cleartext password, and the SQL name of the user it is for.
562
+ # +algorithm+ specifies the encryption algorithm to use to encrypt the password.
563
+ # Currently supported algorithms are +md5+ and +scram-sha-256+ (+on+ and +off+ are also accepted as aliases for +md5+, for compatibility with older server versions).
564
+ # Note that support for +scram-sha-256+ was introduced in PostgreSQL version 10, and will not work correctly with older server versions.
565
+ # If algorithm is omitted or +nil+, this function will query the server for the current value of the +password_encryption+ setting.
566
+ # That can block, and will fail if the current transaction is aborted, or if the connection is busy executing another query.
567
+ # If you wish to use the default algorithm for the server but want to avoid blocking, query +password_encryption+ yourself before calling #encrypt_password, and pass that value as the algorithm.
568
+ #
569
+ # Return value is the encrypted password.
570
+ # The caller can assume the string doesn't contain any special characters that would require escaping.
571
+ #
572
+ # See also corresponding {libpq function}[https://www.postgresql.org/docs/current/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN].
573
+ def encrypt_password( password, username, algorithm=nil )
574
+ algorithm ||= exec("SHOW password_encryption").getvalue(0,0)
575
+ sync_encrypt_password(password, username, algorithm)
576
+ end
577
+ alias async_encrypt_password encrypt_password
578
+
570
579
  # call-seq:
571
580
  # conn.reset()
572
581
  #
@@ -576,7 +585,9 @@ class PG::Connection
576
585
  # Use connection options from PG::Connection.new to reconnect with the same options but with renewed DNS resolution.
577
586
  # Use conninfo_hash as a fallback when connect_start was used to create the connection object.
578
587
  iopts = @iopts_for_reset || conninfo_hash.compact
579
- if iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
588
+ if iopts[:host] && !iopts[:host].empty? &&
589
+ iopts[:port] && !iopts[:port].empty? &&
590
+ PG.library_version >= 100000
580
591
  iopts = self.class.send(:resolve_hosts, iopts)
581
592
  end
582
593
  conninfo = self.class.parse_connect_args( iopts );
@@ -586,128 +597,283 @@ class PG::Connection
586
597
  end
587
598
  alias async_reset reset
588
599
 
589
- # call-seq:
590
- # conn.cancel() -> String
591
- #
592
- # Requests cancellation of the command currently being
593
- # processed.
594
- #
595
- # Returns +nil+ on success, or a string containing the
596
- # error message if a failure occurs.
597
- def cancel
598
- be_pid = backend_pid
599
- be_key = backend_key
600
- cancel_request = [0x10, 1234, 5678, be_pid, be_key].pack("NnnNN")
601
-
602
- if Fiber.respond_to?(:scheduler) && Fiber.scheduler && RUBY_PLATFORM =~ /mingw|mswin/
603
- # Ruby's nonblocking IO is not really supported on Windows.
604
- # We work around by using threads and explicit calls to wait_readable/wait_writable.
605
- cl = Thread.new(socket_io.remote_address) { |ra| ra.connect }.value
606
- begin
607
- cl.write_nonblock(cancel_request)
608
- rescue IO::WaitReadable, Errno::EINTR
609
- cl.wait_writable
610
- retry
600
+ if defined?(PG::CancelConnection)
601
+ # PostgreSQL-17+
602
+
603
+ def sync_cancel
604
+ cancon = PG::CancelConnection.new(self)
605
+ cancon.sync_cancel
606
+ rescue PG::Error => err
607
+ err.to_s
608
+ end
609
+
610
+ # call-seq:
611
+ # conn.cancel() -> String
612
+ #
613
+ # Requests cancellation of the command currently being
614
+ # processed.
615
+ #
616
+ # Returns +nil+ on success, or a string containing the
617
+ # error message if a failure occurs.
618
+ #
619
+ # On PostgreSQL-17+ client library the class PG::CancelConnection is used.
620
+ # On older client library a pure ruby implementation is used.
621
+ def cancel
622
+ cancon = PG::CancelConnection.new(self)
623
+ cancon.async_cancel
624
+ rescue PG::Error => err
625
+ err.to_s
626
+ end
627
+
628
+ else
629
+
630
+ # PostgreSQL < 17
631
+
632
+ def cancel
633
+ be_pid = backend_pid
634
+ be_key = backend_key
635
+ cancel_request = [0x10, 1234, 5678, be_pid, be_key].pack("NnnNN")
636
+
637
+ if Fiber.respond_to?(:scheduler) && Fiber.scheduler && RUBY_PLATFORM =~ /mingw|mswin/
638
+ # Ruby's nonblocking IO is not really supported on Windows.
639
+ # We work around by using threads and explicit calls to wait_readable/wait_writable.
640
+ cl = Thread.new(socket_io.remote_address) { |ra| ra.connect }.value
641
+ begin
642
+ cl.write_nonblock(cancel_request)
643
+ rescue IO::WaitReadable, Errno::EINTR
644
+ cl.wait_writable
645
+ retry
646
+ end
647
+ begin
648
+ cl.read_nonblock(1)
649
+ rescue IO::WaitReadable, Errno::EINTR
650
+ cl.wait_readable
651
+ retry
652
+ rescue EOFError
653
+ end
654
+ else
655
+ cl = socket_io.remote_address.connect
656
+ # Send CANCEL_REQUEST_CODE and parameters
657
+ cl.write(cancel_request)
658
+ # Wait for the postmaster to close the connection, which indicates that it's processed the request.
659
+ cl.read(1)
611
660
  end
612
- begin
613
- cl.read_nonblock(1)
614
- rescue IO::WaitReadable, Errno::EINTR
615
- cl.wait_readable
616
- retry
617
- rescue EOFError
661
+
662
+ cl.close
663
+ nil
664
+ rescue SystemCallError => err
665
+ err.to_s
666
+ end
667
+ end
668
+ alias async_cancel cancel
669
+
670
+ PLACEHOLDER_RE = /
671
+ '(?:''|[^'])*' | # string literal
672
+ "(?:""|[^"])*" | # quoted identifier
673
+ --[^\n]* | # line comment
674
+ \/\*.*?\*\/ | # block comment
675
+ \$\$.*?\$\$ | # dollar-quoted string. E.g. $$ $1 $$
676
+ \$(?<__dq_tag>[A-Za-z_][A-Za-z_0-9]*)\$.*?\$\k<__dq_tag>\$ | # named dollar-quoted string. E.g. $foo$ $1 $foo$
677
+ (?<placeholder>\$(?:[1-9]\d*)) # placeholder we are interested in
678
+ /mx
679
+ private_constant :PLACEHOLDER_RE
680
+ SAFE_TYPE_NAME_PART_RE = /\A[A-Za-z_][A-Za-z0-9_$]*\z/
681
+ private_constant :SAFE_TYPE_NAME_PART_RE
682
+
683
+ private def quote_type_name(name)
684
+ name = name.to_s.dup
685
+ array_suffix = name.slice!(/(?:\[\])+\z/) || ""
686
+ name = name.split(".", -1).map do |part|
687
+ SAFE_TYPE_NAME_PART_RE.match?(part) ? part : quote_ident(part)
688
+ end.join(".")
689
+ name + array_suffix
690
+ end
691
+
692
+ # Compiles your prepared SQL statement and the given positional arguments into plain SQL string.
693
+ #
694
+ # The resulting SQL string can be used with +conn.exec+ like the prepared SQL statement and parameters with +conn.exec_params+.
695
+ # +conn.exec_params+ is usually preferred because it's faster and safer.
696
+ # +embed_params+ is intended for debugging messages with positional parameters.
697
+ # It avoids manual insertion for later inspection in +psql+ or so.
698
+ #
699
+ # When using PG::Connection#exec_params, it's possible to set the numeric database type OID of a parameter by either a hash with +:type+ key, or by PG::Coder#oid .
700
+ # +embed_params+ casts the parameter the same way, but needs the name of the type instead its OID.
701
+ # That name must be provided by either a +:typename+ key in addition to the +:type+ OID or by PG::Coder#name in addition to PG::Coder#oid.
702
+ #
703
+ # Only text parameter format can be embedded into SQL text.
704
+ # Binary data ( <tt>:format => 1</tt> or PG::Coder#format == 1 ) raises an ArgumentError.
705
+ # However the common pattern of sending BYTEA (OID 17) data as format 1, which avoids escapting large blobs, is recognized and probably escaped into the SQL result string.
706
+ #
707
+ # Example:
708
+ # conn.embed_params('SELECT $1 AS a, $2 AS b, $3 AS c', [1, 2, {value: "\0", type: 17, format: 1}])
709
+ # => "SELECT '1' AS a, '2' AS b, '\\x00'::bytea AS c"
710
+ def embed_params(sql, params, type_map: type_map_for_queries)
711
+ return sql if params.empty?
712
+
713
+ oid_to_typecast = proc do |oid, tname, errtext|
714
+ if oid && oid > 0
715
+ if tname.to_s.empty?
716
+ raise(ArgumentError, "Database type name of OID #{oid.inspect} missing#{errtext}")
717
+ else
718
+ "::#{quote_type_name(tname)}"
719
+ end
618
720
  end
619
- elsif RUBY_ENGINE == 'truffleruby'
620
- begin
621
- cl = socket_io.remote_address.connect
622
- rescue NotImplementedError
623
- # Workaround for truffleruby < 21.3.0
624
- cl2 = Socket.for_fd(socket_io.fileno)
625
- cl2.autoclose = false
626
- adr = cl2.remote_address
627
- if adr.ip?
628
- cl = TCPSocket.new(adr.ip_address, adr.ip_port)
629
- cl.autoclose = false
721
+ end
722
+
723
+ encoders = type_map.query_param_encoders(params)
724
+ params = encoders.map.with_index do |enc, i|
725
+ value = params[i]
726
+ case value
727
+ when NilClass
728
+ 'NULL'
729
+ when PG::BasicTypeMapForQueries::BinaryData
730
+ "'#{ escape_bytea(value) }'"
731
+ else
732
+ if enc
733
+ raise ArgumentError, "binary encoded parameter from #{enc.inspect} cannot be inserted into SQL text" if enc.format != 0
734
+ "'#{escape(enc.encode(value))}'#{oid_to_typecast[enc.oid, enc.name, " - please set name of encoder #{enc.inspect}"]}"
735
+ elsif Hash === value
736
+ typename = value[:format] == 1 && value[:type] == 17 ?
737
+ "::bytea" : oid_to_typecast[value[:type], value[:typename], " - please set parameter key :typename"]
738
+
739
+ case value[:value]
740
+ when NilClass
741
+ "NULL#{typename}"
742
+ else
743
+ if value[:format] == 1
744
+ raise ArgumentError, "binary encoded parameter with OID #{value[:type].inspect} cannot be inserted into SQL text" if value[:type] && value[:type] != 17
745
+ "'#{escape_bytea(value[:value].to_s)}'#{typename}"
746
+ else
747
+ "'#{escape(value[:value].to_s)}'#{typename}"
748
+ end
749
+ end
630
750
  else
631
- cl = UNIXSocket.new(adr.unix_path)
632
- cl.autoclose = false
751
+ "'#{escape(value.to_s)}'"
633
752
  end
634
753
  end
635
- cl.write(cancel_request)
636
- cl.read(1)
637
- else
638
- cl = socket_io.remote_address.connect
639
- # Send CANCEL_REQUEST_CODE and parameters
640
- cl.write(cancel_request)
641
- # Wait for the postmaster to close the connection, which indicates that it's processed the request.
642
- cl.read(1)
643
754
  end
644
755
 
645
- cl.close
646
- nil
647
- rescue SystemCallError => err
648
- err.to_s
756
+ sql.gsub(PLACEHOLDER_RE).each do |matched|
757
+ placeholder = Regexp.last_match[:placeholder]
758
+ # Do not replace non-positional args string and pass it as is
759
+ next matched unless placeholder
760
+
761
+ params[placeholder[1..].to_i - 1]
762
+ end
649
763
  end
650
- alias async_cancel cancel
651
764
 
652
- private def async_connect_or_reset(poll_meth)
653
- # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
765
+ module Pollable
766
+ # Track the progress of the connection, waiting for the socket to become readable/writable before polling it.
767
+ #
768
+ # Connecting to multiple hosts is done like so:
769
+ # - All hosts are passed to PG::Connection.connect_start
770
+ # - As soon as the host is tried to connect the related host is removed from the hosts list
771
+ # - When the polling status changes to `PG::PGRES_POLLING_OK` the connection is returned and ready to use.
772
+ # - When the polling status changes to `PG::PGRES_POLLING_FAILED` connecting is aborted and a PG::ConnectionBad is raised with details to all connection attempts.
773
+ # - When a timeout occurs, connecting is restarted with the remaining hosts.
774
+ #
775
+ # The downside is that this connects only once to hosts which are listed twice when they timeout.
776
+ private def polling_loop(poll_meth)
777
+ connect_timeout = conninfo_hash[:connect_timeout]
778
+ if (timeo = connect_timeout.to_i) && timeo > 0
779
+ host_count = (conninfo_hash[:hostaddr].to_s.empty? ? conninfo_hash[:host] : conninfo_hash[:hostaddr]).to_s.count(",") + 1
780
+ stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
781
+ end
782
+ iopts = conninfo_hash.compact
783
+ connection_errors = []
784
+ poll_status = PG::PGRES_POLLING_WRITING
654
785
 
655
- if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
656
- host_count = conninfo_hash[:host].to_s.count(",") + 1
657
- stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
658
- end
786
+ until poll_status == PG::PGRES_POLLING_OK ||
787
+ poll_status == PG::PGRES_POLLING_FAILED
788
+
789
+ # Set single timeout to parameter "connect_timeout" but
790
+ # don't exceed total connection time of number-of-hosts * connect_timeout.
791
+ timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
659
792
 
660
- poll_status = PG::PGRES_POLLING_WRITING
661
- until poll_status == PG::PGRES_POLLING_OK ||
662
- poll_status == PG::PGRES_POLLING_FAILED
793
+ hostcnt = remove_current_host(iopts)
663
794
 
664
- # Set single timeout to parameter "connect_timeout" but
665
- # don't exceed total connection time of number-of-hosts * connect_timeout.
666
- timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
667
- event = if !timeout || timeout >= 0
668
- # If the socket needs to read, wait 'til it becomes readable to poll again
669
- case poll_status
670
- when PG::PGRES_POLLING_READING
671
- if defined?(IO::READABLE) # ruby-3.0+
795
+ event = if !timeout || timeout >= 0
796
+ # If the socket needs to read, wait 'til it becomes readable to poll again
797
+ case poll_status
798
+ when PG::PGRES_POLLING_READING
672
799
  socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
673
- else
674
- IO.select([socket_io], nil, [socket_io], timeout)
675
- end
676
800
 
677
- # ...and the same for when the socket needs to write
678
- when PG::PGRES_POLLING_WRITING
679
- if defined?(IO::WRITABLE) # ruby-3.0+
801
+ # ...and the same for when the socket needs to write
802
+ when PG::PGRES_POLLING_WRITING
680
803
  # Use wait instead of wait_readable, since connection errors are delivered as
681
804
  # exceptional/priority events on Windows.
682
805
  socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
683
- else
684
- # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
685
- IO.select(nil, [socket_io], [socket_io], timeout)
686
806
  end
687
807
  end
688
- end
689
- # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
690
- # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
691
- unless event
692
- if self.class.send(:host_is_named_pipe?, host)
693
- connhost = "on socket \"#{host}\""
694
- elsif respond_to?(:hostaddr)
695
- connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
696
- else
697
- connhost = "at \"#{host}\", port #{port}"
808
+
809
+ # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
810
+ # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
811
+ unless event
812
+ connection_errors << (error_message + "timeout expired")
813
+ if hostcnt > 0
814
+ reset_start2(self.class.parse_connect_args(iopts))
815
+ # Restart polling with waiting for writable.
816
+ # Otherwise "not connected" error is raised on Windows.
817
+ poll_status = PG::PGRES_POLLING_WRITING
818
+ next
819
+ else
820
+ finish
821
+ raise PG::ConnectionBad.new(connection_errors.join("\n").b, connection: self)
822
+ end
698
823
  end
699
- raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
824
+
825
+ # Check to see if it's finished or failed yet
826
+ poll_status = send( poll_meth )
700
827
  end
701
828
 
702
- # Check to see if it's finished or failed yet
703
- poll_status = send( poll_meth )
829
+ unless status == PG::CONNECTION_OK
830
+ msg = error_message
831
+ finish
832
+ raise PG::ConnectionBad.new(connection_errors.map{|e| e + "\n" }.join.b + msg, connection: self)
833
+ end
704
834
  end
705
835
 
706
- unless status == PG::CONNECTION_OK
707
- msg = error_message
708
- finish
709
- raise PG::ConnectionBad.new(msg, connection: self)
836
+ # Remove the host to which the connection is currently established from the option hash.
837
+ # Affected options are:
838
+ # - :host
839
+ # - :hostaddr
840
+ # - :port
841
+ #
842
+ # Return the number of remaining hosts.
843
+ private def remove_current_host(iopts)
844
+ ihosts = iopts[:host]&.split(",", -1)
845
+ ihostaddrs = iopts[:hostaddr]&.split(",", -1)
846
+ iports = iopts[:port]&.split(",", -1)
847
+ iports = iports * (ihosts || ihostaddrs || [1]).size if iports&.size == 1
848
+
849
+ idx = (ihosts || ihostaddrs || iports).index.with_index do |_, i|
850
+ (ihosts ? ihosts[i] == host : true) &&
851
+ (ihostaddrs && respond_to?(:hostaddr, true) ? ihostaddrs[i] == hostaddr : true) &&
852
+ (iports ? iports[i].to_i == port : true)
853
+ end
854
+
855
+ if idx
856
+ ihosts&.delete_at(idx)
857
+ ihostaddrs&.delete_at(idx)
858
+ iports&.delete_at(idx)
859
+
860
+ iopts.merge!(
861
+ host: ihosts.join(",")) if ihosts
862
+ iopts.merge!(
863
+ hostaddr: ihostaddrs.join(",")) if ihostaddrs
864
+ iopts.merge!(
865
+ port: iports.join(",")) if iports
866
+ end
867
+
868
+ (ihosts || ihostaddrs || iports).size
710
869
  end
870
+ end
871
+
872
+ include Pollable
873
+
874
+ private def async_connect_or_reset(poll_meth)
875
+ # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
876
+ polling_loop(poll_meth)
711
877
 
712
878
  # Set connection to nonblocking to handle all blocking states in ruby.
713
879
  # That way a fiber scheduler is able to handle IO requests.
@@ -723,7 +889,7 @@ class PG::Connection
723
889
  # PG::Connection.new(connection_string) -> conn
724
890
  # PG::Connection.new(host, port, options, tty, dbname, user, password) -> conn
725
891
  #
726
- # Create a connection to the specified server.
892
+ # === Create a connection to the specified server.
727
893
  #
728
894
  # +connection_hash+ must be a ruby Hash with connection parameters.
729
895
  # See the {list of valid parameters}[https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS] in the PostgreSQL documentation.
@@ -747,7 +913,13 @@ class PG::Connection
747
913
  # [+password+]
748
914
  # login password
749
915
  #
750
- # Examples:
916
+ #
917
+ # If the Ruby default internal encoding is set (i.e., <code>Encoding.default_internal != nil</code>), the
918
+ # connection will have its +client_encoding+ set accordingly.
919
+ #
920
+ # Raises a PG::Error if the connection fails.
921
+ #
922
+ # === Examples:
751
923
  #
752
924
  # # Connect using all defaults
753
925
  # PG::Connection.new
@@ -764,10 +936,18 @@ class PG::Connection
764
936
  # # As an URI
765
937
  # PG::Connection.new( "postgresql://user:pass@pgsql.example.com:5432/testdb?sslmode=require" )
766
938
  #
767
- # If the Ruby default internal encoding is set (i.e., <code>Encoding.default_internal != nil</code>), the
768
- # connection will have its +client_encoding+ set accordingly.
939
+ # === Specifying Multiple Hosts
940
+ #
941
+ # It is possible to specify multiple hosts to connect to, so that they are tried in the given order or optionally in random order.
942
+ # In the Keyword/Value format, the host, hostaddr, and port options accept comma-separated lists of values.
943
+ # 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:
944
+ # - All hosts are resolved before the first connection is tried.
945
+ # This means that when +load_balance_hosts+ is set to +random+, then all resolved addresses are tried randomly in one level.
946
+ # When a host resolves to more than one address, it is therefore tried more often than a host that has only one address.
947
+ # - 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.
948
+ # 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.
949
+ # This is because in non-timeout cases the host is tried multiple times.
769
950
  #
770
- # Raises a PG::Error if the connection fails.
771
951
  def new(*args)
772
952
  conn = connect_to_hosts(*args)
773
953
 
@@ -788,7 +968,6 @@ class PG::Connection
788
968
 
789
969
  # Resolve DNS in Ruby to avoid blocking state while connecting.
790
970
  # Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
791
- # This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
792
971
  private def resolve_hosts(iopts)
793
972
  ihosts = iopts[:host].split(",", -1)
794
973
  iports = iopts[:port].split(",", -1)
@@ -798,16 +977,7 @@ class PG::Connection
798
977
 
799
978
  dests = ihosts.each_with_index.flat_map do |mhost, idx|
800
979
  unless host_is_named_pipe?(mhost)
801
- if Fiber.respond_to?(:scheduler) &&
802
- Fiber.scheduler &&
803
- RUBY_VERSION < '3.1.'
804
-
805
- # Use a second thread to avoid blocking of the scheduler.
806
- # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
807
- hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
808
- else
809
- hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
810
- end
980
+ hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
811
981
  else
812
982
  # No hostname to resolve (UnixSocket)
813
983
  hostaddrs = [nil]
@@ -820,21 +990,47 @@ class PG::Connection
820
990
  port: dests.map{|d| d[2] }.join(","))
821
991
  end
822
992
 
993
+ RESOLUTION_KEYS = [:host, :hostaddr, :port].freeze
994
+ private_constant :RESOLUTION_KEYS
995
+
823
996
  private def connect_to_hosts(*args)
824
997
  option_string = parse_connect_args(*args)
825
- iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
826
- iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
998
+ iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}) { |h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
999
+
1000
+ has_explicit_host = (!iopts[:host].to_s.empty? || !iopts[:hostaddr].to_s.empty?) && !iopts[:port].to_s.empty?
1001
+ has_explicit_service = !iopts[:service].to_s.empty?
1002
+
1003
+ iopts_with_defaults = PG::Connection.conndefaults.each_with_object({}) do |h, o|
1004
+ k = h[:keyword].to_sym
1005
+ # Only use the host/hostname/port keys from defaults and leave user/dbname/sslmode/... for libpq, so that they are processed in the right order.
1006
+ o[k] = h[:val] if h[:val] && RESOLUTION_KEYS.include?(k)
1007
+ end.merge(iopts)
1008
+
1009
+ if PG::BUNDLED_LIBPQ_WITH_UNIXSOCKET && iopts_with_defaults[:host].to_s.empty? && iopts_with_defaults[:hostaddr].to_s.empty?
1010
+ # Many distros patch the hardcoded default UnixSocket path in libpq to /var/run/postgresql instead of /tmp .
1011
+ # We simply try them all.
1012
+ iopts[:host] = "/var/run/postgresql" + # Ubuntu, Debian, Fedora, Opensuse
1013
+ ",/run/postgresql" + # Alpine, Archlinux, Gentoo
1014
+ ",/tmp" # Stock PostgreSQL
1015
+ end
827
1016
 
828
1017
  iopts_for_reset = iopts
829
- if iopts[:hostaddr]
1018
+ if iopts_with_defaults[:hostaddr]
830
1019
  # hostaddr is provided -> no need to resolve hostnames
831
-
832
- elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
833
- iopts = resolve_hosts(iopts)
1020
+ elsif has_explicit_service && !has_explicit_host
1021
+ # The pg_service.conf might provide host/user/port/etc.
1022
+ # Ruby-pg cannot interpret pg_service without reading INI file and the additional LDAP stuff.
1023
+ # So, pass params through and let libpq resolve the service, possibly blocking the Thread.scheduler.
1024
+ # This ensures the processing order of libpq which is:
1025
+ # connection string => service file => environment variable => compiled default
1026
+ elsif iopts_with_defaults[:host] && !iopts_with_defaults[:host].empty?
1027
+ # Do host resolution in Ruby to avoid blocking Thread.scheduler while DNS queries in libpq.
1028
+ iopts_for_reset = iopts_with_defaults
1029
+ iopts = resolve_hosts(iopts_with_defaults)
834
1030
  else
835
1031
  # No host given
836
1032
  end
837
- conn = self.connect_start(iopts) or
1033
+ conn = connect_start(iopts) or
838
1034
  raise(PG::Error, "Unable to create a new connection")
839
1035
 
840
1036
  raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
@@ -897,14 +1093,29 @@ class PG::Connection
897
1093
  private_constant :REDIRECT_CLASS_METHODS
898
1094
 
899
1095
  # These methods are affected by PQsetnonblocking
900
- REDIRECT_SEND_METHODS = PG.make_shareable({
1096
+ REDIRECT_SEND_METHODS = {
901
1097
  :isnonblocking => [:async_isnonblocking, :sync_isnonblocking],
902
1098
  :nonblocking? => [:async_isnonblocking, :sync_isnonblocking],
903
1099
  :put_copy_data => [:async_put_copy_data, :sync_put_copy_data],
904
1100
  :put_copy_end => [:async_put_copy_end, :sync_put_copy_end],
905
1101
  :flush => [:async_flush, :sync_flush],
906
- })
1102
+ }
907
1103
  private_constant :REDIRECT_SEND_METHODS
1104
+ if PG::Connection.instance_methods.include? :sync_pipeline_sync
1105
+ if PG::Connection.instance_methods.include? :send_pipeline_sync
1106
+ # PostgreSQL-17+
1107
+ REDIRECT_SEND_METHODS.merge!({
1108
+ :pipeline_sync => [:async_pipeline_sync, :sync_pipeline_sync],
1109
+ })
1110
+ else
1111
+ # PostgreSQL-14+
1112
+ REDIRECT_SEND_METHODS.merge!({
1113
+ :pipeline_sync => [:sync_pipeline_sync, :sync_pipeline_sync],
1114
+ })
1115
+ end
1116
+ end
1117
+ PG.make_shareable(REDIRECT_SEND_METHODS)
1118
+
908
1119
  REDIRECT_METHODS = {
909
1120
  :exec => [:async_exec, :sync_exec],
910
1121
  :query => [:async_exec, :sync_exec],
@@ -921,12 +1132,14 @@ class PG::Connection
921
1132
  :set_client_encoding => [:async_set_client_encoding, :sync_set_client_encoding],
922
1133
  :client_encoding= => [:async_set_client_encoding, :sync_set_client_encoding],
923
1134
  :cancel => [:async_cancel, :sync_cancel],
1135
+ :encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
924
1136
  }
925
1137
  private_constant :REDIRECT_METHODS
926
-
927
- if PG::Connection.instance_methods.include? :async_encrypt_password
1138
+ if PG::Connection.instance_methods.include? :async_close_prepared
1139
+ # PostgreSQL-17+
928
1140
  REDIRECT_METHODS.merge!({
929
- :encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
1141
+ :close_prepared => [:async_close_prepared, :sync_close_prepared],
1142
+ :close_portal => [:async_close_portal, :sync_close_portal],
930
1143
  })
931
1144
  end
932
1145
  PG.make_shareable(REDIRECT_METHODS)