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.
- 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} +302 -115
- 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.rb +0 -0
- data/ext/errorcodes.txt +2 -1
- data/ext/extconf.rb +0 -0
- data/ext/pg.c +14 -54
- data/ext/pg.h +12 -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 +385 -266
- 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 +117 -34
- data/ext/pg_text_decoder.c +28 -10
- data/ext/pg_text_encoder.c +23 -10
- data/ext/pg_tuple.c +36 -39
- 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 +269 -139
- 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/misc/openssl-pg-segfault.rb +0 -0
- data/pg.gemspec +4 -2
- data/rakelib/task_extension.rb +1 -1
- data/sample/array_insert.rb +0 -0
- data/sample/async_api.rb +3 -7
- data/sample/async_copyto.rb +0 -0
- data/sample/async_mixed.rb +0 -0
- data/sample/check_conn.rb +0 -0
- data/sample/copydata.rb +0 -0
- data/sample/copyfrom.rb +0 -0
- data/sample/copyto.rb +0 -0
- data/sample/cursor.rb +0 -0
- data/sample/disk_usage_report.rb +0 -0
- data/sample/issue-119.rb +0 -0
- data/sample/losample.rb +0 -0
- data/sample/minimal-testcase.rb +0 -0
- data/sample/notify_wait.rb +0 -0
- data/sample/pg_statistics.rb +0 -0
- data/sample/replication_monitor.rb +0 -0
- data/sample/test_binary_values.rb +0 -0
- data/sample/wal_shipper.rb +0 -0
- data/sample/warehouse_partitions.rb +0 -0
- 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
|
|
@@ -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
|
-
#
|
624
|
-
|
625
|
-
|
626
|
-
|
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
|
-
|
629
|
-
|
630
|
-
|
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
|
-
|
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
|
701
|
-
conn =
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
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
|
-
#
|
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
|
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
|