em-pg-client-12 0.3.4

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.
@@ -0,0 +1,171 @@
1
+ $:.unshift "lib"
2
+ gem 'eventmachine', '~> 1.0.0'
3
+ gem 'pg', ENV['EM_PG_CLIENT_TEST_PG_VERSION']
4
+ require 'eventmachine'
5
+ require 'em-synchrony'
6
+ require 'pg/em'
7
+ module PGSpecMacros
8
+ def test_blocking
9
+ client = subject.new(options)
10
+ client.should be_an_instance_of subject
11
+ client.on_connect.should be on_connect
12
+ client.exec_prepared(query_name) do |result|
13
+ result.should be_an_instance_of PG::Result
14
+ result[0]['pg_database_size'].to_i.should be > 0
15
+ end
16
+ client.reset
17
+ client.exec_prepared(query_name) do |result|
18
+ result.should be_an_instance_of PG::Result
19
+ result[0]['pg_database_size'].to_i.should be > 0
20
+ end
21
+ end
22
+
23
+ def test_blocking_error
24
+ expect do
25
+ subject.new(options)
26
+ end.to raise_error(on_connect_exception)
27
+ client = subject.new
28
+ client.should be_an_instance_of subject
29
+ client.on_connect = on_connect
30
+ expect do
31
+ client.reset
32
+ end.to raise_error(on_connect_exception)
33
+ end
34
+ end
35
+
36
+ RSpec.configure do |config|
37
+ config.include(PGSpecMacros)
38
+ end
39
+
40
+ shared_context 'test on_connect' do
41
+
42
+ it "should invoke on_connect after deferrable connect and reset" do
43
+ EM.run do
44
+ subject.connect_defer(options) do |client|
45
+ client.should be_an_instance_of subject
46
+ client.on_connect.should be on_connect
47
+ client.exec_prepared_defer(query_name) do |result|
48
+ result.should be_an_instance_of PG::Result
49
+ result[0]['pg_database_size'].to_i.should be > 0
50
+ client.reset_defer do |client|
51
+ client.should be_an_instance_of subject
52
+ client.exec_prepared_defer(query_name) do |result|
53
+ result.should be_an_instance_of PG::Result
54
+ result[0]['pg_database_size'].to_i.should be > 0
55
+ EM.stop
56
+ end.should be_a_kind_of ::EM::Deferrable
57
+ end.should be_a_kind_of ::EM::Deferrable
58
+ end.should be_a_kind_of ::EM::Deferrable
59
+ end.should be_a_kind_of ::EM::Deferrable
60
+ end
61
+ end
62
+
63
+ it "should invoke on_connect after synchrony connect and reset" do
64
+ EM.synchrony do
65
+ test_blocking
66
+ EM.stop
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ shared_context 'test on_connect error' do
73
+
74
+ it "should fail on_connect with exception after deferrable connect and reset" do
75
+ EM.run do
76
+ subject.connect_defer(options) do |ex|
77
+ ex.should be_an_instance_of on_connect_exception
78
+ subject.connect_defer do |client|
79
+ client.should be_an_instance_of subject
80
+ client.on_connect = on_connect
81
+ client.reset_defer do |ex|
82
+ ex.should be_an_instance_of on_connect_exception
83
+ EM.stop
84
+ end.should be_a_kind_of ::EM::Deferrable
85
+ end.should be_a_kind_of ::EM::Deferrable
86
+ end.should be_a_kind_of ::EM::Deferrable
87
+ end
88
+ end
89
+
90
+ it "should fail on_connect with exception after synchrony connect and reset" do
91
+ EM.synchrony do
92
+ test_blocking_error
93
+ EM.stop
94
+ end
95
+ end
96
+ end
97
+
98
+ shared_context 'test blocking' do
99
+ it "should invoke on_connect after blocking connect and reset" do
100
+ test_blocking
101
+ end
102
+ end
103
+
104
+ shared_context 'test blocking on_connect error' do
105
+ it "should fail on_connect with exception after blocking connect and reset" do
106
+ test_blocking_error
107
+ end
108
+ end
109
+
110
+ describe 'on_connect option' do
111
+ subject { PG::EM::Client }
112
+ let(:query_name) { 'get_db_size' }
113
+ let(:query) { 'SELECT pg_database_size(current_database());' }
114
+ let(:options) { {on_connect: on_connect} }
115
+ let(:sleep_query){ 'SELECT pg_sleep(0.1)'}
116
+ let(:on_connect_exception) { Class.new(StandardError) }
117
+
118
+
119
+ describe 'with deferrable on_connect' do
120
+ let(:on_connect) { proc {|client, is_async|
121
+ is_async.should be_true
122
+ PG::EM::FeaturedDeferrable.new.tap do |df|
123
+ client.exec_defer(sleep_query).callback do
124
+ df.bind_status client.prepare_defer(query_name, query)
125
+ end.errback { df.fail on_connect_exception }
126
+ end
127
+ } }
128
+
129
+ include_context 'test on_connect'
130
+ end
131
+
132
+ describe 'with synchrony on_connect' do
133
+ let(:on_connect) { proc {|client, is_async|
134
+ is_async.should be_true
135
+ was_async = false
136
+ EM.next_tick { was_async = true }
137
+ client.exec(sleep_query)
138
+ client.prepare(query_name, query)
139
+ was_async.should be_true
140
+ } }
141
+
142
+ include_context 'test on_connect'
143
+ end
144
+
145
+ describe 'with blocking on_connect' do
146
+ let(:on_connect) { proc {|client, is_async|
147
+ is_async.should be_false
148
+ client.prepare(query_name, query)
149
+ } }
150
+
151
+ include_context 'test blocking'
152
+ end
153
+
154
+ describe 'with error raised in on_connect' do
155
+ let(:on_connect) { proc {|client|
156
+ raise on_connect_exception
157
+ } }
158
+
159
+ include_context 'test on_connect error'
160
+ include_context 'test blocking on_connect error'
161
+ end
162
+
163
+ describe 'with on_connect deferrable failure' do
164
+ let(:on_connect) { proc {|client|
165
+ ::EM::DefaultDeferrable.new.tap {|df| df.fail on_connect_exception.new }
166
+ } }
167
+
168
+ include_context 'test on_connect error'
169
+ end
170
+
171
+ end
@@ -0,0 +1,200 @@
1
+ $:.unshift "lib"
2
+ gem 'eventmachine', '~> 1.0.0'
3
+ gem 'pg', ENV['EM_PG_CLIENT_TEST_PG_VERSION']
4
+ require 'eventmachine'
5
+ require 'em-synchrony'
6
+ require 'em-synchrony/fiber_iterator'
7
+ require 'pg/em/connection_pool'
8
+
9
+ shared_context 'test on_connect' do
10
+
11
+ it 'should call prepared statement concurrently with synchrony' do
12
+ results = []
13
+ pool = subject.new(options)
14
+ pool.max_size.should eq concurrency
15
+ pool.size.should eq 1
16
+ start = Time.now
17
+ EM::Synchrony::FiberIterator.new((1..concurrency), concurrency).each do |index|
18
+ pool.exec_prepared(query_name) do |result|
19
+ result.should be_an_instance_of PG::Result
20
+ result[0]['pg_database_size'].to_i.should be > 0
21
+ end
22
+ pool.query(sleep_query).should be_an_instance_of PG::Result
23
+ results << index
24
+ end
25
+ delta = Time.now - start
26
+ delta.should be_between(sleep_interval, sleep_interval * concurrency / 2)
27
+ results.sort.should eq (1..concurrency).to_a
28
+ pool.size.should eq concurrency
29
+ EM.stop
30
+ end
31
+
32
+ it 'should call prepared statement concurrently with deferrable' do
33
+ results = []
34
+ subject.connect_defer(options) do |pool|
35
+ pool.max_size.should eq concurrency
36
+ pool.size.should eq 1
37
+ start = Time.now
38
+ concurrency.times do |index|
39
+ pool.exec_prepared_defer(query_name) do |result|
40
+ result.should be_an_instance_of PG::Result
41
+ result[0]['pg_database_size'].to_i.should be > 0
42
+ pool.query_defer(sleep_query) do |result|
43
+ result.should be_an_instance_of PG::Result
44
+ results << index
45
+ if results.length == concurrency
46
+ delta = Time.now - start
47
+ delta.should be_between(sleep_interval, sleep_interval * concurrency / 2)
48
+ results.sort.should eq (0...concurrency).to_a
49
+ pool.size.should eq concurrency
50
+ EM.stop
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ shared_context 'test on_connect error' do
60
+
61
+ it "should fail on_connect with exception after synchrony connect" do
62
+ expect do
63
+ subject.new(options)
64
+ end.to raise_error(on_connect_exception)
65
+ client = subject.new(options.merge(lazy: true))
66
+ client.should be_an_instance_of subject
67
+ expect do
68
+ client.query(sleep_query)
69
+ end.to raise_error(on_connect_exception)
70
+ EM.stop
71
+ end
72
+
73
+ it "should fail on_connect with exception after deferrable connect" do
74
+ subject.connect_defer(options) do |ex|
75
+ ex.should be_an_instance_of on_connect_exception
76
+ pool = subject.new(options.merge(lazy: true))
77
+ pool.should be_an_instance_of subject
78
+ pool.query_defer(sleep_query) do |ex|
79
+ ex.should be_an_instance_of on_connect_exception
80
+ EM.stop
81
+ end.should be_an_instance_of PG::EM::FeaturedDeferrable
82
+ end.should be_an_instance_of PG::EM::FeaturedDeferrable
83
+ end
84
+
85
+ end
86
+
87
+ describe 'connection pool' do
88
+ subject { PG::EM::ConnectionPool }
89
+
90
+ describe 'on_connect' do
91
+ let(:query_name) { 'get_db_size' }
92
+ let(:query) { 'SELECT pg_database_size(current_database());' }
93
+ let(:concurrency) { 10 }
94
+ let(:options) { {size: concurrency, on_connect: on_connect} }
95
+ let(:sleep_interval) { 0.1 }
96
+ let(:sleep_query) { "SELECT pg_sleep(#{sleep_interval})"}
97
+ let(:on_connect_exception) { Class.new(StandardError) }
98
+ let(:on_connect) { proc {} }
99
+
100
+ around(:each) do |testcase|
101
+ EM.synchrony do
102
+ begin
103
+ testcase.call
104
+ end
105
+ end
106
+ end
107
+
108
+ it 'should setup block as on_connect client option' do
109
+ connect_hook = false
110
+ pool = subject.new { connect_hook = true }
111
+ connect_hook.should be_true
112
+ pool.should be_an_instance_of subject
113
+ pool.on_connect.should be_an_instance_of Proc
114
+ EM.stop
115
+ end
116
+
117
+ it 'should prefer on_connect from options' do
118
+ connect_hook = false
119
+ pool = subject.new(options) { connect_hook = true }
120
+ connect_hook.should be_false
121
+ pool.should be_an_instance_of subject
122
+ pool.on_connect.should be on_connect
123
+ EM.stop
124
+ end
125
+
126
+ describe 'with deferrable on_connect' do
127
+ let(:on_connect) { proc {|client, is_async|
128
+ is_async.should be_true
129
+ PG::EM::FeaturedDeferrable.new.tap do |df|
130
+ client.exec_defer(sleep_query).callback do
131
+ df.bind_status client.prepare_defer(query_name, query)
132
+ end.errback { df.fail on_connect_exception }
133
+ end
134
+ } }
135
+
136
+ include_context 'test on_connect'
137
+ end
138
+
139
+ describe 'with synchrony on_connect' do
140
+ let(:on_connect) { proc {|client, is_async|
141
+ is_async.should be_true
142
+ was_async = false
143
+ EM.next_tick { was_async = true }
144
+ client.exec(sleep_query)
145
+ client.prepare(query_name, query)
146
+ was_async.should be_true
147
+ } }
148
+
149
+ include_context 'test on_connect'
150
+ end
151
+
152
+ describe 'with error raised in on_connect' do
153
+ let(:on_connect) { proc {|client|
154
+ raise on_connect_exception
155
+ } }
156
+
157
+ include_context 'test on_connect error'
158
+ end
159
+
160
+ describe 'with on_connect deferrable failure' do
161
+ let(:on_connect) { proc {|client|
162
+ EM::DefaultDeferrable.new.tap {|df| df.fail on_connect_exception.new }
163
+ } }
164
+
165
+ include_context 'test on_connect error'
166
+ end
167
+ end
168
+
169
+ describe '#transaction' do
170
+ let(:concurrency) { 2 }
171
+ let(:options) { {size: concurrency} }
172
+ let(:query) { 'SELECT pg_database_size(current_database());' }
173
+
174
+ around(:each) do |testcase|
175
+ EM.synchrony do
176
+ begin
177
+ @pool = subject.new(options)
178
+ testcase.call
179
+ end
180
+ end
181
+ end
182
+
183
+ it 'should lock transaction connection to fiber' do
184
+ over_count = 2
185
+ @pool.transaction do |pg|
186
+ @pool.hold {|c| c.should be pg }
187
+ Fiber.new do
188
+ @pool.size.should eq 1
189
+ @pool.hold {|c| c.should_not be pg }
190
+ @pool.size.should eq 2
191
+ EM.stop if (over_count-=1).zero?
192
+ end.resume
193
+ @pool.hold {|c| c.should be pg }
194
+ @pool.size.should eq 2
195
+ end
196
+ EM.stop if (over_count-=1).zero?
197
+ end
198
+
199
+ end
200
+ end
@@ -0,0 +1,787 @@
1
+ $:.unshift "lib"
2
+ gem 'eventmachine', '~> 1.0.0'
3
+ gem 'pg', ENV['EM_PG_CLIENT_TEST_PG_VERSION']
4
+ require 'date'
5
+ require 'em-synchrony'
6
+ require 'pg/em'
7
+
8
+ NOTIFY_PAYLOAD = defined?(PG.library_version) && PG.library_version >= 90000
9
+ NOTIFY_PAYLOAD_QUERY = NOTIFY_PAYLOAD ? %q[NOTIFY "ruby-em-pg-client", 'foo'] : %q[NOTIFY "ruby-em-pg-client"]
10
+
11
+ describe PG::EM::Client do
12
+
13
+ it "should be client #{PG::VERSION}" do
14
+ @client.should be_an_instance_of described_class
15
+ end
16
+
17
+ it "should have disabled async_autoreconnect" do
18
+ @client.async_autoreconnect.should be_false
19
+ end
20
+
21
+ it "should enable async_autoreconnect" do
22
+ @client.async_autoreconnect = true
23
+ @client.async_autoreconnect.should be_true
24
+ end
25
+
26
+ it "should have same internal and external encoding" do
27
+ @client.external_encoding.should be @client.internal_encoding
28
+ end
29
+
30
+ it "should begin transaction" do
31
+ @client.query('BEGIN TRANSACTION').should be_an_instance_of PG::Result
32
+ end
33
+
34
+ it "should drop table `foo` if exists" do
35
+ @client.query(
36
+ 'DROP TABLE IF EXISTS foo'
37
+ ).should be_an_instance_of PG::Result
38
+ end
39
+
40
+ it "should create simple table `foo`" do
41
+ @client.query(
42
+ 'CREATE TABLE foo (id integer,cdate timestamp with time zone,data varchar)'
43
+ ).should be_an_instance_of PG::Result
44
+ end
45
+
46
+ it "should populate foo with some data " do
47
+ results = @values.map do |(data, id)|
48
+ @client.exec_params('INSERT INTO foo (id,cdate,data) VALUES($1,$2,$3) returning cdate', [id, DateTime.now, data]) do |result|
49
+ result.should be_an_instance_of PG::Result
50
+ DateTime.parse(result[0]['cdate'])
51
+ end
52
+ end
53
+ @cdates.replace results
54
+ results.length.should == @values.length
55
+ results.each {|r| r.should be_an_instance_of DateTime }
56
+ end
57
+
58
+ it "should create prepared statement" do
59
+ @client.prepare('get_foo',
60
+ 'SELECT * FROM foo order by id'
61
+ ).should be_an_instance_of PG::Result
62
+ end
63
+
64
+ it "should describe prepared statement" do
65
+ @client.describe_prepared('get_foo') do |result|
66
+ result.should be_an_instance_of PG::Result
67
+ result.nfields.should eq 3
68
+ result.fname(0).should eq 'id'
69
+ result.values.should be_empty
70
+ end
71
+ end
72
+
73
+ it "should read foo table with prepared statement" do
74
+ ret = @client.exec_prepared('get_foo') do |result|
75
+ result.should be_an_instance_of PG::Result
76
+ result.each_with_index do |row, i|
77
+ row['id'].to_i.should == i
78
+ DateTime.parse(row['cdate']).should == @cdates[i]
79
+ row['data'].should == @values[i][0]
80
+ end
81
+ result
82
+ end
83
+ ret.should be_an_instance_of PG::Result
84
+ expect { ret.fields }.to raise_error(PG::Error, /result has been cleared/)
85
+ end
86
+
87
+ it "should declare cursor" do
88
+ @client.query(
89
+ 'DECLARE foobar SCROLL CURSOR FOR SELECT * FROM foo'
90
+ ).should be_an_instance_of PG::Result
91
+ end
92
+
93
+ it "should fetch two rows from table" do
94
+ ret = @client.query('FETCH FORWARD 2 FROM foobar') do |result|
95
+ result.should be_an_instance_of PG::Result
96
+ result.nfields.should eq 3
97
+ result.fname(0).should eq 'id'
98
+ result.values.length.should eq 2
99
+ result
100
+ end
101
+ ret.should be_an_instance_of PG::Result
102
+ expect { ret.fields }.to raise_error(PG::Error, /result has been cleared/)
103
+ end
104
+
105
+ it "should describe cursor with describe_portal" do
106
+ @client.describe_portal('foobar') do |result|
107
+ result.should be_an_instance_of PG::Result
108
+ result.nfields.should eq 3
109
+ result.fname(0).should eq 'id'
110
+ end
111
+ end
112
+
113
+ it "should close cursor" do
114
+ @client.query(
115
+ 'CLOSE foobar'
116
+ ).should be_an_instance_of PG::Result
117
+ end
118
+
119
+ if described_class.single_row_mode?
120
+
121
+ it "should get each result in single row mode" do
122
+ @client.single_row_mode?.should be_true
123
+ @client.send_query('SELECT data, id FROM foo order by id')
124
+ @client.set_single_row_mode
125
+ @values.each do |data, id|
126
+ result = @client.get_result
127
+ result.should be_an_instance_of PG::Result
128
+ result.result_status.should eq PG::PGRES_SINGLE_TUPLE
129
+ result.check
130
+ value = result.to_a
131
+ result.clear
132
+ value.should eq [{'data' => data, 'id' => id.to_s}]
133
+ end
134
+ result = @client.get_result
135
+ result.should be_an_instance_of PG::Result
136
+ result.check
137
+ result.result_status.should eq PG::PGRES_TUPLES_OK
138
+ result.to_a.should eq []
139
+ result.clear
140
+ @client.get_result.should be_nil
141
+ end
142
+
143
+ end
144
+
145
+ it "should connect to database asynchronously" do
146
+ this = :first
147
+ Encoding.default_internal = Encoding::ISO_8859_1
148
+ f = Fiber.current
149
+ Fiber.new do
150
+ begin
151
+ result = described_class.new do |conn|
152
+ this = :second
153
+ Encoding.default_internal = nil
154
+ conn.should be_an_instance_of described_class
155
+ conn.external_encoding.should_not eq(conn.internal_encoding)
156
+ conn.internal_encoding.should be Encoding::ISO_8859_1
157
+ conn.get_client_encoding.should eq "LATIN1"
158
+ conn.query('SELECT pg_database_size(current_database());') do |result|
159
+ result.should be_an_instance_of PG::Result
160
+ result[0]['pg_database_size'].to_i.should be > 0
161
+ end
162
+ conn
163
+ end
164
+ result.should be_an_instance_of described_class
165
+ result.finished?.should be_true
166
+ ensure
167
+ f.resume
168
+ end
169
+ end.resume
170
+ this.should be :first
171
+ Fiber.yield
172
+ this.should be :second
173
+ end
174
+
175
+ it "should connect without setting incompatible encoding" do
176
+ this = :first
177
+ Encoding.default_internal = Encoding::Emacs_Mule
178
+ f = Fiber.current
179
+ Fiber.new do
180
+ begin
181
+ conn = described_class.new
182
+ this = :second
183
+ Encoding.default_internal = nil
184
+ conn.should be_an_instance_of described_class
185
+ conn.external_encoding.should be conn.internal_encoding
186
+ conn.finish
187
+ ensure
188
+ f.resume
189
+ end
190
+ end.resume
191
+ this.should be :first
192
+ Fiber.yield
193
+ this.should be :second
194
+ end
195
+
196
+ it "should raise syntax error in misspelled multiple statement" do
197
+ expect {
198
+ @client.query('SELECT * from pg_class; SRELECT CURRENT_TIMESTAMP; SELECT 42 number')
199
+ }.to raise_error(PG::SyntaxError, /syntax error/)
200
+ end
201
+
202
+ it "should rollback transaction" do
203
+ @client.query('ROLLBACK') do |result|
204
+ result.should be_an_instance_of PG::Result
205
+ end
206
+ end
207
+
208
+ it "should return only last statement" do
209
+ @client.query('SELECT * from pg_class; SELECT CURRENT_TIMESTAMP; SELECT 42 number') do |result|
210
+ result.should be_an_instance_of PG::Result
211
+ result[0]['number'].should eq "42"
212
+ end
213
+ end
214
+
215
+ it "should timeout expire while executing query" do
216
+ @client.query_timeout.should eq 0
217
+ @client.query_timeout = 1.5
218
+ @client.query_timeout.should eq 1.5
219
+ start_time = Time.now
220
+ expect {
221
+ @client.query('SELECT pg_sleep(2)')
222
+ }.to raise_error(PG::ConnectionBad, /query timeout expired/)
223
+ (Time.now - start_time).should be < 2
224
+ @client.query_timeout = 0
225
+ @client.query_timeout.should eq 0
226
+ @client.async_command_aborted.should be_true
227
+ @client.status.should be PG::CONNECTION_BAD
228
+ @client.async_autoreconnect = false
229
+ expect {
230
+ @client.query('SELECT 1')
231
+ }.to raise_error(PG::ConnectionBad, /previous query expired/)
232
+ @client.async_autoreconnect = true
233
+ end
234
+
235
+ it "should timeout not expire while executing query with partial results" do
236
+ @client.query_timeout.should eq 0
237
+ @client.query_timeout = 1.1
238
+ @client.query_timeout.should eq 1.1
239
+ start_time = Time.now
240
+ @client.query(
241
+ 'SELECT * from pg_class;' +
242
+ 'SELECT pg_sleep(1);' +
243
+ 'SELECT * from pg_class;' +
244
+ 'SELECT pg_sleep(1);' +
245
+ 'SELECT 42 number') do |result|
246
+ (Time.now - start_time).should be > 2
247
+ result.should be_an_instance_of PG::Result
248
+ result[0]['number'].should eq "42"
249
+ @client.query_timeout = 0
250
+ @client.query_timeout.should eq 0
251
+ @client.async_command_aborted.should be_false
252
+ @client.status.should be PG::CONNECTION_OK
253
+ end
254
+ end
255
+
256
+ it "should timeout expire while executing query with partial results" do
257
+ @client.query_timeout.should eq 0
258
+ @client.query_timeout = 1.1
259
+ @client.query_timeout.should eq 1.1
260
+ start_time = Time.now
261
+ expect {
262
+ @client.query(
263
+ 'SELECT * from pg_class;' +
264
+ 'SELECT pg_sleep(1);' +
265
+ 'SELECT * from pg_class;' +
266
+ 'SELECT pg_sleep(2);' +
267
+ 'SELECT 42 number')
268
+ }.to raise_error(PG::ConnectionBad, /query timeout expired/)
269
+ (Time.now - start_time).should be > 2
270
+ @client.async_command_aborted.should be_true
271
+ @client.status.should be PG::CONNECTION_BAD
272
+ @client.query_timeout = 0
273
+ @client.query_timeout.should eq 0
274
+ end
275
+
276
+ it "should clear connection with blocking reset" do
277
+ @after_em_stop = proc do
278
+ @client.async_command_aborted.should be_true
279
+ @client.status.should be PG::CONNECTION_BAD
280
+ @client.reset
281
+ @client.async_command_aborted.should be_false
282
+ @client.status.should be PG::CONNECTION_OK
283
+ end
284
+ end
285
+
286
+ it "should not expire after executing erraneous query" do
287
+ @client.query_timeout.should eq 0
288
+ @client.query_timeout = 1
289
+ @client.query_timeout.should eq 1
290
+ expect {
291
+ @client.query('SELLECT 1')
292
+ }.to raise_error(PG::SyntaxError, /syntax error/)
293
+ @client.async_command_aborted.should be_false
294
+ @client.status.should be PG::CONNECTION_OK
295
+ ::EM::Synchrony.sleep 1.5
296
+ @client.async_command_aborted.should be_false
297
+ @client.status.should be PG::CONNECTION_OK
298
+ @client.query_timeout = 0
299
+ @client.query_timeout.should eq 0
300
+ end
301
+
302
+ it "should get last result asynchronously" do
303
+ result = @client.get_last_result
304
+ result.should be_nil
305
+ @client.get_last_result.should be_nil
306
+ @client.send_query('SELECT 1; SELECT 2; SELECT 3')
307
+ asynchronous = false
308
+ EM.next_tick { asynchronous = true }
309
+ result = @client.get_last_result
310
+ result.should be_an_instance_of PG::Result
311
+ result.getvalue(0,0).should eq '3'
312
+ result.clear
313
+ @client.get_last_result.should be_nil
314
+ asynchronous.should be true
315
+ end
316
+
317
+ it "should get each result asynchronously" do
318
+ result = @client.get_result
319
+ result.should be_nil
320
+ @client.get_result do |result|
321
+ result.should be_nil
322
+ end.should be_nil
323
+ @client.send_query('SELECT 4,pg_sleep(0.1); SELECT 5; SELECT 6')
324
+ asynchronous = false
325
+ EM.next_tick { asynchronous = true }
326
+ %w[4 5 6].map do |value|
327
+ result = @client.get_result do |result|
328
+ result.should be_an_instance_of PG::Result
329
+ result.check
330
+ result.result_status.should eq PG::PGRES_TUPLES_OK
331
+ result.getvalue(0,0).should eq value
332
+ result
333
+ end
334
+ expect do
335
+ result.clear
336
+ end.to raise_error PG::Error, /cleared/
337
+ value
338
+ end.should eq %w[4 5 6]
339
+ @client.get_result.should be_nil
340
+ asynchronous.should be true
341
+ end
342
+
343
+ it "should receive notification while waiting for it" do
344
+ sender = described_class.new
345
+ notify_flag = false
346
+ result = sender.query('SELECT pg_backend_pid()')
347
+ result.should be_an_instance_of PG::Result
348
+ sender_pid = result.getvalue(0,0).to_i
349
+ @client.query('LISTEN "ruby-em-pg-client"').should be_an_instance_of PG::Result
350
+ f = nil
351
+ EM::Synchrony.next_tick do
352
+ begin
353
+ sender.query(NOTIFY_PAYLOAD_QUERY).should be_an_instance_of PG::Result
354
+ sender.finish
355
+ ensure
356
+ sender = nil
357
+ f.resume if f
358
+ end
359
+ end
360
+ @client.wait_for_notify do |name, pid, payload|
361
+ name.should eq 'ruby-em-pg-client'
362
+ pid.should eq sender_pid
363
+ payload.should eq (NOTIFY_PAYLOAD ? 'foo' : '')
364
+ @client.query('UNLISTEN *').should be_an_instance_of PG::Result
365
+ notify_flag = true
366
+ end.should eq 'ruby-em-pg-client'
367
+ notify_flag.should be_true
368
+ f = Fiber.current
369
+ Fiber.yield if sender
370
+ end
371
+
372
+ it "should receive previously sent notification" do
373
+ notify_flag = false
374
+ @client.query('LISTEN "ruby-em-pg-client"').should be_an_instance_of PG::Result
375
+ result = @client.query('SELECT pg_backend_pid()')
376
+ result.should be_an_instance_of PG::Result
377
+ sender_pid = result.getvalue(0,0).to_i
378
+ @client.query(%q[NOTIFY "ruby-em-pg-client"]).should be_an_instance_of PG::Result
379
+ @client.wait_for_notify(1) do |name, pid, payload|
380
+ name.should eq 'ruby-em-pg-client'
381
+ pid.should eq sender_pid
382
+ payload.should eq ''
383
+ @client.query('UNLISTEN *').should be_an_instance_of PG::Result
384
+ notify_flag = true
385
+ end.should eq 'ruby-em-pg-client'
386
+ notify_flag.should be_true
387
+ end
388
+
389
+ it "should perform queries and receive own notification while waiting for it" do
390
+ f = nil
391
+ sender_pid = nil
392
+ notify_flag = false
393
+ Fiber.new do
394
+ begin
395
+ @client.wait_for_notify do |name, pid, payload|
396
+ name.should eq 'ruby-em-pg-client'
397
+ pid.should eq sender_pid
398
+ payload.should eq (NOTIFY_PAYLOAD ? 'foo' : '')
399
+ notify_flag = true
400
+ end.should eq 'ruby-em-pg-client'
401
+ notify_flag.should be_true
402
+ @client.query('UNLISTEN *').should be_an_instance_of PG::Result
403
+ ensure
404
+ sender_pid = nil
405
+ f.resume if f
406
+ end
407
+ end.resume
408
+ result = @client.query('SELECT pg_backend_pid()')
409
+ result.should be_an_instance_of PG::Result
410
+ sender_pid = result.getvalue(0,0).to_i
411
+ @client.query('LISTEN "ruby-em-pg-client"').should be_an_instance_of PG::Result
412
+ @client.query(NOTIFY_PAYLOAD_QUERY).should be_an_instance_of PG::Result
413
+ f = Fiber.current
414
+ Fiber.yield if sender_pid
415
+ end
416
+
417
+ it "should reach timeout while waiting for notification" do
418
+ start_time = Time.now
419
+ async_flag = false
420
+ EM.next_tick do
421
+ async_flag = true
422
+ end
423
+ @client.wait_for_notify(0.2) do
424
+ raise "This block should not be called"
425
+ end.should be_nil
426
+ (Time.now - start_time).should be >= 0.2
427
+ async_flag.should be_true
428
+ end
429
+
430
+ it "should reach timeout while waiting for notification and executing query" do
431
+ start_time = Time.now
432
+ async_flag = false
433
+ EM.next_tick do
434
+ async_flag = true
435
+ end
436
+ f = Fiber.current
437
+ Fiber.new do
438
+ begin
439
+ @client.query('SELECT pg_sleep(0.5)').should be_an_instance_of PG::Result
440
+ (Time.now - start_time).should be >= 0.5
441
+ async_flag.should be_true
442
+ ensure
443
+ f.resume
444
+ end
445
+ end.resume
446
+ @client.wait_for_notify(0.1) do
447
+ raise "This block should not be called"
448
+ end.should be_nil
449
+ delta = Time.now - start_time
450
+ delta.should be >= 0.1
451
+ delta.should be < 0.5
452
+ async_flag.should be_true
453
+ Fiber.yield
454
+ end
455
+
456
+ it "should timeout expire while executing query and waiting for notification" do
457
+ @client.query_timeout.should eq 0
458
+ @client.query_timeout = 0.2
459
+ @client.query_timeout.should eq 0.2
460
+ f = Fiber.current
461
+ visit_counter = 0
462
+ start_time = Time.now
463
+ Fiber.new do
464
+ expect {
465
+ @client.wait_for_notify do
466
+ raise "This block should not be called"
467
+ end
468
+ }.to raise_error(PG::ConnectionBad, /query timeout expired/)
469
+ (Time.now - start_time).should be >= 0.2
470
+ (visit_counter+=1).should eq 2
471
+ @client.query_timeout = 0
472
+ @client.query_timeout.should eq 0
473
+ @client.async_command_aborted.should be_true
474
+ @client.status.should be PG::CONNECTION_BAD
475
+ @client.async_autoreconnect = false
476
+ expect {
477
+ @client.wait_for_notify do
478
+ raise "This block should not be called"
479
+ end
480
+ }.to raise_error(PG::ConnectionBad, /previous query expired/)
481
+ @client.async_autoreconnect = true
482
+ f.resume
483
+ end.resume
484
+ expect {
485
+ @client.query('SELECT pg_sleep(1)')
486
+ }.to raise_error(PG::ConnectionBad, /query timeout expired/)
487
+ (Time.now - start_time).should be_between(0.2, 1)
488
+ (visit_counter+=1).should eq 1
489
+ Fiber.yield
490
+ @client.reset
491
+ end
492
+
493
+ it "should fail wait_for_notify on connection reset" do
494
+ @client.status.should be PG::CONNECTION_OK
495
+ visit_counter = 0
496
+ Fiber.new do
497
+ expect {
498
+ @client.wait_for_notify do
499
+ raise "This block should not be called"
500
+ end
501
+ }.to raise_error(PG::ConnectionBad, /connection reset/)
502
+ (visit_counter+=1).should eq 1
503
+ end.resume
504
+ @client.reset
505
+ (visit_counter+=1).should eq 2
506
+ end
507
+
508
+ it "should fail wait_for_notify and slow query on connection reset" do
509
+ @client.status.should be PG::CONNECTION_OK
510
+ visit_counter = 0
511
+ Fiber.new do
512
+ expect {
513
+ @client.query('SELECT pg_sleep(10)')
514
+ }.to raise_error(PG::ConnectionBad, /connection reset/)
515
+ (visit_counter+=1).should eq 1
516
+ end.resume
517
+ Fiber.new do
518
+ expect {
519
+ @client.wait_for_notify do
520
+ raise "This block should not be called"
521
+ end
522
+ }.to raise_error(PG::ConnectionBad, /connection reset/)
523
+ (visit_counter+=1).should eq 2
524
+ end.resume
525
+ @client.reset
526
+ (visit_counter+=1).should eq 3
527
+ end
528
+
529
+ describe 'PG::EM::Client#transaction' do
530
+
531
+ before(:all) do
532
+ @client.query_timeout = 0
533
+ @client.query_timeout.should eq 0
534
+ end
535
+
536
+ it "should raise ArgumentError when there is no block" do
537
+ expect do
538
+ @client.transaction
539
+ end.to raise_error(ArgumentError, /Must supply block for PG::EM::Client#transaction/)
540
+ end
541
+
542
+ it "should commit transaction and return whatever block yields" do
543
+ @client.transaction_status.should be PG::PQTRANS_IDLE
544
+ @client.transaction do |pg|
545
+ pg.should be @client
546
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
547
+ @client.instance_variable_get(:@client_tran_count).should eq 1
548
+ @client.query(
549
+ 'DROP TABLE IF EXISTS bar'
550
+ ).should be_an_instance_of PG::Result
551
+ @client.query(
552
+ 'CREATE TABLE bar (key integer, value varchar)'
553
+ ).should be_an_instance_of PG::Result
554
+ @client.query("INSERT INTO bar (key,value) VALUES(42,'xyz') returning value") do |result|
555
+ result.should be_an_instance_of PG::Result
556
+ result[0]['value']
557
+ end
558
+ end.should eq 'xyz'
559
+ @client.query('SELECT * FROM bar') do |result|
560
+ result.should be_an_instance_of PG::Result
561
+ result[0]['key'].should eq '42'
562
+ end
563
+ @client.transaction_status.should be PG::PQTRANS_IDLE
564
+ @client.instance_variable_get(:@client_tran_count).should eq 0
565
+ end
566
+
567
+ it "should rollback transaction on error and raise that error" do
568
+ @client.transaction_status.should be PG::PQTRANS_IDLE
569
+ expect do
570
+ @client.transaction do |pg|
571
+ pg.should be @client
572
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
573
+ @client.instance_variable_get(:@client_tran_count).should eq 1
574
+ @client.query(
575
+ "INSERT INTO bar (key,value) VALUES(11,'abc')"
576
+ ).should be_an_instance_of PG::Result
577
+ @client.query('SELECT * FROM bar ORDER BY key') do |result|
578
+ result.should be_an_instance_of PG::Result
579
+ result[0]['key'].should eq '11'
580
+ end
581
+ @client.query('SELECT count(*) AS count FROM bar') do |result|
582
+ result.should be_an_instance_of PG::Result
583
+ result[0]['count'].should eq '2'
584
+ end
585
+ raise "rollback"
586
+ end
587
+ end.to raise_error(RuntimeError, /rollback/)
588
+ @client.query('SELECT count(*) AS count FROM bar') do |result|
589
+ result.should be_an_instance_of PG::Result
590
+ result[0]['count'].should eq '1'
591
+ end
592
+ @client.transaction_status.should be PG::PQTRANS_IDLE
593
+ @client.instance_variable_get(:@client_tran_count).should eq 0
594
+ end
595
+
596
+ it "should allow nesting transaction and return whatever innermost block yields" do
597
+ @client.transaction_status.should be PG::PQTRANS_IDLE
598
+ @client.transaction do |pg|
599
+ pg.should be @client
600
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
601
+ @client.instance_variable_get(:@client_tran_count).should eq 1
602
+ @client.query(
603
+ "INSERT INTO bar (key,value) VALUES(100,'hundred') returning value"
604
+ ).should be_an_instance_of PG::Result
605
+ @client.transaction do |pg|
606
+ pg.should be @client
607
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
608
+ @client.instance_variable_get(:@client_tran_count).should eq 2
609
+ @client.query(
610
+ "INSERT INTO bar (key,value) VALUES(1000,'thousand') returning value"
611
+ ).should be_an_instance_of PG::Result
612
+ @client.transaction do |pg|
613
+ pg.should be @client
614
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
615
+ @client.instance_variable_get(:@client_tran_count).should eq 3
616
+ @client.query("INSERT INTO bar (key,value) VALUES(1000000,'million') returning value")
617
+ end
618
+ end
619
+ end.tap do |result|
620
+ result.should be_an_instance_of PG::Result
621
+ result[0]['value'].should eq 'million'
622
+ end
623
+ @client.query('SELECT key,value FROM bar ORDER BY key') do |result|
624
+ result.should be_an_instance_of PG::Result
625
+ result.column_values(0).should eq ['42','100','1000','1000000']
626
+ result.column_values(1).should eq ['xyz','hundred','thousand','million']
627
+ end
628
+ @client.transaction_status.should be PG::PQTRANS_IDLE
629
+ @client.instance_variable_get(:@client_tran_count).should eq 0
630
+ end
631
+
632
+ it "should allow nesting transaction and rollback on error" do
633
+ @client.transaction_status.should be PG::PQTRANS_IDLE
634
+ expect do
635
+ @client.transaction do |pg|
636
+ pg.should be @client
637
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
638
+ @client.instance_variable_get(:@client_tran_count).should eq 1
639
+ @client.query(
640
+ "INSERT INTO bar (key,value) VALUES(200,'two hundred') returning value"
641
+ ).should be_an_instance_of PG::Result
642
+ @client.transaction do |pg|
643
+ pg.should be @client
644
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
645
+ @client.instance_variable_get(:@client_tran_count).should eq 2
646
+ @client.query(
647
+ "INSERT INTO bar (key,value) VALUES(2000,'two thousands') returning value"
648
+ ).should be_an_instance_of PG::Result
649
+ @client.transaction do |pg|
650
+ pg.should be @client
651
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
652
+ @client.instance_variable_get(:@client_tran_count).should eq 3
653
+ @client.query(
654
+ "INSERT INTO bar (key,value) VALUES(2000000,'two millions') returning value"
655
+ ).should be_an_instance_of PG::Result
656
+ raise "rollback from here"
657
+ end
658
+ end
659
+ end
660
+ end.to raise_error(RuntimeError, /rollback from here/)
661
+ @client.query('SELECT key,value FROM bar ORDER BY key') do |result|
662
+ result.should be_an_instance_of PG::Result
663
+ result.column_values(0).should eq ['42','100','1000','1000000']
664
+ result.column_values(1).should eq ['xyz','hundred','thousand','million']
665
+ end
666
+ @client.transaction_status.should be PG::PQTRANS_IDLE
667
+ @client.instance_variable_get(:@client_tran_count).should eq 0
668
+ end
669
+
670
+ it "should allow rollback on rescued sql error from nested transaction" do
671
+ flag = false
672
+ @client.transaction_status.should be PG::PQTRANS_IDLE
673
+ @client.transaction do |pg|
674
+ pg.should be @client
675
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
676
+ @client.instance_variable_get(:@client_tran_count).should eq 1
677
+ @client.query(
678
+ "INSERT INTO bar (key,value) VALUES(300,'three hundred') returning value"
679
+ ).should be_an_instance_of PG::Result
680
+ @client.transaction do |pg|
681
+ pg.should be @client
682
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
683
+ @client.instance_variable_get(:@client_tran_count).should eq 2
684
+ @client.query(
685
+ "INSERT INTO bar (key,value) VALUES(3000,'three thousands') returning value"
686
+ ).should be_an_instance_of PG::Result
687
+ @client.transaction do |pg|
688
+ pg.should be @client
689
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
690
+ @client.instance_variable_get(:@client_tran_count).should eq 3
691
+ expect {
692
+ @client.query('SRELECT CURRENT_TIMESTAMP')
693
+ }.to raise_error(PG::SyntaxError, /syntax error/)
694
+ @client.transaction_status.should be PG::PQTRANS_INERROR
695
+ @client.instance_variable_get(:@client_tran_count).should eq 3
696
+ expect {
697
+ @client.query('SELECT CURRENT_TIMESTAMP')
698
+ }.to raise_error(PG::InFailedSqlTransaction, /transaction is aborted/)
699
+ @client.transaction_status.should be PG::PQTRANS_INERROR
700
+ @client.instance_variable_get(:@client_tran_count).should eq 3
701
+ expect do
702
+ @client.transaction { 'foo' }
703
+ end.to raise_error(PG::InFailedSqlTransaction, /transaction is aborted/)
704
+ @client.transaction_status.should be PG::PQTRANS_INERROR
705
+ @client.instance_variable_get(:@client_tran_count).should eq 3
706
+ flag = :was_here
707
+ end
708
+ @client.transaction_status.should be PG::PQTRANS_INERROR
709
+ @client.instance_variable_get(:@client_tran_count).should eq 2
710
+ expect {
711
+ @client.query('SELECT CURRENT_TIMESTAMP')
712
+ }.to raise_error(PG::InFailedSqlTransaction, /transaction is aborted/)
713
+ expect do
714
+ @client.transaction { 'foo' }
715
+ end.to raise_error(PG::InFailedSqlTransaction, /transaction is aborted/)
716
+ @client.transaction_status.should be PG::PQTRANS_INERROR
717
+ @client.instance_variable_get(:@client_tran_count).should eq 2
718
+ end
719
+ @client.transaction_status.should be PG::PQTRANS_INERROR
720
+ @client.instance_variable_get(:@client_tran_count).should eq 1
721
+ expect {
722
+ @client.query('SELECT CURRENT_TIMESTAMP')
723
+ }.to raise_error(PG::InFailedSqlTransaction, /transaction is aborted/)
724
+ expect do
725
+ @client.transaction { 'foo' }
726
+ end.to raise_error(PG::InFailedSqlTransaction, /transaction is aborted/)
727
+ @client.transaction_status.should be PG::PQTRANS_INERROR
728
+ @client.instance_variable_get(:@client_tran_count).should eq 1
729
+ end
730
+ @client.transaction_status.should be PG::PQTRANS_IDLE
731
+ @client.instance_variable_get(:@client_tran_count).should eq 0
732
+ @client.transaction { 'foo' }.should eq 'foo'
733
+ @client.query('SELECT key,value FROM bar ORDER BY key') do |result|
734
+ result.should be_an_instance_of PG::Result
735
+ result.column_values(0).should eq ['42','100','1000','1000000']
736
+ result.column_values(1).should eq ['xyz','hundred','thousand','million']
737
+ end
738
+ flag.should be :was_here
739
+ end
740
+
741
+ it "should detect premature transaction state change" do
742
+ flag = false
743
+ @client.transaction_status.should be PG::PQTRANS_IDLE
744
+ @client.instance_variable_get(:@client_tran_count).should eq 0
745
+ @client.transaction do |pg|
746
+ pg.should be @client
747
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
748
+ @client.query('ROLLBACK')
749
+ @client.instance_variable_get(:@client_tran_count).should eq 1
750
+ @client.transaction_status.should be PG::PQTRANS_IDLE
751
+ @client.transaction do
752
+ @client.transaction_status.should be PG::PQTRANS_INTRANS
753
+ @client.instance_variable_get(:@client_tran_count).should eq 1
754
+ 'foo'
755
+ end.should eq 'foo'
756
+ @client.transaction_status.should be PG::PQTRANS_IDLE
757
+ @client.instance_variable_get(:@client_tran_count).should eq 0
758
+ end
759
+ @client.instance_variable_get(:@client_tran_count).should eq 0
760
+ end
761
+ end
762
+
763
+ around(:each) do |testcase|
764
+ @after_em_stop = nil
765
+ EM.synchrony do
766
+ begin
767
+ testcase.call
768
+ ensure
769
+ EM.stop
770
+ end
771
+ end
772
+ @after_em_stop.call if @after_em_stop
773
+ end
774
+
775
+ before(:all) do
776
+ @cdates = []
777
+ @values = Array(('AA'..'ZZ').each_with_index)
778
+ ENV['PGCLIENTENCODING'] = nil
779
+ Encoding.default_internal = nil
780
+ @client = described_class.new
781
+ end
782
+
783
+ after(:all) do
784
+ @client.close
785
+ end
786
+
787
+ end