pg 1.3.5 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pg/connection.rb CHANGED
@@ -46,37 +46,6 @@ class PG::Connection
46
46
  hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
47
47
  end
48
48
 
49
- # Decode a connection string to Hash options
50
- #
51
- # Value are properly unquoted and unescaped.
52
- def self.connect_string_to_hash( str )
53
- options = {}
54
- key = nil
55
- value = String.new
56
- str.scan(/\G\s*(?>([^\s\\\']+)\s*=\s*|([^\s\\\']+)|'((?:[^\'\\]|\\.)*)'|(\\.?)|(\S))(\s|\z)?/m) do
57
- |k, word, sq, esc, garbage, sep|
58
- raise ArgumentError, "unterminated quoted string in connection info string: #{str.inspect}" if garbage
59
- if k
60
- key = k
61
- else
62
- value << (word || (sq || esc).gsub(/\\(.)/, '\\1'))
63
- end
64
- if sep
65
- raise ArgumentError, "missing = after #{value.inspect}" unless key
66
- options[key.to_sym] = value
67
- key = nil
68
- value = String.new
69
- end
70
- end
71
- options
72
- end
73
-
74
- # URI defined in RFC3986
75
- # This regexp is modified to allow host to specify multiple comma separated components captured as <hostports> and to disallow comma in hostnames.
76
- # Taken from: https://github.com/ruby/ruby/blob/be04006c7d2f9aeb7e9d8d09d945b3a9c7850202/lib/uri/rfc3986_parser.rb#L6
77
- HOST_AND_PORT = /(?<hostport>(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[-\.!$&-+0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)/
78
- POSTGRESQL_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<hostports>#{HOST_AND_PORT}(?:,\g<hostport>)*))(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
79
-
80
49
  # Parse the connection +args+ into a connection-parameter string.
81
50
  # See PG::Connection.new for valid arguments.
82
51
  #
@@ -87,91 +56,43 @@ class PG::Connection
87
56
  # * URI object
88
57
  # * positional arguments
89
58
  #
90
- # The method adds the option "hostaddr" and "fallback_application_name" if they aren't already set.
91
- # The URI and the options string is passed through and "hostaddr" as well as "fallback_application_name"
92
- # are added to the end.
93
- def self::parse_connect_args( *args )
59
+ # The method adds the option "fallback_application_name" if it isn't already set.
60
+ # It returns a connection string with "key=value" pairs.
61
+ def self.parse_connect_args( *args )
94
62
  hash_arg = args.last.is_a?( Hash ) ? args.pop.transform_keys(&:to_sym) : {}
95
- option_string = ""
96
63
  iopts = {}
97
64
 
98
65
  if args.length == 1
99
66
  case args.first
100
- when URI, POSTGRESQL_URI
101
- uri = args.first.to_s
102
- uri_match = POSTGRESQL_URI.match(uri)
103
- if uri_match['query']
104
- iopts = URI.decode_www_form(uri_match['query']).to_h.transform_keys(&:to_sym)
105
- end
106
- # extract "host1,host2" from "host1:5432,host2:5432"
107
- iopts[:host] = uri_match['hostports'].split(',', -1).map do |hostport|
108
- hostmatch = /\A#{HOST_AND_PORT}\z/.match(hostport)
109
- hostmatch['IPv6address'] || hostmatch['IPv4address'] || hostmatch['reg-name']&.gsub(/%(\h\h)/){ $1.hex.chr }
110
- end.join(',')
111
- oopts = {}
112
- when /=/
113
- # Option string style
114
- option_string = args.first.to_s
115
- iopts = connect_string_to_hash(option_string)
116
- oopts = {}
67
+ when URI, /=/, /:\/\//
68
+ # Option or URL string style
69
+ conn_string = args.first.to_s
70
+ iopts = PG::Connection.conninfo_parse(conn_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
117
71
  else
118
72
  # Positional parameters (only host given)
119
73
  iopts[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
120
- oopts = iopts.dup
121
74
  end
122
75
  else
123
- # Positional parameters
76
+ # Positional parameters with host and more
124
77
  max = CONNECT_ARGUMENT_ORDER.length
125
78
  raise ArgumentError,
126
- "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
79
+ "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
127
80
 
128
81
  CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
129
82
  iopts[ k.to_sym ] = v if v
130
83
  end
131
84
  iopts.delete(:tty) # ignore obsolete tty parameter
132
- oopts = iopts.dup
133
85
  end
134
86
 
135
87
  iopts.merge!( hash_arg )
136
- oopts.merge!( hash_arg )
137
-
138
- # Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
139
- if (host=iopts[:host]) && !iopts[:hostaddr]
140
- hostaddrs = host.split(",", -1).map do |mhost|
141
- if !mhost.empty? && !mhost.start_with?("/") && # isn't UnixSocket
142
- # isn't a path on Windows
143
- (RUBY_PLATFORM !~ /mingw|mswin/ || mhost !~ /\A\w:[\/\\]/)
144
-
145
- if Fiber.respond_to?(:scheduler) &&
146
- Fiber.scheduler &&
147
- RUBY_VERSION < '3.1.'
148
-
149
- # Use a second thread to avoid blocking of the scheduler.
150
- # `IPSocket.getaddress` isn't fiber aware before ruby-3.1.
151
- Thread.new{ IPSocket.getaddress(mhost) rescue '' }.value
152
- else
153
- IPSocket.getaddress(mhost) rescue ''
154
- end
155
- end
156
- end
157
- oopts[:hostaddr] = hostaddrs.join(",") if hostaddrs.any?
158
- end
159
88
 
160
89
  if !iopts[:fallback_application_name]
161
- oopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
90
+ iopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
162
91
  end
163
92
 
164
- if uri
165
- uri += uri_match['query'] ? "&" : "?"
166
- uri += URI.encode_www_form( oopts )
167
- return uri
168
- else
169
- option_string += ' ' unless option_string.empty? && oopts.empty?
170
- return option_string + connect_hash_to_string(oopts)
171
- end
93
+ return connect_hash_to_string(iopts)
172
94
  end
173
95
 
174
-
175
96
  # call-seq:
176
97
  # conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
177
98
  #
@@ -241,7 +162,7 @@ class PG::Connection
241
162
  # ["more", "data", "to", "copy"]
242
163
 
243
164
  def copy_data( sql, coder=nil )
244
- raise PG::NotInBlockingMode, "copy_data can not be used in nonblocking mode" if nonblocking?
165
+ raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
245
166
  res = exec( sql )
246
167
 
247
168
  case res.result_status
@@ -273,11 +194,15 @@ class PG::Connection
273
194
  yield res
274
195
  rescue Exception => err
275
196
  cancel
276
- while get_copy_data
197
+ begin
198
+ while get_copy_data
199
+ end
200
+ rescue PG::Error
201
+ # Ignore error in cleanup to avoid losing original exception
277
202
  end
278
203
  while get_result
279
204
  end
280
- raise
205
+ raise err
281
206
  else
282
207
  res = get_last_result
283
208
  if !res || res.result_status != PGRES_COMMAND_OK
@@ -285,7 +210,7 @@ class PG::Connection
285
210
  end
286
211
  while get_result
287
212
  end
288
- raise PG::NotAllCopyDataRetrieved, "Not all COPY data retrieved"
213
+ raise PG::NotAllCopyDataRetrieved.new("Not all COPY data retrieved", connection: self)
289
214
  end
290
215
  res
291
216
  ensure
@@ -310,16 +235,17 @@ class PG::Connection
310
235
  # and a +COMMIT+ at the end of the block, or
311
236
  # +ROLLBACK+ if any exception occurs.
312
237
  def transaction
238
+ rollback = false
313
239
  exec "BEGIN"
314
- res = yield(self)
240
+ yield(self)
315
241
  rescue Exception
242
+ rollback = true
316
243
  cancel if transaction_status == PG::PQTRANS_ACTIVE
317
244
  block
318
245
  exec "ROLLBACK"
319
246
  raise
320
- else
321
- exec "COMMIT"
322
- res
247
+ ensure
248
+ exec "COMMIT" unless rollback
323
249
  end
324
250
 
325
251
  ### Returns an array of Hashes with connection defaults. See ::conndefaults
@@ -482,10 +408,10 @@ class PG::Connection
482
408
  # See also #copy_data.
483
409
  #
484
410
  def put_copy_data(buffer, encoder=nil)
485
- until sync_put_copy_data(buffer, encoder)
486
- flush
411
+ until res=sync_put_copy_data(buffer, encoder)
412
+ res = flush
487
413
  end
488
- flush
414
+ res
489
415
  end
490
416
  alias async_put_copy_data put_copy_data
491
417
 
@@ -545,6 +471,7 @@ class PG::Connection
545
471
  def reset
546
472
  reset_start
547
473
  async_connect_or_reset(:reset_poll)
474
+ self
548
475
  end
549
476
  alias async_reset reset
550
477
 
@@ -613,28 +540,62 @@ class PG::Connection
613
540
 
614
541
  private def async_connect_or_reset(poll_meth)
615
542
  # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
543
+
544
+ if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
545
+ # Lowest timeout is 2 seconds - like in libpq
546
+ timeo = [timeo, 2].max
547
+ stop_time = timeo + Process.clock_gettime(Process::CLOCK_MONOTONIC)
548
+ end
549
+
616
550
  poll_status = PG::PGRES_POLLING_WRITING
617
551
  until poll_status == PG::PGRES_POLLING_OK ||
618
552
  poll_status == PG::PGRES_POLLING_FAILED
619
553
 
620
- # If the socket needs to read, wait 'til it becomes readable to poll again
621
- case poll_status
622
- when PG::PGRES_POLLING_READING
623
- socket_io.wait_readable
554
+ timeout = stop_time&.-(Process.clock_gettime(Process::CLOCK_MONOTONIC))
555
+ event = if !timeout || timeout >= 0
556
+ # If the socket needs to read, wait 'til it becomes readable to poll again
557
+ case poll_status
558
+ when PG::PGRES_POLLING_READING
559
+ if defined?(IO::READABLE) # ruby-3.0+
560
+ socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
561
+ else
562
+ IO.select([socket_io], nil, [socket_io], timeout)
563
+ end
624
564
 
625
- # ...and the same for when the socket needs to write
626
- when PG::PGRES_POLLING_WRITING
627
- socket_io.wait_writable
565
+ # ...and the same for when the socket needs to write
566
+ when PG::PGRES_POLLING_WRITING
567
+ if defined?(IO::WRITABLE) # ruby-3.0+
568
+ # Use wait instead of wait_readable, since connection errors are delivered as
569
+ # exceptional/priority events on Windows.
570
+ socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
571
+ else
572
+ # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
573
+ IO.select(nil, [socket_io], [socket_io], timeout)
574
+ end
575
+ end
576
+ end
577
+ # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
578
+ # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
579
+ unless event
580
+ if self.class.send(:host_is_named_pipe?, host)
581
+ connhost = "on socket \"#{host}\""
582
+ elsif respond_to?(:hostaddr)
583
+ connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
584
+ else
585
+ connhost = "at \"#{host}\", port #{port}"
586
+ end
587
+ raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
628
588
  end
629
589
 
630
590
  # Check to see if it's finished or failed yet
631
591
  poll_status = send( poll_meth )
592
+ @last_status = status unless [PG::CONNECTION_BAD, PG::CONNECTION_OK].include?(status)
632
593
  end
633
594
 
634
595
  unless status == PG::CONNECTION_OK
635
596
  msg = error_message
636
597
  finish
637
- raise PG::ConnectionBad, msg
598
+ raise PG::ConnectionBad.new(msg, connection: self)
638
599
  end
639
600
 
640
601
  # Set connection to nonblocking to handle all blocking states in ruby.
@@ -642,8 +603,6 @@ class PG::Connection
642
603
  sync_setnonblocking(true)
643
604
  self.flush_data = true
644
605
  set_default_encoding
645
-
646
- self
647
606
  end
648
607
 
649
608
  class << self
@@ -699,12 +658,16 @@ class PG::Connection
699
658
  #
700
659
  # Raises a PG::Error if the connection fails.
701
660
  def new(*args, **kwargs)
702
- conn = self.connect_start(*args, **kwargs ) or
703
- raise(PG::Error, "Unable to create a new connection")
661
+ conn = connect_to_hosts(*args, **kwargs)
704
662
 
705
- raise(PG::ConnectionBad, conn.error_message) if conn.status == PG::CONNECTION_BAD
706
-
707
- conn.send(:async_connect_or_reset, :connect_poll)
663
+ if block_given?
664
+ begin
665
+ return yield conn
666
+ ensure
667
+ conn.finish
668
+ end
669
+ end
670
+ conn
708
671
  end
709
672
  alias async_connect new
710
673
  alias connect new
@@ -712,6 +675,99 @@ class PG::Connection
712
675
  alias setdb new
713
676
  alias setdblogin new
714
677
 
678
+ private def connect_to_hosts(*args)
679
+ option_string = parse_connect_args(*args)
680
+ iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
681
+ iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
682
+
683
+ errors = []
684
+ if iopts[:hostaddr]
685
+ # hostaddr is provided -> no need to resolve hostnames
686
+ ihostaddrs = iopts[:hostaddr].split(",", -1)
687
+
688
+ ihosts = iopts[:host].split(",", -1) if iopts[:host]
689
+ raise PG::ConnectionBad, "could not match #{ihosts.size} host names to #{ihostaddrs.size} hostaddr values" if ihosts && ihosts.size != ihostaddrs.size
690
+
691
+ iports = iopts[:port].split(",", -1)
692
+ iports = iports * ihostaddrs.size if iports.size == 1
693
+ raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihostaddrs.size} hosts" if iports.size != ihostaddrs.size
694
+
695
+ # Try to connect to each hostaddr with separate timeout
696
+ ihostaddrs.each_with_index do |ihostaddr, idx|
697
+ oopts = iopts.merge(hostaddr: ihostaddr, port: iports[idx])
698
+ oopts[:host] = ihosts[idx] if ihosts
699
+ c = connect_internal(oopts, errors)
700
+ return c if c
701
+ end
702
+ elsif iopts[:host]
703
+ # Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
704
+ ihosts = iopts[:host].split(",", -1)
705
+
706
+ iports = iopts[:port].split(",", -1)
707
+ iports = iports * ihosts.size if iports.size == 1
708
+ raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
709
+
710
+ ihosts.each_with_index do |mhost, idx|
711
+ unless host_is_named_pipe?(mhost)
712
+ addrs = if Fiber.respond_to?(:scheduler) &&
713
+ Fiber.scheduler &&
714
+ RUBY_VERSION < '3.1.'
715
+
716
+ # Use a second thread to avoid blocking of the scheduler.
717
+ # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
718
+ Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
719
+ else
720
+ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
721
+ end
722
+
723
+ # Try to connect to each host with separate timeout
724
+ addrs.each do |addr|
725
+ oopts = iopts.merge(hostaddr: addr, host: mhost, port: iports[idx])
726
+ c = connect_internal(oopts, errors)
727
+ return c if c
728
+ end
729
+ else
730
+ # No hostname to resolve (UnixSocket)
731
+ oopts = iopts.merge(host: mhost, port: iports[idx])
732
+ c = connect_internal(oopts, errors)
733
+ return c if c
734
+ end
735
+ end
736
+ else
737
+ # No host given
738
+ return connect_internal(iopts)
739
+ end
740
+ raise PG::ConnectionBad, errors.join("\n")
741
+ end
742
+
743
+ private def connect_internal(opts, errors=nil)
744
+ begin
745
+ conn = self.connect_start(opts) or
746
+ raise(PG::Error, "Unable to create a new connection")
747
+
748
+ raise PG::ConnectionBad.new(conn.error_message, connection: self) if conn.status == PG::CONNECTION_BAD
749
+
750
+ conn.send(:async_connect_or_reset, :connect_poll)
751
+ rescue PG::ConnectionBad => err
752
+ if errors && !(conn && [PG::CONNECTION_AWAITING_RESPONSE].include?(conn.instance_variable_get(:@last_status)))
753
+ # Seems to be no authentication error -> try next host
754
+ errors << err
755
+ return nil
756
+ else
757
+ # Probably an authentication error
758
+ raise
759
+ end
760
+ end
761
+ conn
762
+ end
763
+
764
+ private def host_is_named_pipe?(host_string)
765
+ host_string.empty? || host_string.start_with?("/") || # it's UnixSocket?
766
+ host_string.start_with?("@") || # it's UnixSocket in the abstract namespace?
767
+ # it's a path on Windows?
768
+ (RUBY_PLATFORM =~ /mingw|mswin/ && host_string =~ /\A([\/\\]|\w:[\/\\])/)
769
+ end
770
+
715
771
  # call-seq:
716
772
  # PG::Connection.ping(connection_hash) -> Integer
717
773
  # PG::Connection.ping(connection_string) -> Integer
data/lib/pg/exceptions.rb CHANGED
@@ -6,7 +6,13 @@ require 'pg' unless defined?( PG )
6
6
 
7
7
  module PG
8
8
 
9
- class Error < StandardError; end
9
+ class Error < StandardError
10
+ def initialize(msg, connection: nil, result: nil)
11
+ @connection = connection
12
+ @result = result
13
+ super(msg)
14
+ end
15
+ end
10
16
 
11
17
  end # module PG
12
18
 
data/lib/pg/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module PG
2
2
  # Library version
3
- VERSION = '1.3.5'
3
+ VERSION = '1.4.0'
4
4
  end
data/lib/pg.rb CHANGED
@@ -59,14 +59,14 @@ module PG
59
59
  # Get the PG library version.
60
60
  #
61
61
  # +include_buildnum+ is no longer used and any value passed will be ignored.
62
- def self::version_string( include_buildnum=nil )
63
- return "%s %s" % [ self.name, VERSION ]
62
+ def self.version_string( include_buildnum=nil )
63
+ "%s %s" % [ self.name, VERSION ]
64
64
  end
65
65
 
66
66
 
67
67
  ### Convenience alias for PG::Connection.new.
68
- def self::connect( *args, **kwargs )
69
- return PG::Connection.new( *args, **kwargs )
68
+ def self.connect( *args, &block )
69
+ Connection.new( *args, &block )
70
70
  end
71
71
 
72
72
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.5
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Granger
@@ -36,7 +36,7 @@ cert_chain:
36
36
  oL1mUdzB8KrZL4/WbG5YNX6UTtJbIOu9qEFbBAy4/jtIkJX+dlNoFwd4GXQW1YNO
37
37
  nA==
38
38
  -----END CERTIFICATE-----
39
- date: 2022-03-31 00:00:00.000000000 Z
39
+ date: 2022-06-20 00:00:00.000000000 Z
40
40
  dependencies: []
41
41
  description: Pg is the Ruby interface to the PostgreSQL RDBMS. It works with PostgreSQL
42
42
  9.3 and later.
@@ -179,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
181
  requirements: []
182
- rubygems_version: 3.2.15
182
+ rubygems_version: 3.3.7
183
183
  signing_key:
184
184
  specification_version: 4
185
185
  summary: Pg is the Ruby interface to the PostgreSQL RDBMS
metadata.gz.sig CHANGED
Binary file