pg 1.3.3 → 1.5.3

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 (94) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.appveyor.yml +15 -9
  4. data/.github/workflows/binary-gems.yml +43 -12
  5. data/.github/workflows/source-gem.yml +28 -20
  6. data/.gitignore +11 -2
  7. data/.travis.yml +2 -2
  8. data/{History.rdoc → History.md} +302 -115
  9. data/README.ja.md +276 -0
  10. data/README.md +286 -0
  11. data/Rakefile +15 -6
  12. data/Rakefile.cross +7 -11
  13. data/certs/larskanis-2023.pem +24 -0
  14. data/ext/errorcodes.def +4 -0
  15. data/ext/errorcodes.rb +0 -0
  16. data/ext/errorcodes.txt +2 -1
  17. data/ext/extconf.rb +0 -0
  18. data/ext/pg.c +14 -54
  19. data/ext/pg.h +12 -5
  20. data/ext/pg_binary_decoder.c +80 -1
  21. data/ext/pg_binary_encoder.c +225 -1
  22. data/ext/pg_coder.c +17 -8
  23. data/ext/pg_connection.c +385 -266
  24. data/ext/pg_copy_coder.c +307 -18
  25. data/ext/pg_errors.c +1 -1
  26. data/ext/pg_record_coder.c +12 -9
  27. data/ext/pg_result.c +117 -34
  28. data/ext/pg_text_decoder.c +28 -10
  29. data/ext/pg_text_encoder.c +23 -10
  30. data/ext/pg_tuple.c +36 -39
  31. data/ext/pg_type_map.c +4 -3
  32. data/ext/pg_type_map_all_strings.c +3 -3
  33. data/ext/pg_type_map_by_class.c +6 -4
  34. data/ext/pg_type_map_by_column.c +9 -5
  35. data/ext/pg_type_map_by_mri_type.c +1 -1
  36. data/ext/pg_type_map_by_oid.c +8 -5
  37. data/ext/pg_type_map_in_ruby.c +6 -3
  38. data/lib/pg/basic_type_map_based_on_result.rb +21 -1
  39. data/lib/pg/basic_type_map_for_queries.rb +13 -8
  40. data/lib/pg/basic_type_map_for_results.rb +26 -3
  41. data/lib/pg/basic_type_registry.rb +36 -33
  42. data/lib/pg/binary_decoder/date.rb +9 -0
  43. data/lib/pg/binary_decoder/timestamp.rb +26 -0
  44. data/lib/pg/binary_encoder/timestamp.rb +20 -0
  45. data/lib/pg/coder.rb +15 -13
  46. data/lib/pg/connection.rb +269 -139
  47. data/lib/pg/exceptions.rb +14 -1
  48. data/lib/pg/text_decoder/date.rb +18 -0
  49. data/lib/pg/text_decoder/inet.rb +9 -0
  50. data/lib/pg/text_decoder/json.rb +14 -0
  51. data/lib/pg/text_decoder/numeric.rb +9 -0
  52. data/lib/pg/text_decoder/timestamp.rb +30 -0
  53. data/lib/pg/text_encoder/date.rb +12 -0
  54. data/lib/pg/text_encoder/inet.rb +28 -0
  55. data/lib/pg/text_encoder/json.rb +14 -0
  56. data/lib/pg/text_encoder/numeric.rb +9 -0
  57. data/lib/pg/text_encoder/timestamp.rb +24 -0
  58. data/lib/pg/version.rb +1 -1
  59. data/lib/pg.rb +59 -19
  60. data/misc/openssl-pg-segfault.rb +0 -0
  61. data/pg.gemspec +4 -2
  62. data/rakelib/task_extension.rb +1 -1
  63. data/sample/array_insert.rb +0 -0
  64. data/sample/async_api.rb +3 -7
  65. data/sample/async_copyto.rb +0 -0
  66. data/sample/async_mixed.rb +0 -0
  67. data/sample/check_conn.rb +0 -0
  68. data/sample/copydata.rb +0 -0
  69. data/sample/copyfrom.rb +0 -0
  70. data/sample/copyto.rb +0 -0
  71. data/sample/cursor.rb +0 -0
  72. data/sample/disk_usage_report.rb +0 -0
  73. data/sample/issue-119.rb +0 -0
  74. data/sample/losample.rb +0 -0
  75. data/sample/minimal-testcase.rb +0 -0
  76. data/sample/notify_wait.rb +0 -0
  77. data/sample/pg_statistics.rb +0 -0
  78. data/sample/replication_monitor.rb +0 -0
  79. data/sample/test_binary_values.rb +0 -0
  80. data/sample/wal_shipper.rb +0 -0
  81. data/sample/warehouse_partitions.rb +0 -0
  82. data/translation/.po4a-version +7 -0
  83. data/translation/po/all.pot +910 -0
  84. data/translation/po/ja.po +1047 -0
  85. data/translation/po4a.cfg +12 -0
  86. data.tar.gz.sig +0 -0
  87. metadata +101 -32
  88. metadata.gz.sig +0 -0
  89. data/README.ja.rdoc +0 -13
  90. data/README.rdoc +0 -214
  91. data/lib/pg/binary_decoder.rb +0 -23
  92. data/lib/pg/constants.rb +0 -12
  93. data/lib/pg/text_decoder.rb +0 -46
  94. data/lib/pg/text_encoder.rb +0 -59
data/lib/pg/connection.rb CHANGED
@@ -2,8 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'pg' unless defined?( PG )
5
- require 'uri'
6
- require 'io/wait'
5
+ require 'io/wait' unless ::IO.public_instance_methods(false).include?(:wait_readable)
7
6
  require 'socket'
8
7
 
9
8
  # The PostgreSQL connection class. The interface for this class is based on
@@ -31,8 +30,8 @@ require 'socket'
31
30
  class PG::Connection
32
31
 
33
32
  # The order the options are passed to the ::connect method.
34
- CONNECT_ARGUMENT_ORDER = %w[host port options tty dbname user password]
35
-
33
+ CONNECT_ARGUMENT_ORDER = %w[host port options tty dbname user password].freeze
34
+ private_constant :CONNECT_ARGUMENT_ORDER
36
35
 
37
36
  ### Quote a single +value+ for use in a connection-parameter string.
38
37
  def self.quote_connstr( value )
@@ -46,36 +45,9 @@ class PG::Connection
46
45
  hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
47
46
  end
48
47
 
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/
48
+ # Shareable program name for Ractor
49
+ PROGRAM_NAME = $PROGRAM_NAME.dup.freeze
50
+ private_constant :PROGRAM_NAME
79
51
 
80
52
  # Parse the connection +args+ into a connection-parameter string.
81
53
  # See PG::Connection.new for valid arguments.
@@ -87,90 +59,66 @@ class PG::Connection
87
59
  # * URI object
88
60
  # * positional arguments
89
61
  #
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 )
62
+ # The method adds the option "fallback_application_name" if it isn't already set.
63
+ # It returns a connection string with "key=value" pairs.
64
+ def self.parse_connect_args( *args )
94
65
  hash_arg = args.last.is_a?( Hash ) ? args.pop.transform_keys(&:to_sym) : {}
95
- option_string = ""
96
66
  iopts = {}
97
67
 
98
68
  if args.length == 1
99
- 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 = {}
69
+ case args.first.to_s
70
+ when /=/, /:\/\//
71
+ # Option or URL string style
72
+ conn_string = args.first.to_s
73
+ iopts = PG::Connection.conninfo_parse(conn_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
117
74
  else
118
75
  # Positional parameters (only host given)
119
76
  iopts[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
120
- oopts = iopts.dup
121
77
  end
122
78
  else
123
- # Positional parameters
79
+ # Positional parameters with host and more
124
80
  max = CONNECT_ARGUMENT_ORDER.length
125
81
  raise ArgumentError,
126
- "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
82
+ "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
127
83
 
128
84
  CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
129
85
  iopts[ k.to_sym ] = v if v
130
86
  end
131
87
  iopts.delete(:tty) # ignore obsolete tty parameter
132
- oopts = iopts.dup
133
88
  end
134
89
 
135
90
  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
91
 
160
92
  if !iopts[:fallback_application_name]
161
- oopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
93
+ iopts[:fallback_application_name] = PROGRAM_NAME.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
162
94
  end
163
95
 
164
- if uri
165
- uri += uri_match['query'] ? "&" : "?"
166
- uri += URI.encode_www_form( oopts )
167
- return uri
96
+ return connect_hash_to_string(iopts)
97
+ end
98
+
99
+ # Return a String representation of the object suitable for debugging.
100
+ def inspect
101
+ str = self.to_s
102
+ str[-1,0] = if finished?
103
+ " finished"
168
104
  else
169
- option_string += ' ' unless option_string.empty? && oopts.empty?
170
- return option_string + connect_hash_to_string(oopts)
105
+ stats = []
106
+ stats << " status=#{ PG.constants.grep(/CONNECTION_/).find{|c| PG.const_get(c) == status} }" if status != CONNECTION_OK
107
+ stats << " transaction_status=#{ PG.constants.grep(/PQTRANS_/).find{|c| PG.const_get(c) == transaction_status} }" if transaction_status != PG::PQTRANS_IDLE
108
+ stats << " nonblocking=#{ isnonblocking }" if isnonblocking
109
+ stats << " pipeline_status=#{ PG.constants.grep(/PQ_PIPELINE_/).find{|c| PG.const_get(c) == pipeline_status} }" if respond_to?(:pipeline_status) && pipeline_status != PG::PQ_PIPELINE_OFF
110
+ stats << " client_encoding=#{ get_client_encoding }" if get_client_encoding != "UTF8"
111
+ stats << " type_map_for_results=#{ type_map_for_results.to_s }" unless type_map_for_results.is_a?(PG::TypeMapAllStrings)
112
+ stats << " type_map_for_queries=#{ type_map_for_queries.to_s }" unless type_map_for_queries.is_a?(PG::TypeMapAllStrings)
113
+ stats << " encoder_for_put_copy_data=#{ encoder_for_put_copy_data.to_s }" if encoder_for_put_copy_data
114
+ stats << " decoder_for_get_copy_data=#{ decoder_for_get_copy_data.to_s }" if decoder_for_get_copy_data
115
+ " host=#{host} port=#{port} user=#{user}#{stats.join}"
171
116
  end
117
+ return str
172
118
  end
173
119
 
120
+ BinarySignature = "PGCOPY\n\377\r\n\0".b
121
+ private_constant :BinarySignature
174
122
 
175
123
  # call-seq:
176
124
  # conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
@@ -218,6 +166,14 @@ class PG::Connection
218
166
  # conn.put_copy_data ['more', 'data', 'to', 'copy']
219
167
  # end
220
168
  #
169
+ # Also PG::BinaryEncoder::CopyRow can be used to send data in binary format to the server.
170
+ # In this case copy_data generates the header and trailer data automatically:
171
+ # enco = PG::BinaryEncoder::CopyRow.new
172
+ # conn.copy_data "COPY my_table FROM STDIN (FORMAT binary)", enco do
173
+ # conn.put_copy_data ['some', 'data', 'to', 'copy']
174
+ # conn.put_copy_data ['more', 'data', 'to', 'copy']
175
+ # end
176
+ #
221
177
  # Example with CSV output format:
222
178
  # conn.copy_data "COPY my_table TO STDOUT CSV" do
223
179
  # while row=conn.get_copy_data
@@ -239,26 +195,58 @@ class PG::Connection
239
195
  # This receives all rows of +my_table+ as ruby array:
240
196
  # ["some", "data", "to", "copy"]
241
197
  # ["more", "data", "to", "copy"]
198
+ #
199
+ # Also PG::BinaryDecoder::CopyRow can be used to retrieve data in binary format from the server.
200
+ # In this case the header and trailer data is processed by the decoder and the remaining +nil+ from get_copy_data is processed by copy_data, so that binary data can be processed equally to text data:
201
+ # deco = PG::BinaryDecoder::CopyRow.new
202
+ # conn.copy_data "COPY my_table TO STDOUT (FORMAT binary)", deco do
203
+ # while row=conn.get_copy_data
204
+ # p row
205
+ # end
206
+ # end
207
+ # This receives all rows of +my_table+ as ruby array:
208
+ # ["some", "data", "to", "copy"]
209
+ # ["more", "data", "to", "copy"]
242
210
 
243
211
  def copy_data( sql, coder=nil )
244
- raise PG::NotInBlockingMode, "copy_data can not be used in nonblocking mode" if nonblocking?
212
+ raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
245
213
  res = exec( sql )
246
214
 
247
215
  case res.result_status
248
216
  when PGRES_COPY_IN
249
217
  begin
218
+ if coder && res.binary_tuples == 1
219
+ # Binary file header (11 byte signature, 32 bit flags and 32 bit extension length)
220
+ put_copy_data(BinarySignature + ("\x00" * 8))
221
+ end
222
+
250
223
  if coder
251
224
  old_coder = self.encoder_for_put_copy_data
252
225
  self.encoder_for_put_copy_data = coder
253
226
  end
227
+
254
228
  yield res
255
229
  rescue Exception => err
256
230
  errmsg = "%s while copy data: %s" % [ err.class.name, err.message ]
257
- put_copy_end( errmsg )
258
- get_result
259
- raise
231
+ begin
232
+ put_copy_end( errmsg )
233
+ rescue PG::Error
234
+ # Ignore error in cleanup to avoid losing original exception
235
+ end
236
+ discard_results
237
+ raise err
260
238
  else
261
- put_copy_end
239
+ begin
240
+ self.encoder_for_put_copy_data = old_coder if coder
241
+
242
+ if coder && res.binary_tuples == 1
243
+ put_copy_data("\xFF\xFF") # Binary file trailer 16 bit "-1"
244
+ end
245
+
246
+ put_copy_end
247
+ rescue PG::Error => err
248
+ raise PG::LostCopyState.new("#{err} (probably by executing another SQL query while running a COPY command)", connection: self)
249
+ end
262
250
  get_last_result
263
251
  ensure
264
252
  self.encoder_for_put_copy_data = old_coder if coder
@@ -271,21 +259,26 @@ class PG::Connection
271
259
  self.decoder_for_get_copy_data = coder
272
260
  end
273
261
  yield res
274
- rescue Exception => err
262
+ rescue Exception
275
263
  cancel
276
- while get_copy_data
277
- end
278
- while get_result
279
- end
264
+ discard_results
280
265
  raise
281
266
  else
282
- res = get_last_result
283
- if !res || res.result_status != PGRES_COMMAND_OK
284
- while get_copy_data
285
- end
286
- while get_result
267
+ if coder && res.binary_tuples == 1
268
+ # There are two end markers in binary mode: file trailer and the final nil.
269
+ # The file trailer is expected to be processed by BinaryDecoder::CopyRow and already returns nil, so that the remaining NULL from PQgetCopyData is retrieved here:
270
+ if get_copy_data
271
+ discard_results
272
+ raise PG::NotAllCopyDataRetrieved.new("Not all binary COPY data retrieved", connection: self)
287
273
  end
288
- raise PG::NotAllCopyDataRetrieved, "Not all COPY data retrieved"
274
+ end
275
+ res = get_last_result
276
+ if !res
277
+ discard_results
278
+ raise PG::LostCopyState.new("Lost COPY state (probably by executing another SQL query while running a COPY command)", connection: self)
279
+ elsif res.result_status != PGRES_COMMAND_OK
280
+ discard_results
281
+ raise PG::NotAllCopyDataRetrieved.new("Not all COPY data retrieved", connection: self)
289
282
  end
290
283
  res
291
284
  ensure
@@ -310,16 +303,17 @@ class PG::Connection
310
303
  # and a +COMMIT+ at the end of the block, or
311
304
  # +ROLLBACK+ if any exception occurs.
312
305
  def transaction
306
+ rollback = false
313
307
  exec "BEGIN"
314
- res = yield(self)
308
+ yield(self)
315
309
  rescue Exception
310
+ rollback = true
316
311
  cancel if transaction_status == PG::PQTRANS_ACTIVE
317
312
  block
318
313
  exec "ROLLBACK"
319
314
  raise
320
- else
321
- exec "COMMIT"
322
- res
315
+ ensure
316
+ exec "COMMIT" unless rollback
323
317
  end
324
318
 
325
319
  ### Returns an array of Hashes with connection defaults. See ::conndefaults
@@ -372,6 +366,23 @@ class PG::Connection
372
366
  end
373
367
  end
374
368
 
369
+ # Read all pending socket input to internal memory and raise an exception in case of errors.
370
+ #
371
+ # This verifies that the connection socket is in a usable state and not aborted in any way.
372
+ # No communication is done with the server.
373
+ # Only pending data is read from the socket - the method doesn't wait for any outstanding server answers.
374
+ #
375
+ # Raises a kind of PG::Error if there was an error reading the data or if the socket is in a failure state.
376
+ #
377
+ # The method doesn't verify that the server is still responding.
378
+ # To verify that the communication to the server works, it is recommended to use something like <tt>conn.exec('')</tt> instead.
379
+ def check_socket
380
+ while socket_io.wait_readable(0)
381
+ consume_input
382
+ end
383
+ nil
384
+ end
385
+
375
386
  # call-seq:
376
387
  # conn.get_result() -> PG::Result
377
388
  # conn.get_result() {|pg_result| block }
@@ -482,10 +493,20 @@ class PG::Connection
482
493
  # See also #copy_data.
483
494
  #
484
495
  def put_copy_data(buffer, encoder=nil)
485
- until sync_put_copy_data(buffer, encoder)
486
- flush
496
+ # sync_put_copy_data does a non-blocking attept to flush data.
497
+ until res=sync_put_copy_data(buffer, encoder)
498
+ # It didn't flush immediately and allocation of more buffering memory failed.
499
+ # Wait for all data sent by doing a blocking flush.
500
+ res = flush
487
501
  end
488
- flush
502
+
503
+ # And do a blocking flush every 100 calls.
504
+ # This is to avoid memory bloat, when sending the data is slower than calls to put_copy_data happen.
505
+ if (@calls_to_put_copy_data += 1) > 100
506
+ @calls_to_put_copy_data = 0
507
+ res = flush
508
+ end
509
+ res
489
510
  end
490
511
  alias async_put_copy_data put_copy_data
491
512
 
@@ -505,6 +526,7 @@ class PG::Connection
505
526
  until sync_put_copy_end(*args)
506
527
  flush
507
528
  end
529
+ @calls_to_put_copy_data = 0
508
530
  flush
509
531
  end
510
532
  alias async_put_copy_end put_copy_end
@@ -545,6 +567,7 @@ class PG::Connection
545
567
  def reset
546
568
  reset_start
547
569
  async_connect_or_reset(:reset_poll)
570
+ self
548
571
  end
549
572
  alias async_reset reset
550
573
 
@@ -612,37 +635,72 @@ class PG::Connection
612
635
  alias async_cancel cancel
613
636
 
614
637
  private def async_connect_or_reset(poll_meth)
615
- # Now grab a reference to the underlying socket so we know when the connection is established
616
- socket = socket_io
617
-
618
638
  # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
639
+
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
646
+
619
647
  poll_status = PG::PGRES_POLLING_WRITING
620
648
  until poll_status == PG::PGRES_POLLING_OK ||
621
649
  poll_status == PG::PGRES_POLLING_FAILED
622
650
 
623
- # If the socket needs to read, wait 'til it becomes readable to poll again
624
- case poll_status
625
- when PG::PGRES_POLLING_READING
626
- socket.wait_readable
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)
662
+ end
627
663
 
628
- # ...and the same for when the socket needs to write
629
- when PG::PGRES_POLLING_WRITING
630
- socket.wait_writable
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)
670
+ 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)
673
+ end
674
+ 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)
631
687
  end
632
688
 
633
689
  # Check to see if it's finished or failed yet
634
690
  poll_status = send( poll_meth )
635
691
  end
636
692
 
637
- raise(PG::ConnectionBad, error_message) unless status == PG::CONNECTION_OK
693
+ unless status == PG::CONNECTION_OK
694
+ msg = error_message
695
+ finish
696
+ raise PG::ConnectionBad.new(msg, connection: self)
697
+ end
638
698
 
639
699
  # Set connection to nonblocking to handle all blocking states in ruby.
640
700
  # That way a fiber scheduler is able to handle IO requests.
641
701
  sync_setnonblocking(true)
642
702
  self.flush_data = true
643
703
  set_default_encoding
644
-
645
- self
646
704
  end
647
705
 
648
706
  class << self
@@ -697,13 +755,17 @@ class PG::Connection
697
755
  # connection will have its +client_encoding+ set accordingly.
698
756
  #
699
757
  # Raises a PG::Error if the connection fails.
700
- def new(*args, **kwargs)
701
- conn = self.connect_start(*args, **kwargs ) or
702
- raise(PG::Error, "Unable to create a new connection")
703
-
704
- raise(PG::ConnectionBad, conn.error_message) if conn.status == PG::CONNECTION_BAD
705
-
706
- conn.send(:async_connect_or_reset, :connect_poll)
758
+ def new(*args)
759
+ conn = connect_to_hosts(*args)
760
+
761
+ if block_given?
762
+ begin
763
+ return yield conn
764
+ ensure
765
+ conn.finish
766
+ end
767
+ end
768
+ conn
707
769
  end
708
770
  alias async_connect new
709
771
  alias connect new
@@ -711,12 +773,74 @@ class PG::Connection
711
773
  alias setdb new
712
774
  alias setdblogin new
713
775
 
776
+ private def connect_to_hosts(*args)
777
+ option_string = parse_connect_args(*args)
778
+ iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
779
+ iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
780
+
781
+ if iopts[:hostaddr]
782
+ # hostaddr is provided -> no need to resolve hostnames
783
+
784
+ 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(","))
816
+ else
817
+ # No host given
818
+ end
819
+ conn = self.connect_start(iopts) or
820
+ raise(PG::Error, "Unable to create a new connection")
821
+
822
+ raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
823
+
824
+ conn.send(:async_connect_or_reset, :connect_poll)
825
+ conn
826
+ end
827
+
828
+ private def host_is_named_pipe?(host_string)
829
+ host_string.empty? || host_string.start_with?("/") || # it's UnixSocket?
830
+ host_string.start_with?("@") || # it's UnixSocket in the abstract namespace?
831
+ # it's a path on Windows?
832
+ (RUBY_PLATFORM =~ /mingw|mswin/ && host_string =~ /\A([\/\\]|\w:[\/\\])/)
833
+ end
834
+
714
835
  # call-seq:
715
836
  # PG::Connection.ping(connection_hash) -> Integer
716
837
  # PG::Connection.ping(connection_string) -> Integer
717
838
  # PG::Connection.ping(host, port, options, tty, dbname, login, password) -> Integer
718
839
  #
719
- # Check server status.
840
+ # PQpingParams reports the status of the server.
841
+ #
842
+ # It accepts connection parameters identical to those of PQ::Connection.new .
843
+ # It is not necessary to supply correct user name, password, or database name values to obtain the server status; however, if incorrect values are provided, the server will log a failed connection attempt.
720
844
  #
721
845
  # See PG::Connection.new for a description of the parameters.
722
846
  #
@@ -729,6 +853,8 @@ class PG::Connection
729
853
  # could not establish connection
730
854
  # [+PQPING_NO_ATTEMPT+]
731
855
  # connection not attempted (bad params)
856
+ #
857
+ # See also check_socket for a way to check the connection without doing any server communication.
732
858
  def ping(*args)
733
859
  if Fiber.respond_to?(:scheduler) && Fiber.scheduler
734
860
  # Run PQping in a second thread to avoid blocking of the scheduler.
@@ -740,23 +866,25 @@ class PG::Connection
740
866
  end
741
867
  alias async_ping ping
742
868
 
743
- REDIRECT_CLASS_METHODS = {
869
+ REDIRECT_CLASS_METHODS = PG.make_shareable({
744
870
  :new => [:async_connect, :sync_connect],
745
871
  :connect => [:async_connect, :sync_connect],
746
872
  :open => [:async_connect, :sync_connect],
747
873
  :setdb => [:async_connect, :sync_connect],
748
874
  :setdblogin => [:async_connect, :sync_connect],
749
875
  :ping => [:async_ping, :sync_ping],
750
- }
876
+ })
877
+ private_constant :REDIRECT_CLASS_METHODS
751
878
 
752
879
  # These methods are affected by PQsetnonblocking
753
- REDIRECT_SEND_METHODS = {
880
+ REDIRECT_SEND_METHODS = PG.make_shareable({
754
881
  :isnonblocking => [:async_isnonblocking, :sync_isnonblocking],
755
882
  :nonblocking? => [:async_isnonblocking, :sync_isnonblocking],
756
883
  :put_copy_data => [:async_put_copy_data, :sync_put_copy_data],
757
884
  :put_copy_end => [:async_put_copy_end, :sync_put_copy_end],
758
885
  :flush => [:async_flush, :sync_flush],
759
- }
886
+ })
887
+ private_constant :REDIRECT_SEND_METHODS
760
888
  REDIRECT_METHODS = {
761
889
  :exec => [:async_exec, :sync_exec],
762
890
  :query => [:async_exec, :sync_exec],
@@ -774,12 +902,14 @@ class PG::Connection
774
902
  :client_encoding= => [:async_set_client_encoding, :sync_set_client_encoding],
775
903
  :cancel => [:async_cancel, :sync_cancel],
776
904
  }
905
+ private_constant :REDIRECT_METHODS
777
906
 
778
907
  if PG::Connection.instance_methods.include? :async_encrypt_password
779
908
  REDIRECT_METHODS.merge!({
780
909
  :encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
781
910
  })
782
911
  end
912
+ PG.make_shareable(REDIRECT_METHODS)
783
913
 
784
914
  def async_send_api=(enable)
785
915
  REDIRECT_SEND_METHODS.each do |ali, (async, sync)|
data/lib/pg/exceptions.rb CHANGED
@@ -6,7 +6,20 @@ 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=nil, connection: nil, result: nil)
11
+ @connection = connection
12
+ @result = result
13
+ super(msg)
14
+ end
15
+ end
16
+
17
+ class NotAllCopyDataRetrieved < PG::Error
18
+ end
19
+ class LostCopyState < PG::Error
20
+ end
21
+ class NotInBlockingMode < PG::Error
22
+ end
10
23
 
11
24
  end # module PG
12
25
 
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'date'
5
+
6
+ module PG
7
+ module TextDecoder
8
+ class Date < SimpleDecoder
9
+ def decode(string, tuple=nil, field=nil)
10
+ if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/
11
+ ::Date.new $1.to_i, $2.to_i, $3.to_i
12
+ else
13
+ string
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end # module PG
@@ -0,0 +1,9 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ module PG
5
+ module TextDecoder
6
+ # Init C part of the decoder
7
+ init_inet
8
+ end
9
+ end # module PG