pg 1.3.0 → 1.4.0
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 +1 -1
- data/.github/workflows/binary-gems.yml +3 -3
- data/.github/workflows/source-gem.yml +5 -6
- data/History.rdoc +98 -1
- data/Rakefile.cross +4 -2
- data/certs/larskanis-2022.pem +26 -0
- data/ext/errorcodes.rb +0 -0
- data/ext/extconf.rb +13 -10
- data/ext/pg.h +1 -0
- data/ext/pg_connection.c +228 -212
- data/ext/pg_record_coder.c +6 -4
- data/ext/pg_result.c +16 -9
- data/ext/pg_tuple.c +2 -8
- data/ext/pg_type_map_all_strings.c +1 -0
- data/ext/pg_type_map_by_column.c +2 -0
- data/lib/pg/basic_type_registry.rb +8 -3
- data/lib/pg/connection.rb +178 -120
- data/lib/pg/exceptions.rb +7 -1
- data/lib/pg/version.rb +1 -1
- data/lib/pg.rb +9 -7
- data/misc/openssl-pg-segfault.rb +0 -0
- data/rakelib/task_extension.rb +46 -0
- 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.tar.gz.sig +0 -0
- metadata +32 -28
- metadata.gz.sig +0 -0
data/ext/pg_record_coder.c
CHANGED
|
@@ -344,10 +344,12 @@ record_isspace(char ch)
|
|
|
344
344
|
* oids = conn.exec( "SELECT (NULL::complex).*" )
|
|
345
345
|
* # Build a type map (PG::TypeMapByColumn) for decoding the "complex" type
|
|
346
346
|
* dtm = PG::BasicTypeMapForResults.new(conn).build_column_map( oids )
|
|
347
|
-
* #
|
|
348
|
-
* PG::BasicTypeRegistry.
|
|
349
|
-
* #
|
|
350
|
-
*
|
|
347
|
+
* # Build a type map and populate with basic types
|
|
348
|
+
* btr = PG::BasicTypeRegistry.new.register_default_types
|
|
349
|
+
* # Register a new record decoder for decoding our type "complex"
|
|
350
|
+
* btr.register_coder(PG::TextDecoder::Record.new(type_map: dtm, name: "complex"))
|
|
351
|
+
* # Apply our basic type registry to all results retrieved from the server
|
|
352
|
+
* conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn, registry: btr)
|
|
351
353
|
* # Now queries decode the "complex" type (and many basic types) automatically
|
|
352
354
|
* conn.exec("SELECT * FROM my_table").to_a
|
|
353
355
|
* # => [{"v1"=>[2.0, 3.0], "v2"=>[4.0, 5.0]}, {"v1"=>[6.0, 7.0], "v2"=>[8.0, 9.0]}]
|
data/ext/pg_result.c
CHANGED
|
@@ -1383,7 +1383,7 @@ pgresult_type_map_get(VALUE self)
|
|
|
1383
1383
|
|
|
1384
1384
|
|
|
1385
1385
|
static void
|
|
1386
|
-
yield_hash(VALUE self, int ntuples, int nfields)
|
|
1386
|
+
yield_hash(VALUE self, int ntuples, int nfields, void *data)
|
|
1387
1387
|
{
|
|
1388
1388
|
int tuple_num;
|
|
1389
1389
|
t_pg_result *this = pgresult_get_this(self);
|
|
@@ -1397,7 +1397,7 @@ yield_hash(VALUE self, int ntuples, int nfields)
|
|
|
1397
1397
|
}
|
|
1398
1398
|
|
|
1399
1399
|
static void
|
|
1400
|
-
yield_array(VALUE self, int ntuples, int nfields)
|
|
1400
|
+
yield_array(VALUE self, int ntuples, int nfields, void *data)
|
|
1401
1401
|
{
|
|
1402
1402
|
int row;
|
|
1403
1403
|
t_pg_result *this = pgresult_get_this(self);
|
|
@@ -1417,7 +1417,7 @@ yield_array(VALUE self, int ntuples, int nfields)
|
|
|
1417
1417
|
}
|
|
1418
1418
|
|
|
1419
1419
|
static void
|
|
1420
|
-
yield_tuple(VALUE self, int ntuples, int nfields)
|
|
1420
|
+
yield_tuple(VALUE self, int ntuples, int nfields, void *data)
|
|
1421
1421
|
{
|
|
1422
1422
|
int tuple_num;
|
|
1423
1423
|
t_pg_result *this = pgresult_get_this(self);
|
|
@@ -1436,8 +1436,9 @@ yield_tuple(VALUE self, int ntuples, int nfields)
|
|
|
1436
1436
|
}
|
|
1437
1437
|
}
|
|
1438
1438
|
|
|
1439
|
-
static
|
|
1440
|
-
|
|
1439
|
+
/* Non-static, and data pointer for use by sequel_pg */
|
|
1440
|
+
VALUE
|
|
1441
|
+
pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* data)
|
|
1441
1442
|
{
|
|
1442
1443
|
t_pg_result *this;
|
|
1443
1444
|
int nfields;
|
|
@@ -1456,6 +1457,7 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int))
|
|
|
1456
1457
|
|
|
1457
1458
|
switch( PQresultStatus(pgresult) ){
|
|
1458
1459
|
case PGRES_TUPLES_OK:
|
|
1460
|
+
case PGRES_COMMAND_OK:
|
|
1459
1461
|
if( ntuples == 0 )
|
|
1460
1462
|
return self;
|
|
1461
1463
|
rb_raise( rb_eInvalidResultStatus, "PG::Result is not in single row mode");
|
|
@@ -1465,7 +1467,12 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int))
|
|
|
1465
1467
|
pg_result_check( self );
|
|
1466
1468
|
}
|
|
1467
1469
|
|
|
1468
|
-
yielder( self, ntuples, nfields );
|
|
1470
|
+
yielder( self, ntuples, nfields, data );
|
|
1471
|
+
|
|
1472
|
+
if( gvl_PQisBusy(pgconn) ){
|
|
1473
|
+
/* wait for input (without blocking) before reading each result */
|
|
1474
|
+
pgconn_block( 0, NULL, this->connection );
|
|
1475
|
+
}
|
|
1469
1476
|
|
|
1470
1477
|
pgresult = gvl_PQgetResult(pgconn);
|
|
1471
1478
|
if( pgresult == NULL )
|
|
@@ -1516,7 +1523,7 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int))
|
|
|
1516
1523
|
static VALUE
|
|
1517
1524
|
pgresult_stream_each(VALUE self)
|
|
1518
1525
|
{
|
|
1519
|
-
return pgresult_stream_any(self, yield_hash);
|
|
1526
|
+
return pgresult_stream_any(self, yield_hash, NULL);
|
|
1520
1527
|
}
|
|
1521
1528
|
|
|
1522
1529
|
/*
|
|
@@ -1532,7 +1539,7 @@ pgresult_stream_each(VALUE self)
|
|
|
1532
1539
|
static VALUE
|
|
1533
1540
|
pgresult_stream_each_row(VALUE self)
|
|
1534
1541
|
{
|
|
1535
|
-
return pgresult_stream_any(self, yield_array);
|
|
1542
|
+
return pgresult_stream_any(self, yield_array, NULL);
|
|
1536
1543
|
}
|
|
1537
1544
|
|
|
1538
1545
|
/*
|
|
@@ -1549,7 +1556,7 @@ pgresult_stream_each_tuple(VALUE self)
|
|
|
1549
1556
|
/* allocate VALUEs that are shared between all streamed tuples */
|
|
1550
1557
|
ensure_init_for_tuple(self);
|
|
1551
1558
|
|
|
1552
|
-
return pgresult_stream_any(self, yield_tuple);
|
|
1559
|
+
return pgresult_stream_any(self, yield_tuple, NULL);
|
|
1553
1560
|
}
|
|
1554
1561
|
|
|
1555
1562
|
/*
|
data/ext/pg_tuple.c
CHANGED
|
@@ -471,10 +471,7 @@ pg_tuple_dump(VALUE self)
|
|
|
471
471
|
values = rb_ary_new4(this->num_fields, &this->values[0]);
|
|
472
472
|
a = rb_ary_new3(2, field_names, values);
|
|
473
473
|
|
|
474
|
-
|
|
475
|
-
rb_copy_generic_ivar(a, self);
|
|
476
|
-
FL_SET(a, FL_EXIVAR);
|
|
477
|
-
}
|
|
474
|
+
rb_copy_generic_ivar(a, self);
|
|
478
475
|
|
|
479
476
|
return a;
|
|
480
477
|
}
|
|
@@ -542,10 +539,7 @@ pg_tuple_load(VALUE self, VALUE a)
|
|
|
542
539
|
|
|
543
540
|
RTYPEDDATA_DATA(self) = this;
|
|
544
541
|
|
|
545
|
-
|
|
546
|
-
rb_copy_generic_ivar(self, a);
|
|
547
|
-
FL_SET(self, FL_EXIVAR);
|
|
548
|
-
}
|
|
542
|
+
rb_copy_generic_ivar(self, a);
|
|
549
543
|
|
|
550
544
|
return self;
|
|
551
545
|
}
|
|
@@ -76,6 +76,7 @@ pg_tmas_fit_to_copy_get( VALUE self )
|
|
|
76
76
|
static VALUE
|
|
77
77
|
pg_tmas_typecast_copy_get( t_typemap *p_typemap, VALUE field_str, int fieldno, int format, int enc_idx )
|
|
78
78
|
{
|
|
79
|
+
rb_str_modify(field_str);
|
|
79
80
|
if( format == 0 ){
|
|
80
81
|
PG_ENCODING_SET_NOCHECK( field_str, enc_idx );
|
|
81
82
|
} else {
|
data/ext/pg_type_map_by_column.c
CHANGED
|
@@ -150,10 +150,12 @@ pg_tmbc_typecast_copy_get( t_typemap *p_typemap, VALUE field_str, int fieldno, i
|
|
|
150
150
|
|
|
151
151
|
/* Is it a pure String conversion? Then we can directly send field_str to the user. */
|
|
152
152
|
if( dec_func == pg_text_dec_string ){
|
|
153
|
+
rb_str_modify(field_str);
|
|
153
154
|
PG_ENCODING_SET_NOCHECK( field_str, enc_idx );
|
|
154
155
|
return field_str;
|
|
155
156
|
}
|
|
156
157
|
if( dec_func == pg_bin_dec_bytea ){
|
|
158
|
+
rb_str_modify(field_str);
|
|
157
159
|
PG_ENCODING_SET_NOCHECK( field_str, rb_ascii8bit_encindex() );
|
|
158
160
|
return field_str;
|
|
159
161
|
}
|
|
@@ -22,7 +22,7 @@ require 'pg' unless defined?( PG )
|
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
24
|
# conn = PG.connect
|
|
25
|
-
# regi = PG::BasicTypeRegistry.new.
|
|
25
|
+
# regi = PG::BasicTypeRegistry.new.register_default_types
|
|
26
26
|
# regi.register_type(0, 'inet', InetEncoder, InetDecoder)
|
|
27
27
|
# conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn, registry: regi)
|
|
28
28
|
class PG::BasicTypeRegistry
|
|
@@ -184,6 +184,7 @@ class PG::BasicTypeRegistry
|
|
|
184
184
|
name = coder.name || raise(ArgumentError, "name of #{coder.inspect} must be defined")
|
|
185
185
|
h[:encoder][name] = coder if coder.respond_to?(:encode)
|
|
186
186
|
h[:decoder][name] = coder if coder.respond_to?(:decode)
|
|
187
|
+
self
|
|
187
188
|
end
|
|
188
189
|
|
|
189
190
|
# Register the given +encoder_class+ and/or +decoder_class+ for casting a PostgreSQL type.
|
|
@@ -193,6 +194,7 @@ class PG::BasicTypeRegistry
|
|
|
193
194
|
def register_type(format, name, encoder_class, decoder_class)
|
|
194
195
|
register_coder(encoder_class.new(name: name, format: format)) if encoder_class
|
|
195
196
|
register_coder(decoder_class.new(name: name, format: format)) if decoder_class
|
|
197
|
+
self
|
|
196
198
|
end
|
|
197
199
|
|
|
198
200
|
# Alias the +old+ type to the +new+ type.
|
|
@@ -205,10 +207,11 @@ class PG::BasicTypeRegistry
|
|
|
205
207
|
@coders_by_name[format][ende].delete(new)
|
|
206
208
|
end
|
|
207
209
|
end
|
|
210
|
+
self
|
|
208
211
|
end
|
|
209
212
|
|
|
210
213
|
# Populate the registry with all builtin types of ruby-pg
|
|
211
|
-
def
|
|
214
|
+
def register_default_types
|
|
212
215
|
register_type 0, 'int2', PG::TextEncoder::Integer, PG::TextDecoder::Integer
|
|
213
216
|
alias_type 0, 'int4', 'int2'
|
|
214
217
|
alias_type 0, 'int8', 'int2'
|
|
@@ -281,8 +284,10 @@ class PG::BasicTypeRegistry
|
|
|
281
284
|
self
|
|
282
285
|
end
|
|
283
286
|
|
|
287
|
+
alias define_default_types register_default_types
|
|
288
|
+
|
|
284
289
|
# @private
|
|
285
|
-
DEFAULT_TYPE_REGISTRY = PG::BasicTypeRegistry.new.
|
|
290
|
+
DEFAULT_TYPE_REGISTRY = PG::BasicTypeRegistry.new.register_default_types
|
|
286
291
|
|
|
287
292
|
# Delegate class method calls to DEFAULT_TYPE_REGISTRY
|
|
288
293
|
class << self
|
data/lib/pg/connection.rb
CHANGED
|
@@ -46,37 +46,6 @@ class PG::Connection
|
|
|
46
46
|
hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Decode a connection string to Hash options
|
|
50
|
-
#
|
|
51
|
-
# Value are properly unquoted and unescaped.
|
|
52
|
-
def self.connect_string_to_hash( str )
|
|
53
|
-
options = {}
|
|
54
|
-
key = nil
|
|
55
|
-
value = String.new
|
|
56
|
-
str.scan(/\G\s*(?>([^\s\\\']+)\s*=\s*|([^\s\\\']+)|'((?:[^\'\\]|\\.)*)'|(\\.?)|(\S))(\s|\z)?/m) do
|
|
57
|
-
|k, word, sq, esc, garbage, sep|
|
|
58
|
-
raise ArgumentError, "unterminated quoted string in connection info string: #{str.inspect}" if garbage
|
|
59
|
-
if k
|
|
60
|
-
key = k
|
|
61
|
-
else
|
|
62
|
-
value << (word || (sq || esc).gsub(/\\(.)/, '\\1'))
|
|
63
|
-
end
|
|
64
|
-
if sep
|
|
65
|
-
raise ArgumentError, "missing = after #{value.inspect}" unless key
|
|
66
|
-
options[key.to_sym] = value
|
|
67
|
-
key = nil
|
|
68
|
-
value = String.new
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
options
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# URI defined in RFC3986
|
|
75
|
-
# This regexp is modified to allow host to specify multiple comma separated components captured as <hostports> and to disallow comma in hostnames.
|
|
76
|
-
# Taken from: https://github.com/ruby/ruby/blob/be04006c7d2f9aeb7e9d8d09d945b3a9c7850202/lib/uri/rfc3986_parser.rb#L6
|
|
77
|
-
HOST_AND_PORT = /(?<hostport>(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[-\.!$&-+0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)/
|
|
78
|
-
POSTGRESQL_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<hostports>#{HOST_AND_PORT}(?:,\g<hostport>)*))(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
|
|
79
|
-
|
|
80
49
|
# Parse the connection +args+ into a connection-parameter string.
|
|
81
50
|
# See PG::Connection.new for valid arguments.
|
|
82
51
|
#
|
|
@@ -87,91 +56,43 @@ class PG::Connection
|
|
|
87
56
|
# * URI object
|
|
88
57
|
# * positional arguments
|
|
89
58
|
#
|
|
90
|
-
# The method adds the option "
|
|
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 = HOST_AND_PORT.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
|
-
uri += uri_match['query'] ? "&" : "?"
|
|
166
|
-
uri += URI.encode_www_form( oopts )
|
|
167
|
-
return uri
|
|
168
|
-
else
|
|
169
|
-
option_string += ' ' unless option_string.empty? && oopts.empty?
|
|
170
|
-
return option_string + connect_hash_to_string(oopts)
|
|
171
|
-
end
|
|
93
|
+
return connect_hash_to_string(iopts)
|
|
172
94
|
end
|
|
173
95
|
|
|
174
|
-
|
|
175
96
|
# call-seq:
|
|
176
97
|
# conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
|
|
177
98
|
#
|
|
@@ -241,6 +162,7 @@ class PG::Connection
|
|
|
241
162
|
# ["more", "data", "to", "copy"]
|
|
242
163
|
|
|
243
164
|
def copy_data( sql, coder=nil )
|
|
165
|
+
raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
|
|
244
166
|
res = exec( sql )
|
|
245
167
|
|
|
246
168
|
case res.result_status
|
|
@@ -272,11 +194,15 @@ class PG::Connection
|
|
|
272
194
|
yield res
|
|
273
195
|
rescue Exception => err
|
|
274
196
|
cancel
|
|
275
|
-
|
|
197
|
+
begin
|
|
198
|
+
while get_copy_data
|
|
199
|
+
end
|
|
200
|
+
rescue PG::Error
|
|
201
|
+
# Ignore error in cleanup to avoid losing original exception
|
|
276
202
|
end
|
|
277
203
|
while get_result
|
|
278
204
|
end
|
|
279
|
-
raise
|
|
205
|
+
raise err
|
|
280
206
|
else
|
|
281
207
|
res = get_last_result
|
|
282
208
|
if !res || res.result_status != PGRES_COMMAND_OK
|
|
@@ -284,7 +210,7 @@ class PG::Connection
|
|
|
284
210
|
end
|
|
285
211
|
while get_result
|
|
286
212
|
end
|
|
287
|
-
raise PG::NotAllCopyDataRetrieved
|
|
213
|
+
raise PG::NotAllCopyDataRetrieved.new("Not all COPY data retrieved", connection: self)
|
|
288
214
|
end
|
|
289
215
|
res
|
|
290
216
|
ensure
|
|
@@ -309,16 +235,17 @@ class PG::Connection
|
|
|
309
235
|
# and a +COMMIT+ at the end of the block, or
|
|
310
236
|
# +ROLLBACK+ if any exception occurs.
|
|
311
237
|
def transaction
|
|
238
|
+
rollback = false
|
|
312
239
|
exec "BEGIN"
|
|
313
|
-
|
|
240
|
+
yield(self)
|
|
314
241
|
rescue Exception
|
|
315
|
-
|
|
242
|
+
rollback = true
|
|
243
|
+
cancel if transaction_status == PG::PQTRANS_ACTIVE
|
|
316
244
|
block
|
|
317
245
|
exec "ROLLBACK"
|
|
318
246
|
raise
|
|
319
|
-
|
|
320
|
-
exec "COMMIT"
|
|
321
|
-
res
|
|
247
|
+
ensure
|
|
248
|
+
exec "COMMIT" unless rollback
|
|
322
249
|
end
|
|
323
250
|
|
|
324
251
|
### Returns an array of Hashes with connection defaults. See ::conndefaults
|
|
@@ -385,7 +312,7 @@ class PG::Connection
|
|
|
385
312
|
# If the optional code block is given, it will be passed <i>result</i> as an argument,
|
|
386
313
|
# and the PG::Result object will automatically be cleared when the block terminates.
|
|
387
314
|
# In this instance, <code>conn.exec</code> returns the value of the block.
|
|
388
|
-
def get_result
|
|
315
|
+
def get_result
|
|
389
316
|
block
|
|
390
317
|
sync_get_result
|
|
391
318
|
end
|
|
@@ -481,10 +408,10 @@ class PG::Connection
|
|
|
481
408
|
# See also #copy_data.
|
|
482
409
|
#
|
|
483
410
|
def put_copy_data(buffer, encoder=nil)
|
|
484
|
-
until sync_put_copy_data(buffer, encoder)
|
|
485
|
-
flush
|
|
411
|
+
until res=sync_put_copy_data(buffer, encoder)
|
|
412
|
+
res = flush
|
|
486
413
|
end
|
|
487
|
-
|
|
414
|
+
res
|
|
488
415
|
end
|
|
489
416
|
alias async_put_copy_data put_copy_data
|
|
490
417
|
|
|
@@ -544,6 +471,7 @@ class PG::Connection
|
|
|
544
471
|
def reset
|
|
545
472
|
reset_start
|
|
546
473
|
async_connect_or_reset(:reset_poll)
|
|
474
|
+
self
|
|
547
475
|
end
|
|
548
476
|
alias async_reset reset
|
|
549
477
|
|
|
@@ -611,37 +539,70 @@ class PG::Connection
|
|
|
611
539
|
alias async_cancel cancel
|
|
612
540
|
|
|
613
541
|
private def async_connect_or_reset(poll_meth)
|
|
614
|
-
# Now grab a reference to the underlying socket so we know when the connection is established
|
|
615
|
-
socket = socket_io
|
|
616
|
-
|
|
617
542
|
# Track the progress of the connection, waiting for the socket to become readable/writable before polling it
|
|
543
|
+
|
|
544
|
+
if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
|
|
545
|
+
# Lowest timeout is 2 seconds - like in libpq
|
|
546
|
+
timeo = [timeo, 2].max
|
|
547
|
+
stop_time = timeo + Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
548
|
+
end
|
|
549
|
+
|
|
618
550
|
poll_status = PG::PGRES_POLLING_WRITING
|
|
619
551
|
until poll_status == PG::PGRES_POLLING_OK ||
|
|
620
552
|
poll_status == PG::PGRES_POLLING_FAILED
|
|
621
553
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
554
|
+
timeout = stop_time&.-(Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
555
|
+
event = if !timeout || timeout >= 0
|
|
556
|
+
# If the socket needs to read, wait 'til it becomes readable to poll again
|
|
557
|
+
case poll_status
|
|
558
|
+
when PG::PGRES_POLLING_READING
|
|
559
|
+
if defined?(IO::READABLE) # ruby-3.0+
|
|
560
|
+
socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
|
|
561
|
+
else
|
|
562
|
+
IO.select([socket_io], nil, [socket_io], timeout)
|
|
563
|
+
end
|
|
626
564
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
565
|
+
# ...and the same for when the socket needs to write
|
|
566
|
+
when PG::PGRES_POLLING_WRITING
|
|
567
|
+
if defined?(IO::WRITABLE) # ruby-3.0+
|
|
568
|
+
# Use wait instead of wait_readable, since connection errors are delivered as
|
|
569
|
+
# exceptional/priority events on Windows.
|
|
570
|
+
socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
|
|
571
|
+
else
|
|
572
|
+
# io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
|
|
573
|
+
IO.select(nil, [socket_io], [socket_io], timeout)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
# connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
|
|
578
|
+
# connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
|
|
579
|
+
unless event
|
|
580
|
+
if self.class.send(:host_is_named_pipe?, host)
|
|
581
|
+
connhost = "on socket \"#{host}\""
|
|
582
|
+
elsif respond_to?(:hostaddr)
|
|
583
|
+
connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
|
|
584
|
+
else
|
|
585
|
+
connhost = "at \"#{host}\", port #{port}"
|
|
586
|
+
end
|
|
587
|
+
raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
|
|
630
588
|
end
|
|
631
589
|
|
|
632
590
|
# Check to see if it's finished or failed yet
|
|
633
591
|
poll_status = send( poll_meth )
|
|
592
|
+
@last_status = status unless [PG::CONNECTION_BAD, PG::CONNECTION_OK].include?(status)
|
|
634
593
|
end
|
|
635
594
|
|
|
636
|
-
|
|
595
|
+
unless status == PG::CONNECTION_OK
|
|
596
|
+
msg = error_message
|
|
597
|
+
finish
|
|
598
|
+
raise PG::ConnectionBad.new(msg, connection: self)
|
|
599
|
+
end
|
|
637
600
|
|
|
638
601
|
# Set connection to nonblocking to handle all blocking states in ruby.
|
|
639
602
|
# That way a fiber scheduler is able to handle IO requests.
|
|
640
603
|
sync_setnonblocking(true)
|
|
641
604
|
self.flush_data = true
|
|
642
605
|
set_default_encoding
|
|
643
|
-
|
|
644
|
-
self
|
|
645
606
|
end
|
|
646
607
|
|
|
647
608
|
class << self
|
|
@@ -697,12 +658,16 @@ class PG::Connection
|
|
|
697
658
|
#
|
|
698
659
|
# Raises a PG::Error if the connection fails.
|
|
699
660
|
def new(*args, **kwargs)
|
|
700
|
-
conn =
|
|
701
|
-
raise(PG::Error, "Unable to create a new connection")
|
|
702
|
-
|
|
703
|
-
raise(PG::ConnectionBad, conn.error_message) if conn.status == PG::CONNECTION_BAD
|
|
661
|
+
conn = connect_to_hosts(*args, **kwargs)
|
|
704
662
|
|
|
705
|
-
|
|
663
|
+
if block_given?
|
|
664
|
+
begin
|
|
665
|
+
return yield conn
|
|
666
|
+
ensure
|
|
667
|
+
conn.finish
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
conn
|
|
706
671
|
end
|
|
707
672
|
alias async_connect new
|
|
708
673
|
alias connect new
|
|
@@ -710,6 +675,99 @@ class PG::Connection
|
|
|
710
675
|
alias setdb new
|
|
711
676
|
alias setdblogin new
|
|
712
677
|
|
|
678
|
+
private def connect_to_hosts(*args)
|
|
679
|
+
option_string = parse_connect_args(*args)
|
|
680
|
+
iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
|
|
681
|
+
iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
|
|
682
|
+
|
|
683
|
+
errors = []
|
|
684
|
+
if iopts[:hostaddr]
|
|
685
|
+
# hostaddr is provided -> no need to resolve hostnames
|
|
686
|
+
ihostaddrs = iopts[:hostaddr].split(",", -1)
|
|
687
|
+
|
|
688
|
+
ihosts = iopts[:host].split(",", -1) if iopts[:host]
|
|
689
|
+
raise PG::ConnectionBad, "could not match #{ihosts.size} host names to #{ihostaddrs.size} hostaddr values" if ihosts && ihosts.size != ihostaddrs.size
|
|
690
|
+
|
|
691
|
+
iports = iopts[:port].split(",", -1)
|
|
692
|
+
iports = iports * ihostaddrs.size if iports.size == 1
|
|
693
|
+
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihostaddrs.size} hosts" if iports.size != ihostaddrs.size
|
|
694
|
+
|
|
695
|
+
# Try to connect to each hostaddr with separate timeout
|
|
696
|
+
ihostaddrs.each_with_index do |ihostaddr, idx|
|
|
697
|
+
oopts = iopts.merge(hostaddr: ihostaddr, port: iports[idx])
|
|
698
|
+
oopts[:host] = ihosts[idx] if ihosts
|
|
699
|
+
c = connect_internal(oopts, errors)
|
|
700
|
+
return c if c
|
|
701
|
+
end
|
|
702
|
+
elsif iopts[:host]
|
|
703
|
+
# Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
|
|
704
|
+
ihosts = iopts[:host].split(",", -1)
|
|
705
|
+
|
|
706
|
+
iports = iopts[:port].split(",", -1)
|
|
707
|
+
iports = iports * ihosts.size if iports.size == 1
|
|
708
|
+
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
|
|
709
|
+
|
|
710
|
+
ihosts.each_with_index do |mhost, idx|
|
|
711
|
+
unless host_is_named_pipe?(mhost)
|
|
712
|
+
addrs = if Fiber.respond_to?(:scheduler) &&
|
|
713
|
+
Fiber.scheduler &&
|
|
714
|
+
RUBY_VERSION < '3.1.'
|
|
715
|
+
|
|
716
|
+
# Use a second thread to avoid blocking of the scheduler.
|
|
717
|
+
# `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
|
|
718
|
+
Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
|
|
719
|
+
else
|
|
720
|
+
Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Try to connect to each host with separate timeout
|
|
724
|
+
addrs.each do |addr|
|
|
725
|
+
oopts = iopts.merge(hostaddr: addr, host: mhost, port: iports[idx])
|
|
726
|
+
c = connect_internal(oopts, errors)
|
|
727
|
+
return c if c
|
|
728
|
+
end
|
|
729
|
+
else
|
|
730
|
+
# No hostname to resolve (UnixSocket)
|
|
731
|
+
oopts = iopts.merge(host: mhost, port: iports[idx])
|
|
732
|
+
c = connect_internal(oopts, errors)
|
|
733
|
+
return c if c
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
else
|
|
737
|
+
# No host given
|
|
738
|
+
return connect_internal(iopts)
|
|
739
|
+
end
|
|
740
|
+
raise PG::ConnectionBad, errors.join("\n")
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
private def connect_internal(opts, errors=nil)
|
|
744
|
+
begin
|
|
745
|
+
conn = self.connect_start(opts) or
|
|
746
|
+
raise(PG::Error, "Unable to create a new connection")
|
|
747
|
+
|
|
748
|
+
raise PG::ConnectionBad.new(conn.error_message, connection: self) if conn.status == PG::CONNECTION_BAD
|
|
749
|
+
|
|
750
|
+
conn.send(:async_connect_or_reset, :connect_poll)
|
|
751
|
+
rescue PG::ConnectionBad => err
|
|
752
|
+
if errors && !(conn && [PG::CONNECTION_AWAITING_RESPONSE].include?(conn.instance_variable_get(:@last_status)))
|
|
753
|
+
# Seems to be no authentication error -> try next host
|
|
754
|
+
errors << err
|
|
755
|
+
return nil
|
|
756
|
+
else
|
|
757
|
+
# Probably an authentication error
|
|
758
|
+
raise
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
conn
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
private def host_is_named_pipe?(host_string)
|
|
765
|
+
host_string.empty? || host_string.start_with?("/") || # it's UnixSocket?
|
|
766
|
+
host_string.start_with?("@") || # it's UnixSocket in the abstract namespace?
|
|
767
|
+
# it's a path on Windows?
|
|
768
|
+
(RUBY_PLATFORM =~ /mingw|mswin/ && host_string =~ /\A([\/\\]|\w:[\/\\])/)
|
|
769
|
+
end
|
|
770
|
+
|
|
713
771
|
# call-seq:
|
|
714
772
|
# PG::Connection.ping(connection_hash) -> Integer
|
|
715
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, 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
data/lib/pg.rb
CHANGED
|
@@ -53,18 +53,20 @@ module PG
|
|
|
53
53
|
|
|
54
54
|
class NotAllCopyDataRetrieved < PG::Error
|
|
55
55
|
end
|
|
56
|
+
class NotInBlockingMode < PG::Error
|
|
57
|
+
end
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# Get the PG library version.
|
|
60
|
+
#
|
|
61
|
+
# +include_buildnum+ is no longer used and any value passed will be ignored.
|
|
62
|
+
def self.version_string( include_buildnum=nil )
|
|
63
|
+
"%s %s" % [ self.name, VERSION ]
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
### Convenience alias for PG::Connection.new.
|
|
66
|
-
def self
|
|
67
|
-
|
|
68
|
+
def self.connect( *args, &block )
|
|
69
|
+
Connection.new( *args, &block )
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
|