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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/{History.md → CHANGELOG.md} +126 -0
- data/Gemfile +11 -7
- data/README-Windows.rdoc +1 -1
- data/README.ja.md +83 -32
- data/README.md +79 -29
- data/Rakefile +84 -19
- data/certs/larskanis-2026.pem +24 -0
- data/ext/errorcodes.def +9 -0
- data/ext/errorcodes.rb +1 -1
- data/ext/errorcodes.txt +8 -1
- data/ext/extconf.rb +194 -30
- data/ext/gvl_wrappers.c +13 -2
- data/ext/gvl_wrappers.h +47 -134
- data/ext/pg.c +23 -16
- data/ext/pg.h +23 -24
- data/ext/pg_binary_decoder.c +157 -3
- data/ext/pg_binary_encoder.c +219 -9
- data/ext/pg_cancel_connection.c +360 -0
- data/ext/pg_coder.c +58 -9
- data/ext/pg_connection.c +558 -291
- data/ext/pg_copy_coder.c +36 -23
- data/ext/pg_record_coder.c +11 -7
- data/ext/pg_result.c +144 -76
- data/ext/pg_text_decoder.c +7 -7
- data/ext/pg_text_encoder.c +29 -11
- data/ext/pg_tuple.c +9 -9
- data/ext/pg_type_map.c +45 -3
- data/ext/pg_type_map_all_strings.c +3 -3
- data/ext/pg_type_map_by_class.c +2 -2
- data/ext/pg_type_map_by_column.c +4 -3
- data/ext/pg_type_map_by_mri_type.c +1 -1
- data/ext/pg_type_map_by_oid.c +4 -2
- data/ext/pg_type_map_in_ruby.c +2 -2
- data/ext/pg_util.c +2 -2
- data/ext/pg_util.h +2 -2
- data/lib/pg/basic_type_map_based_on_result.rb +5 -0
- data/lib/pg/basic_type_map_for_queries.rb +16 -5
- data/lib/pg/basic_type_map_for_results.rb +5 -0
- data/lib/pg/basic_type_registry.rb +2 -2
- data/lib/pg/cancel_connection.rb +53 -0
- data/lib/pg/coder.rb +4 -2
- data/lib/pg/connection.rb +393 -180
- data/lib/pg/text_decoder/date.rb +1 -1
- data/lib/pg/text_decoder/json.rb +1 -1
- data/lib/pg/text_encoder/date.rb +7 -1
- data/lib/pg/text_encoder/json.rb +1 -1
- data/lib/pg/version.rb +2 -1
- data/lib/pg.rb +156 -130
- data/misc/glibc/Dockerfile +20 -0
- data/misc/glibc/docker-compose.yml +9 -0
- data/misc/glibc/glibc_spec.rb +5 -0
- data/misc/yugabyte/Dockerfile +9 -0
- data/misc/yugabyte/docker-compose.yml +28 -0
- data/misc/yugabyte/pg-test.rb +45 -0
- data/pg.gemspec +5 -3
- data/ports/patches/krb5/1.22.2/0001-Allow-static-linking-krb5-library.patch +30 -0
- data/ports/patches/krb5/1.22.2/0002-unknown-command-line-option-on-clang.patch +12 -0
- data/ports/patches/postgresql/18.6/0001-Use-workaround-of-__builtin_setjmp-only-on-MINGW-on-.patch +42 -0
- data/rakelib/pg_gem_helper.rb +64 -0
- data/sample/async_mixed.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +36 -22
- metadata.gz.sig +0 -0
- data/Manifest.txt +0 -72
- 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(:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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? :
|
|
535
|
+
if method_defined? :send_pipeline_sync
|
|
543
536
|
# call-seq:
|
|
544
|
-
# conn.
|
|
545
|
-
#
|
|
546
|
-
#
|
|
547
|
-
#
|
|
548
|
-
#
|
|
549
|
-
#
|
|
550
|
-
#
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
661
|
-
until poll_status == PG::PGRES_POLLING_OK ||
|
|
662
|
-
poll_status == PG::PGRES_POLLING_FAILED
|
|
793
|
+
hostcnt = remove_current_host(iopts)
|
|
663
794
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
824
|
+
|
|
825
|
+
# Check to see if it's finished or failed yet
|
|
826
|
+
poll_status = send( poll_meth )
|
|
700
827
|
end
|
|
701
828
|
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
768
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1018
|
+
if iopts_with_defaults[:hostaddr]
|
|
830
1019
|
# hostaddr is provided -> no need to resolve hostnames
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1138
|
+
if PG::Connection.instance_methods.include? :async_close_prepared
|
|
1139
|
+
# PostgreSQL-17+
|
|
928
1140
|
REDIRECT_METHODS.merge!({
|
|
929
|
-
:
|
|
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)
|