pg 1.3.5 → 1.4.5
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/.github/workflows/binary-gems.yml +2 -2
- data/.github/workflows/source-gem.yml +10 -8
- data/History.rdoc +65 -0
- data/README.rdoc +24 -5
- data/Rakefile +3 -3
- data/Rakefile.cross +2 -2
- data/ext/errorcodes.def +4 -0
- data/ext/errorcodes.txt +2 -1
- data/ext/extconf.rb +0 -31
- data/ext/pg.c +4 -26
- data/ext/pg.h +1 -0
- data/ext/pg_binary_decoder.c +1 -1
- data/ext/pg_binary_encoder.c +1 -1
- data/ext/pg_coder.c +1 -1
- data/ext/pg_connection.c +212 -181
- data/ext/pg_copy_coder.c +1 -1
- data/ext/pg_errors.c +1 -1
- data/ext/pg_record_coder.c +1 -1
- data/ext/pg_result.c +13 -11
- data/ext/pg_text_decoder.c +1 -1
- data/ext/pg_text_encoder.c +1 -1
- data/ext/pg_tuple.c +1 -1
- data/ext/pg_type_map.c +1 -1
- data/ext/pg_type_map_all_strings.c +1 -1
- data/ext/pg_type_map_by_class.c +1 -1
- data/ext/pg_type_map_by_column.c +2 -2
- data/ext/pg_type_map_by_mri_type.c +1 -1
- data/ext/pg_type_map_by_oid.c +1 -1
- data/ext/pg_type_map_in_ruby.c +1 -1
- data/lib/pg/connection.rb +172 -116
- data/lib/pg/exceptions.rb +7 -1
- data/lib/pg/version.rb +1 -1
- data/lib/pg.rb +4 -4
- data.tar.gz.sig +0 -0
- metadata +21 -27
- metadata.gz.sig +0 -0
data/ext/pg_copy_coder.c
CHANGED
data/ext/pg_errors.c
CHANGED
data/ext/pg_record_coder.c
CHANGED
data/ext/pg_result.c
CHANGED
@@ -1382,21 +1382,20 @@ pgresult_type_map_get(VALUE self)
|
|
1382
1382
|
}
|
1383
1383
|
|
1384
1384
|
|
1385
|
-
static
|
1385
|
+
static int
|
1386
1386
|
yield_hash(VALUE self, int ntuples, int nfields, void *data)
|
1387
1387
|
{
|
1388
1388
|
int tuple_num;
|
1389
|
-
t_pg_result *this = pgresult_get_this(self);
|
1390
1389
|
UNUSED(nfields);
|
1391
1390
|
|
1392
1391
|
for(tuple_num = 0; tuple_num < ntuples; tuple_num++) {
|
1393
1392
|
rb_yield(pgresult_aref(self, INT2NUM(tuple_num)));
|
1394
1393
|
}
|
1395
1394
|
|
1396
|
-
|
1395
|
+
return 1; /* clear the result */
|
1397
1396
|
}
|
1398
1397
|
|
1399
|
-
static
|
1398
|
+
static int
|
1400
1399
|
yield_array(VALUE self, int ntuples, int nfields, void *data)
|
1401
1400
|
{
|
1402
1401
|
int row;
|
@@ -1413,10 +1412,10 @@ yield_array(VALUE self, int ntuples, int nfields, void *data)
|
|
1413
1412
|
rb_yield( rb_ary_new4( nfields, row_values ));
|
1414
1413
|
}
|
1415
1414
|
|
1416
|
-
|
1415
|
+
return 1; /* clear the result */
|
1417
1416
|
}
|
1418
1417
|
|
1419
|
-
static
|
1418
|
+
static int
|
1420
1419
|
yield_tuple(VALUE self, int ntuples, int nfields, void *data)
|
1421
1420
|
{
|
1422
1421
|
int tuple_num;
|
@@ -1434,11 +1433,12 @@ yield_tuple(VALUE self, int ntuples, int nfields, void *data)
|
|
1434
1433
|
VALUE tuple = pgresult_tuple(copy, INT2FIX(tuple_num));
|
1435
1434
|
rb_yield( tuple );
|
1436
1435
|
}
|
1436
|
+
return 0; /* don't clear the result */
|
1437
1437
|
}
|
1438
1438
|
|
1439
1439
|
/* Non-static, and data pointer for use by sequel_pg */
|
1440
1440
|
VALUE
|
1441
|
-
pgresult_stream_any(VALUE self,
|
1441
|
+
pgresult_stream_any(VALUE self, int (*yielder)(VALUE, int, int, void*), void* data)
|
1442
1442
|
{
|
1443
1443
|
t_pg_result *this;
|
1444
1444
|
int nfields;
|
@@ -1467,7 +1467,9 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* d
|
|
1467
1467
|
pg_result_check( self );
|
1468
1468
|
}
|
1469
1469
|
|
1470
|
-
yielder( self, ntuples, nfields, data )
|
1470
|
+
if( yielder( self, ntuples, nfields, data ) ){
|
1471
|
+
pgresult_clear( this );
|
1472
|
+
}
|
1471
1473
|
|
1472
1474
|
if( gvl_PQisBusy(pgconn) ){
|
1473
1475
|
/* wait for input (without blocking) before reading each result */
|
@@ -1476,10 +1478,10 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* d
|
|
1476
1478
|
|
1477
1479
|
pgresult = gvl_PQgetResult(pgconn);
|
1478
1480
|
if( pgresult == NULL )
|
1479
|
-
rb_raise( rb_eNoResultError, "no result received - possibly an intersection with another
|
1481
|
+
rb_raise( rb_eNoResultError, "no result received - possibly an intersection with another query");
|
1480
1482
|
|
1481
1483
|
if( nfields != PQnfields(pgresult) )
|
1482
|
-
rb_raise( rb_eInvalidChangeOfResultFields, "number of fields
|
1484
|
+
rb_raise( rb_eInvalidChangeOfResultFields, "number of fields changed in single row mode from %d to %d - this is a sign for intersection with another query", nfields, PQnfields(pgresult));
|
1483
1485
|
|
1484
1486
|
this->pgresult = pgresult;
|
1485
1487
|
}
|
@@ -1617,7 +1619,7 @@ pgresult_field_name_type_get(VALUE self)
|
|
1617
1619
|
}
|
1618
1620
|
|
1619
1621
|
void
|
1620
|
-
init_pg_result()
|
1622
|
+
init_pg_result(void)
|
1621
1623
|
{
|
1622
1624
|
sym_string = ID2SYM(rb_intern("string"));
|
1623
1625
|
sym_symbol = ID2SYM(rb_intern("symbol"));
|
data/ext/pg_text_decoder.c
CHANGED
@@ -923,7 +923,7 @@ pg_text_dec_inet(t_pg_coder *conv, const char *val, int len, int tuple, int fiel
|
|
923
923
|
}
|
924
924
|
|
925
925
|
void
|
926
|
-
init_pg_text_decoder()
|
926
|
+
init_pg_text_decoder(void)
|
927
927
|
{
|
928
928
|
rb_require("ipaddr");
|
929
929
|
s_IPAddr = rb_funcall(rb_cObject, rb_intern("const_get"), 1, rb_str_new2("IPAddr"));
|
data/ext/pg_text_encoder.c
CHANGED
data/ext/pg_tuple.c
CHANGED
data/ext/pg_type_map.c
CHANGED
data/ext/pg_type_map_by_class.c
CHANGED
data/ext/pg_type_map_by_column.c
CHANGED
@@ -243,7 +243,7 @@ pg_tmbc_s_allocate( VALUE klass )
|
|
243
243
|
}
|
244
244
|
|
245
245
|
VALUE
|
246
|
-
pg_tmbc_allocate()
|
246
|
+
pg_tmbc_allocate(void)
|
247
247
|
{
|
248
248
|
return pg_tmbc_s_allocate(rb_cTypeMapByColumn);
|
249
249
|
}
|
@@ -320,7 +320,7 @@ pg_tmbc_coders(VALUE self)
|
|
320
320
|
}
|
321
321
|
|
322
322
|
void
|
323
|
-
init_pg_type_map_by_column()
|
323
|
+
init_pg_type_map_by_column(void)
|
324
324
|
{
|
325
325
|
s_id_decode = rb_intern("decode");
|
326
326
|
s_id_encode = rb_intern("encode");
|
data/ext/pg_type_map_by_oid.c
CHANGED
data/ext/pg_type_map_in_ruby.c
CHANGED
data/lib/pg/connection.rb
CHANGED
@@ -46,37 +46,6 @@ class PG::Connection
|
|
46
46
|
hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
|
47
47
|
end
|
48
48
|
|
49
|
-
# Decode a connection string to Hash options
|
50
|
-
#
|
51
|
-
# Value are properly unquoted and unescaped.
|
52
|
-
def self.connect_string_to_hash( str )
|
53
|
-
options = {}
|
54
|
-
key = nil
|
55
|
-
value = String.new
|
56
|
-
str.scan(/\G\s*(?>([^\s\\\']+)\s*=\s*|([^\s\\\']+)|'((?:[^\'\\]|\\.)*)'|(\\.?)|(\S))(\s|\z)?/m) do
|
57
|
-
|k, word, sq, esc, garbage, sep|
|
58
|
-
raise ArgumentError, "unterminated quoted string in connection info string: #{str.inspect}" if garbage
|
59
|
-
if k
|
60
|
-
key = k
|
61
|
-
else
|
62
|
-
value << (word || (sq || esc).gsub(/\\(.)/, '\\1'))
|
63
|
-
end
|
64
|
-
if sep
|
65
|
-
raise ArgumentError, "missing = after #{value.inspect}" unless key
|
66
|
-
options[key.to_sym] = value
|
67
|
-
key = nil
|
68
|
-
value = String.new
|
69
|
-
end
|
70
|
-
end
|
71
|
-
options
|
72
|
-
end
|
73
|
-
|
74
|
-
# URI defined in RFC3986
|
75
|
-
# This regexp is modified to allow host to specify multiple comma separated components captured as <hostports> and to disallow comma in hostnames.
|
76
|
-
# Taken from: https://github.com/ruby/ruby/blob/be04006c7d2f9aeb7e9d8d09d945b3a9c7850202/lib/uri/rfc3986_parser.rb#L6
|
77
|
-
HOST_AND_PORT = /(?<hostport>(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[-\.!$&-+0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)/
|
78
|
-
POSTGRESQL_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<hostports>#{HOST_AND_PORT}(?:,\g<hostport>)*))(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
|
79
|
-
|
80
49
|
# Parse the connection +args+ into a connection-parameter string.
|
81
50
|
# See PG::Connection.new for valid arguments.
|
82
51
|
#
|
@@ -87,91 +56,64 @@ class PG::Connection
|
|
87
56
|
# * URI object
|
88
57
|
# * positional arguments
|
89
58
|
#
|
90
|
-
# The method adds the option "
|
91
|
-
#
|
92
|
-
|
93
|
-
def self::parse_connect_args( *args )
|
59
|
+
# The method adds the option "fallback_application_name" if it isn't already set.
|
60
|
+
# It returns a connection string with "key=value" pairs.
|
61
|
+
def self.parse_connect_args( *args )
|
94
62
|
hash_arg = args.last.is_a?( Hash ) ? args.pop.transform_keys(&:to_sym) : {}
|
95
|
-
option_string = ""
|
96
63
|
iopts = {}
|
97
64
|
|
98
65
|
if args.length == 1
|
99
66
|
case args.first
|
100
|
-
when URI,
|
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 = {}
|
67
|
+
when URI, /=/, /:\/\//
|
68
|
+
# Option or URL string style
|
69
|
+
conn_string = args.first.to_s
|
70
|
+
iopts = PG::Connection.conninfo_parse(conn_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
|
117
71
|
else
|
118
72
|
# Positional parameters (only host given)
|
119
73
|
iopts[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
|
120
|
-
oopts = iopts.dup
|
121
74
|
end
|
122
75
|
else
|
123
|
-
# Positional parameters
|
76
|
+
# Positional parameters with host and more
|
124
77
|
max = CONNECT_ARGUMENT_ORDER.length
|
125
78
|
raise ArgumentError,
|
126
|
-
|
79
|
+
"Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
|
127
80
|
|
128
81
|
CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
|
129
82
|
iopts[ k.to_sym ] = v if v
|
130
83
|
end
|
131
84
|
iopts.delete(:tty) # ignore obsolete tty parameter
|
132
|
-
oopts = iopts.dup
|
133
85
|
end
|
134
86
|
|
135
87
|
iopts.merge!( hash_arg )
|
136
|
-
oopts.merge!( hash_arg )
|
137
|
-
|
138
|
-
# Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
|
139
|
-
if (host=iopts[:host]) && !iopts[:hostaddr]
|
140
|
-
hostaddrs = host.split(",", -1).map do |mhost|
|
141
|
-
if !mhost.empty? && !mhost.start_with?("/") && # isn't UnixSocket
|
142
|
-
# isn't a path on Windows
|
143
|
-
(RUBY_PLATFORM !~ /mingw|mswin/ || mhost !~ /\A\w:[\/\\]/)
|
144
|
-
|
145
|
-
if Fiber.respond_to?(:scheduler) &&
|
146
|
-
Fiber.scheduler &&
|
147
|
-
RUBY_VERSION < '3.1.'
|
148
|
-
|
149
|
-
# Use a second thread to avoid blocking of the scheduler.
|
150
|
-
# `IPSocket.getaddress` isn't fiber aware before ruby-3.1.
|
151
|
-
Thread.new{ IPSocket.getaddress(mhost) rescue '' }.value
|
152
|
-
else
|
153
|
-
IPSocket.getaddress(mhost) rescue ''
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
oopts[:hostaddr] = hostaddrs.join(",") if hostaddrs.any?
|
158
|
-
end
|
159
88
|
|
160
89
|
if !iopts[:fallback_application_name]
|
161
|
-
|
90
|
+
iopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
|
162
91
|
end
|
163
92
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
93
|
+
return connect_hash_to_string(iopts)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return a String representation of the object suitable for debugging.
|
97
|
+
def inspect
|
98
|
+
str = self.to_s
|
99
|
+
str[-1,0] = if finished?
|
100
|
+
" finished"
|
168
101
|
else
|
169
|
-
|
170
|
-
|
102
|
+
stats = []
|
103
|
+
stats << " status=#{ PG.constants.grep(/CONNECTION_/).find{|c| PG.const_get(c) == status} }" if status != CONNECTION_OK
|
104
|
+
stats << " transaction_status=#{ PG.constants.grep(/PQTRANS_/).find{|c| PG.const_get(c) == transaction_status} }" if transaction_status != PG::PQTRANS_IDLE
|
105
|
+
stats << " nonblocking=#{ isnonblocking }" if isnonblocking
|
106
|
+
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
|
107
|
+
stats << " client_encoding=#{ get_client_encoding }" if get_client_encoding != "UTF8"
|
108
|
+
stats << " type_map_for_results=#{ type_map_for_results.to_s }" unless type_map_for_results.is_a?(PG::TypeMapAllStrings)
|
109
|
+
stats << " type_map_for_queries=#{ type_map_for_queries.to_s }" unless type_map_for_queries.is_a?(PG::TypeMapAllStrings)
|
110
|
+
stats << " encoder_for_put_copy_data=#{ encoder_for_put_copy_data.to_s }" if encoder_for_put_copy_data
|
111
|
+
stats << " decoder_for_get_copy_data=#{ decoder_for_get_copy_data.to_s }" if decoder_for_get_copy_data
|
112
|
+
" host=#{host} port=#{port} user=#{user}#{stats.join}"
|
171
113
|
end
|
114
|
+
return str
|
172
115
|
end
|
173
116
|
|
174
|
-
|
175
117
|
# call-seq:
|
176
118
|
# conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
|
177
119
|
#
|
@@ -241,7 +183,7 @@ class PG::Connection
|
|
241
183
|
# ["more", "data", "to", "copy"]
|
242
184
|
|
243
185
|
def copy_data( sql, coder=nil )
|
244
|
-
raise PG::NotInBlockingMode
|
186
|
+
raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
|
245
187
|
res = exec( sql )
|
246
188
|
|
247
189
|
case res.result_status
|
@@ -273,11 +215,15 @@ class PG::Connection
|
|
273
215
|
yield res
|
274
216
|
rescue Exception => err
|
275
217
|
cancel
|
276
|
-
|
218
|
+
begin
|
219
|
+
while get_copy_data
|
220
|
+
end
|
221
|
+
rescue PG::Error
|
222
|
+
# Ignore error in cleanup to avoid losing original exception
|
277
223
|
end
|
278
224
|
while get_result
|
279
225
|
end
|
280
|
-
raise
|
226
|
+
raise err
|
281
227
|
else
|
282
228
|
res = get_last_result
|
283
229
|
if !res || res.result_status != PGRES_COMMAND_OK
|
@@ -285,7 +231,7 @@ class PG::Connection
|
|
285
231
|
end
|
286
232
|
while get_result
|
287
233
|
end
|
288
|
-
raise PG::NotAllCopyDataRetrieved
|
234
|
+
raise PG::NotAllCopyDataRetrieved.new("Not all COPY data retrieved", connection: self)
|
289
235
|
end
|
290
236
|
res
|
291
237
|
ensure
|
@@ -310,16 +256,17 @@ class PG::Connection
|
|
310
256
|
# and a +COMMIT+ at the end of the block, or
|
311
257
|
# +ROLLBACK+ if any exception occurs.
|
312
258
|
def transaction
|
259
|
+
rollback = false
|
313
260
|
exec "BEGIN"
|
314
|
-
|
261
|
+
yield(self)
|
315
262
|
rescue Exception
|
263
|
+
rollback = true
|
316
264
|
cancel if transaction_status == PG::PQTRANS_ACTIVE
|
317
265
|
block
|
318
266
|
exec "ROLLBACK"
|
319
267
|
raise
|
320
|
-
|
321
|
-
exec "COMMIT"
|
322
|
-
res
|
268
|
+
ensure
|
269
|
+
exec "COMMIT" unless rollback
|
323
270
|
end
|
324
271
|
|
325
272
|
### Returns an array of Hashes with connection defaults. See ::conndefaults
|
@@ -482,10 +429,20 @@ class PG::Connection
|
|
482
429
|
# See also #copy_data.
|
483
430
|
#
|
484
431
|
def put_copy_data(buffer, encoder=nil)
|
485
|
-
|
486
|
-
|
432
|
+
# sync_put_copy_data does a non-blocking attept to flush data.
|
433
|
+
until res=sync_put_copy_data(buffer, encoder)
|
434
|
+
# It didn't flush immediately and allocation of more buffering memory failed.
|
435
|
+
# Wait for all data sent by doing a blocking flush.
|
436
|
+
res = flush
|
487
437
|
end
|
488
|
-
|
438
|
+
|
439
|
+
# And do a blocking flush every 100 calls.
|
440
|
+
# This is to avoid memory bloat, when sending the data is slower than calls to put_copy_data happen.
|
441
|
+
if (@calls_to_put_copy_data += 1) > 100
|
442
|
+
@calls_to_put_copy_data = 0
|
443
|
+
res = flush
|
444
|
+
end
|
445
|
+
res
|
489
446
|
end
|
490
447
|
alias async_put_copy_data put_copy_data
|
491
448
|
|
@@ -505,6 +462,7 @@ class PG::Connection
|
|
505
462
|
until sync_put_copy_end(*args)
|
506
463
|
flush
|
507
464
|
end
|
465
|
+
@calls_to_put_copy_data = 0
|
508
466
|
flush
|
509
467
|
end
|
510
468
|
alias async_put_copy_end put_copy_end
|
@@ -545,6 +503,7 @@ class PG::Connection
|
|
545
503
|
def reset
|
546
504
|
reset_start
|
547
505
|
async_connect_or_reset(:reset_poll)
|
506
|
+
self
|
548
507
|
end
|
549
508
|
alias async_reset reset
|
550
509
|
|
@@ -613,18 +572,54 @@ class PG::Connection
|
|
613
572
|
|
614
573
|
private def async_connect_or_reset(poll_meth)
|
615
574
|
# Track the progress of the connection, waiting for the socket to become readable/writable before polling it
|
575
|
+
|
576
|
+
if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
|
577
|
+
# Lowest timeout is 2 seconds - like in libpq
|
578
|
+
timeo = [timeo, 2].max
|
579
|
+
host_count = conninfo_hash[:host].to_s.count(",") + 1
|
580
|
+
stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
581
|
+
end
|
582
|
+
|
616
583
|
poll_status = PG::PGRES_POLLING_WRITING
|
617
584
|
until poll_status == PG::PGRES_POLLING_OK ||
|
618
585
|
poll_status == PG::PGRES_POLLING_FAILED
|
619
586
|
|
620
|
-
#
|
621
|
-
|
622
|
-
|
623
|
-
|
587
|
+
# Set single timeout to parameter "connect_timeout" but
|
588
|
+
# don't exceed total connection time of number-of-hosts * connect_timeout.
|
589
|
+
timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
|
590
|
+
event = if !timeout || timeout >= 0
|
591
|
+
# If the socket needs to read, wait 'til it becomes readable to poll again
|
592
|
+
case poll_status
|
593
|
+
when PG::PGRES_POLLING_READING
|
594
|
+
if defined?(IO::READABLE) # ruby-3.0+
|
595
|
+
socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
|
596
|
+
else
|
597
|
+
IO.select([socket_io], nil, [socket_io], timeout)
|
598
|
+
end
|
624
599
|
|
625
|
-
|
626
|
-
|
627
|
-
|
600
|
+
# ...and the same for when the socket needs to write
|
601
|
+
when PG::PGRES_POLLING_WRITING
|
602
|
+
if defined?(IO::WRITABLE) # ruby-3.0+
|
603
|
+
# Use wait instead of wait_readable, since connection errors are delivered as
|
604
|
+
# exceptional/priority events on Windows.
|
605
|
+
socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
|
606
|
+
else
|
607
|
+
# io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
|
608
|
+
IO.select(nil, [socket_io], [socket_io], timeout)
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
# connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
|
613
|
+
# connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
|
614
|
+
unless event
|
615
|
+
if self.class.send(:host_is_named_pipe?, host)
|
616
|
+
connhost = "on socket \"#{host}\""
|
617
|
+
elsif respond_to?(:hostaddr)
|
618
|
+
connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
|
619
|
+
else
|
620
|
+
connhost = "at \"#{host}\", port #{port}"
|
621
|
+
end
|
622
|
+
raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
|
628
623
|
end
|
629
624
|
|
630
625
|
# Check to see if it's finished or failed yet
|
@@ -634,7 +629,7 @@ class PG::Connection
|
|
634
629
|
unless status == PG::CONNECTION_OK
|
635
630
|
msg = error_message
|
636
631
|
finish
|
637
|
-
raise PG::ConnectionBad,
|
632
|
+
raise PG::ConnectionBad.new(msg, connection: self)
|
638
633
|
end
|
639
634
|
|
640
635
|
# Set connection to nonblocking to handle all blocking states in ruby.
|
@@ -642,8 +637,6 @@ class PG::Connection
|
|
642
637
|
sync_setnonblocking(true)
|
643
638
|
self.flush_data = true
|
644
639
|
set_default_encoding
|
645
|
-
|
646
|
-
self
|
647
640
|
end
|
648
641
|
|
649
642
|
class << self
|
@@ -698,13 +691,17 @@ class PG::Connection
|
|
698
691
|
# connection will have its +client_encoding+ set accordingly.
|
699
692
|
#
|
700
693
|
# Raises a PG::Error if the connection fails.
|
701
|
-
def new(*args
|
702
|
-
conn =
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
694
|
+
def new(*args)
|
695
|
+
conn = connect_to_hosts(*args)
|
696
|
+
|
697
|
+
if block_given?
|
698
|
+
begin
|
699
|
+
return yield conn
|
700
|
+
ensure
|
701
|
+
conn.finish
|
702
|
+
end
|
703
|
+
end
|
704
|
+
conn
|
708
705
|
end
|
709
706
|
alias async_connect new
|
710
707
|
alias connect new
|
@@ -712,6 +709,65 @@ class PG::Connection
|
|
712
709
|
alias setdb new
|
713
710
|
alias setdblogin new
|
714
711
|
|
712
|
+
private def connect_to_hosts(*args)
|
713
|
+
option_string = parse_connect_args(*args)
|
714
|
+
iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
|
715
|
+
iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
|
716
|
+
|
717
|
+
if iopts[:hostaddr]
|
718
|
+
# hostaddr is provided -> no need to resolve hostnames
|
719
|
+
|
720
|
+
elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
|
721
|
+
# Resolve DNS in Ruby to avoid blocking state while connecting.
|
722
|
+
# Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
|
723
|
+
# This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
|
724
|
+
ihosts = iopts[:host].split(",", -1)
|
725
|
+
iports = iopts[:port].split(",", -1)
|
726
|
+
iports = [nil] if iports.size == 0
|
727
|
+
iports = iports * ihosts.size if iports.size == 1
|
728
|
+
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
|
729
|
+
|
730
|
+
dests = ihosts.each_with_index.flat_map do |mhost, idx|
|
731
|
+
unless host_is_named_pipe?(mhost)
|
732
|
+
if Fiber.respond_to?(:scheduler) &&
|
733
|
+
Fiber.scheduler &&
|
734
|
+
RUBY_VERSION < '3.1.'
|
735
|
+
|
736
|
+
# Use a second thread to avoid blocking of the scheduler.
|
737
|
+
# `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
|
738
|
+
hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
|
739
|
+
else
|
740
|
+
hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
|
741
|
+
end
|
742
|
+
else
|
743
|
+
# No hostname to resolve (UnixSocket)
|
744
|
+
hostaddrs = [nil]
|
745
|
+
end
|
746
|
+
hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
|
747
|
+
end
|
748
|
+
iopts.merge!(
|
749
|
+
hostaddr: dests.map{|d| d[0] }.join(","),
|
750
|
+
host: dests.map{|d| d[1] }.join(","),
|
751
|
+
port: dests.map{|d| d[2] }.join(","))
|
752
|
+
else
|
753
|
+
# No host given
|
754
|
+
end
|
755
|
+
conn = self.connect_start(iopts) or
|
756
|
+
raise(PG::Error, "Unable to create a new connection")
|
757
|
+
|
758
|
+
raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
|
759
|
+
|
760
|
+
conn.send(:async_connect_or_reset, :connect_poll)
|
761
|
+
conn
|
762
|
+
end
|
763
|
+
|
764
|
+
private def host_is_named_pipe?(host_string)
|
765
|
+
host_string.empty? || host_string.start_with?("/") || # it's UnixSocket?
|
766
|
+
host_string.start_with?("@") || # it's UnixSocket in the abstract namespace?
|
767
|
+
# it's a path on Windows?
|
768
|
+
(RUBY_PLATFORM =~ /mingw|mswin/ && host_string =~ /\A([\/\\]|\w:[\/\\])/)
|
769
|
+
end
|
770
|
+
|
715
771
|
# call-seq:
|
716
772
|
# PG::Connection.ping(connection_hash) -> Integer
|
717
773
|
# PG::Connection.ping(connection_string) -> Integer
|
data/lib/pg/exceptions.rb
CHANGED
@@ -6,7 +6,13 @@ require 'pg' unless defined?( PG )
|
|
6
6
|
|
7
7
|
module PG
|
8
8
|
|
9
|
-
class Error < StandardError
|
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
|
10
16
|
|
11
17
|
end # module PG
|
12
18
|
|
data/lib/pg/version.rb
CHANGED