pg 1.3.4 → 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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.appveyor.yml +15 -9
- data/.github/workflows/binary-gems.yml +43 -12
- data/.github/workflows/source-gem.yml +28 -20
- data/.gitignore +11 -2
- data/.travis.yml +2 -2
- data/{History.rdoc → History.md} +293 -122
- data/README.ja.md +276 -0
- data/README.md +286 -0
- data/Rakefile +15 -6
- data/Rakefile.cross +7 -11
- data/certs/larskanis-2023.pem +24 -0
- data/ext/errorcodes.def +4 -0
- data/ext/errorcodes.txt +2 -1
- data/ext/pg.c +14 -54
- data/ext/pg.h +11 -5
- data/ext/pg_binary_decoder.c +80 -1
- data/ext/pg_binary_encoder.c +225 -1
- data/ext/pg_coder.c +17 -8
- data/ext/pg_connection.c +380 -250
- data/ext/pg_copy_coder.c +307 -18
- data/ext/pg_errors.c +1 -1
- data/ext/pg_record_coder.c +12 -9
- data/ext/pg_result.c +104 -27
- data/ext/pg_text_decoder.c +28 -10
- data/ext/pg_text_encoder.c +23 -10
- data/ext/pg_tuple.c +35 -32
- data/ext/pg_type_map.c +4 -3
- data/ext/pg_type_map_all_strings.c +3 -3
- data/ext/pg_type_map_by_class.c +6 -4
- data/ext/pg_type_map_by_column.c +9 -5
- data/ext/pg_type_map_by_mri_type.c +1 -1
- data/ext/pg_type_map_by_oid.c +8 -5
- data/ext/pg_type_map_in_ruby.c +6 -3
- data/lib/pg/basic_type_map_based_on_result.rb +21 -1
- data/lib/pg/basic_type_map_for_queries.rb +13 -8
- data/lib/pg/basic_type_map_for_results.rb +26 -3
- data/lib/pg/basic_type_registry.rb +36 -33
- data/lib/pg/binary_decoder/date.rb +9 -0
- data/lib/pg/binary_decoder/timestamp.rb +26 -0
- data/lib/pg/binary_encoder/timestamp.rb +20 -0
- data/lib/pg/coder.rb +15 -13
- data/lib/pg/connection.rb +265 -136
- data/lib/pg/exceptions.rb +14 -1
- data/lib/pg/text_decoder/date.rb +18 -0
- data/lib/pg/text_decoder/inet.rb +9 -0
- data/lib/pg/text_decoder/json.rb +14 -0
- data/lib/pg/text_decoder/numeric.rb +9 -0
- data/lib/pg/text_decoder/timestamp.rb +30 -0
- data/lib/pg/text_encoder/date.rb +12 -0
- data/lib/pg/text_encoder/inet.rb +28 -0
- data/lib/pg/text_encoder/json.rb +14 -0
- data/lib/pg/text_encoder/numeric.rb +9 -0
- data/lib/pg/text_encoder/timestamp.rb +24 -0
- data/lib/pg/version.rb +1 -1
- data/lib/pg.rb +59 -19
- data/pg.gemspec +4 -2
- data/rakelib/task_extension.rb +1 -1
- data/translation/.po4a-version +7 -0
- data/translation/po/all.pot +910 -0
- data/translation/po/ja.po +1047 -0
- data/translation/po4a.cfg +12 -0
- data.tar.gz.sig +0 -0
- metadata +101 -32
- metadata.gz.sig +0 -0
- data/README.ja.rdoc +0 -13
- data/README.rdoc +0 -214
- data/lib/pg/binary_decoder.rb +0 -23
- data/lib/pg/constants.rb +0 -12
- data/lib/pg/text_decoder.rb +0 -46
- 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 '
|
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
|
-
#
|
50
|
-
|
51
|
-
|
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 "
|
91
|
-
#
|
92
|
-
|
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
|
101
|
-
|
102
|
-
|
103
|
-
if
|
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
|
-
|
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
|
-
|
93
|
+
iopts[:fallback_application_name] = PROGRAM_NAME.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
|
162
94
|
end
|
163
95
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
|
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
|
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
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
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
|
262
|
+
rescue Exception
|
275
263
|
cancel
|
276
|
-
|
277
|
-
end
|
278
|
-
while get_result
|
279
|
-
end
|
264
|
+
discard_results
|
280
265
|
raise
|
281
266
|
else
|
282
|
-
res
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
486
|
-
|
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
|
-
|
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
|
|
@@ -613,18 +636,54 @@ class PG::Connection
|
|
613
636
|
|
614
637
|
private def async_connect_or_reset(poll_meth)
|
615
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
|
+
|
616
647
|
poll_status = PG::PGRES_POLLING_WRITING
|
617
648
|
until poll_status == PG::PGRES_POLLING_OK ||
|
618
649
|
poll_status == PG::PGRES_POLLING_FAILED
|
619
650
|
|
620
|
-
#
|
621
|
-
|
622
|
-
|
623
|
-
|
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
|
624
663
|
|
625
|
-
|
626
|
-
|
627
|
-
|
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)
|
628
687
|
end
|
629
688
|
|
630
689
|
# Check to see if it's finished or failed yet
|
@@ -634,7 +693,7 @@ class PG::Connection
|
|
634
693
|
unless status == PG::CONNECTION_OK
|
635
694
|
msg = error_message
|
636
695
|
finish
|
637
|
-
raise PG::ConnectionBad,
|
696
|
+
raise PG::ConnectionBad.new(msg, connection: self)
|
638
697
|
end
|
639
698
|
|
640
699
|
# Set connection to nonblocking to handle all blocking states in ruby.
|
@@ -642,8 +701,6 @@ class PG::Connection
|
|
642
701
|
sync_setnonblocking(true)
|
643
702
|
self.flush_data = true
|
644
703
|
set_default_encoding
|
645
|
-
|
646
|
-
self
|
647
704
|
end
|
648
705
|
|
649
706
|
class << self
|
@@ -698,13 +755,17 @@ class PG::Connection
|
|
698
755
|
# connection will have its +client_encoding+ set accordingly.
|
699
756
|
#
|
700
757
|
# Raises a PG::Error if the connection fails.
|
701
|
-
def new(*args
|
702
|
-
conn =
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
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
|
708
769
|
end
|
709
770
|
alias async_connect new
|
710
771
|
alias connect new
|
@@ -712,12 +773,74 @@ class PG::Connection
|
|
712
773
|
alias setdb new
|
713
774
|
alias setdblogin new
|
714
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
|
+
|
715
835
|
# call-seq:
|
716
836
|
# PG::Connection.ping(connection_hash) -> Integer
|
717
837
|
# PG::Connection.ping(connection_string) -> Integer
|
718
838
|
# PG::Connection.ping(host, port, options, tty, dbname, login, password) -> Integer
|
719
839
|
#
|
720
|
-
#
|
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.
|
721
844
|
#
|
722
845
|
# See PG::Connection.new for a description of the parameters.
|
723
846
|
#
|
@@ -730,6 +853,8 @@ class PG::Connection
|
|
730
853
|
# could not establish connection
|
731
854
|
# [+PQPING_NO_ATTEMPT+]
|
732
855
|
# connection not attempted (bad params)
|
856
|
+
#
|
857
|
+
# See also check_socket for a way to check the connection without doing any server communication.
|
733
858
|
def ping(*args)
|
734
859
|
if Fiber.respond_to?(:scheduler) && Fiber.scheduler
|
735
860
|
# Run PQping in a second thread to avoid blocking of the scheduler.
|
@@ -741,23 +866,25 @@ class PG::Connection
|
|
741
866
|
end
|
742
867
|
alias async_ping ping
|
743
868
|
|
744
|
-
REDIRECT_CLASS_METHODS = {
|
869
|
+
REDIRECT_CLASS_METHODS = PG.make_shareable({
|
745
870
|
:new => [:async_connect, :sync_connect],
|
746
871
|
:connect => [:async_connect, :sync_connect],
|
747
872
|
:open => [:async_connect, :sync_connect],
|
748
873
|
:setdb => [:async_connect, :sync_connect],
|
749
874
|
:setdblogin => [:async_connect, :sync_connect],
|
750
875
|
:ping => [:async_ping, :sync_ping],
|
751
|
-
}
|
876
|
+
})
|
877
|
+
private_constant :REDIRECT_CLASS_METHODS
|
752
878
|
|
753
879
|
# These methods are affected by PQsetnonblocking
|
754
|
-
REDIRECT_SEND_METHODS = {
|
880
|
+
REDIRECT_SEND_METHODS = PG.make_shareable({
|
755
881
|
:isnonblocking => [:async_isnonblocking, :sync_isnonblocking],
|
756
882
|
:nonblocking? => [:async_isnonblocking, :sync_isnonblocking],
|
757
883
|
:put_copy_data => [:async_put_copy_data, :sync_put_copy_data],
|
758
884
|
:put_copy_end => [:async_put_copy_end, :sync_put_copy_end],
|
759
885
|
:flush => [:async_flush, :sync_flush],
|
760
|
-
}
|
886
|
+
})
|
887
|
+
private_constant :REDIRECT_SEND_METHODS
|
761
888
|
REDIRECT_METHODS = {
|
762
889
|
:exec => [:async_exec, :sync_exec],
|
763
890
|
:query => [:async_exec, :sync_exec],
|
@@ -775,12 +902,14 @@ class PG::Connection
|
|
775
902
|
:client_encoding= => [:async_set_client_encoding, :sync_set_client_encoding],
|
776
903
|
:cancel => [:async_cancel, :sync_cancel],
|
777
904
|
}
|
905
|
+
private_constant :REDIRECT_METHODS
|
778
906
|
|
779
907
|
if PG::Connection.instance_methods.include? :async_encrypt_password
|
780
908
|
REDIRECT_METHODS.merge!({
|
781
909
|
:encrypt_password => [:async_encrypt_password, :sync_encrypt_password],
|
782
910
|
})
|
783
911
|
end
|
912
|
+
PG.make_shareable(REDIRECT_METHODS)
|
784
913
|
|
785
914
|
def async_send_api=(enable)
|
786
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
|
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,14 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module PG
|
7
|
+
module TextDecoder
|
8
|
+
class JSON < SimpleDecoder
|
9
|
+
def decode(string, tuple=nil, field=nil)
|
10
|
+
::JSON.parse(string, quirks_mode: true)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end # module PG
|