em-pg-client-12 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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