cql-rb 1.0.0.pre4 → 1.0.0.pre5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -3
- data/lib/cql/client.rb +65 -15
- data/lib/cql/io.rb +2 -0
- data/lib/cql/io/io_reactor.rb +90 -304
- data/lib/cql/io/node_connection.rb +206 -0
- data/lib/cql/protocol/encoding.rb +1 -0
- data/lib/cql/protocol/request_frame.rb +26 -0
- data/lib/cql/protocol/response_frame.rb +17 -0
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client_spec.rb +95 -56
- data/spec/cql/io/io_reactor_spec.rb +47 -37
- data/spec/cql/protocol/encoding_spec.rb +5 -0
- data/spec/cql/protocol/request_frame_spec.rb +74 -0
- data/spec/cql/protocol/response_frame_spec.rb +18 -0
- data/spec/integration/client_spec.rb +46 -9
- data/spec/integration/protocol_spec.rb +83 -14
- data/spec/integration/regression_spec.rb +2 -2
- data/spec/spec_helper.rb +6 -0
- metadata +3 -2
@@ -0,0 +1,206 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'resolv-replace'
|
5
|
+
|
6
|
+
|
7
|
+
module Cql
|
8
|
+
module Io
|
9
|
+
# @private
|
10
|
+
class NodeConnection
|
11
|
+
def initialize(*args)
|
12
|
+
@host, @port, @connection_timeout = args
|
13
|
+
@connected_future = Future.new
|
14
|
+
@io = nil
|
15
|
+
@addrinfo = nil
|
16
|
+
@write_buffer = ''
|
17
|
+
@read_buffer = ''
|
18
|
+
@current_frame = Protocol::ResponseFrame.new(@read_buffer)
|
19
|
+
@response_tasks = [nil] * 128
|
20
|
+
@event_listeners = Hash.new { |h, k| h[k] = [] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def open
|
24
|
+
@connection_started_at = Time.now
|
25
|
+
begin
|
26
|
+
addrinfo = Socket.getaddrinfo(@host, @port, Socket::AF_INET, Socket::SOCK_STREAM)
|
27
|
+
_, port, _, ip, address_family, socket_type = addrinfo.first
|
28
|
+
@sockaddr = Socket.sockaddr_in(port, ip)
|
29
|
+
@io = Socket.new(address_family, socket_type, 0)
|
30
|
+
@io.connect_nonblock(@sockaddr)
|
31
|
+
rescue Errno::EINPROGRESS
|
32
|
+
# ok
|
33
|
+
rescue SystemCallError, SocketError => e
|
34
|
+
fail_connection!(e)
|
35
|
+
end
|
36
|
+
@connected_future
|
37
|
+
end
|
38
|
+
|
39
|
+
def connection_id
|
40
|
+
self.object_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_io
|
44
|
+
@io
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_event(&listener)
|
48
|
+
@event_listeners[:event] << listener
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_close(&listener)
|
52
|
+
@event_listeners[:close] << listener
|
53
|
+
end
|
54
|
+
|
55
|
+
def connected?
|
56
|
+
@io && !connecting?
|
57
|
+
end
|
58
|
+
|
59
|
+
def connecting?
|
60
|
+
@io && !(@connected_future.complete? || @connected_future.failed?)
|
61
|
+
end
|
62
|
+
|
63
|
+
def closed?
|
64
|
+
@io.nil? && !connecting?
|
65
|
+
end
|
66
|
+
|
67
|
+
def has_capacity?
|
68
|
+
!!next_stream_id && connected?
|
69
|
+
end
|
70
|
+
|
71
|
+
def can_write?
|
72
|
+
@io && (!@write_buffer.empty? || connecting?)
|
73
|
+
end
|
74
|
+
|
75
|
+
def perform_request(request, future)
|
76
|
+
stream_id = next_stream_id
|
77
|
+
Protocol::RequestFrame.new(request, stream_id).write(@write_buffer)
|
78
|
+
@response_tasks[stream_id] = future
|
79
|
+
rescue => e
|
80
|
+
case e
|
81
|
+
when CqlError
|
82
|
+
error = e
|
83
|
+
else
|
84
|
+
error = IoError.new(e.message)
|
85
|
+
error.set_backtrace(e.backtrace)
|
86
|
+
end
|
87
|
+
@response_tasks.delete(stream_id)
|
88
|
+
future.fail!(error)
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_read
|
92
|
+
new_bytes = @io.read_nonblock(2**16)
|
93
|
+
@current_frame << new_bytes
|
94
|
+
while @current_frame.complete?
|
95
|
+
stream_id = @current_frame.stream_id
|
96
|
+
if stream_id == EVENT_STREAM_ID
|
97
|
+
@event_listeners[:event].each { |listener| listener.call(@current_frame.body) }
|
98
|
+
elsif @response_tasks[stream_id]
|
99
|
+
@response_tasks[stream_id].complete!([@current_frame.body, connection_id])
|
100
|
+
@response_tasks[stream_id] = nil
|
101
|
+
else
|
102
|
+
# TODO dropping the request on the floor here, but we didn't send it
|
103
|
+
end
|
104
|
+
@current_frame = Protocol::ResponseFrame.new(@read_buffer)
|
105
|
+
end
|
106
|
+
rescue => e
|
107
|
+
force_close(e)
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_write
|
111
|
+
succeed_connection! if connecting?
|
112
|
+
if !@write_buffer.empty?
|
113
|
+
bytes_written = @io.write_nonblock(@write_buffer)
|
114
|
+
@write_buffer.slice!(0, bytes_written)
|
115
|
+
end
|
116
|
+
rescue => e
|
117
|
+
force_close(e)
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_connecting
|
121
|
+
if connecting_timed_out?
|
122
|
+
fail_connection!(ConnectionTimeoutError.new("Could not connect to #{@host}:#{@port} within #{@connection_timeout}s"))
|
123
|
+
else
|
124
|
+
@io.connect_nonblock(@sockaddr)
|
125
|
+
succeed_connection!
|
126
|
+
end
|
127
|
+
rescue Errno::EISCONN
|
128
|
+
# ok
|
129
|
+
succeed_connection!
|
130
|
+
rescue SystemCallError, SocketError => e
|
131
|
+
fail_connection!(e)
|
132
|
+
end
|
133
|
+
|
134
|
+
def close
|
135
|
+
if @io
|
136
|
+
begin
|
137
|
+
@io.close
|
138
|
+
rescue SystemCallError
|
139
|
+
# nothing to do, it wasn't open
|
140
|
+
end
|
141
|
+
if connecting?
|
142
|
+
succeed_connection!
|
143
|
+
end
|
144
|
+
@io = nil
|
145
|
+
@event_listeners[:close].each { |listener| listener.call(self) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_s
|
150
|
+
state = begin
|
151
|
+
if connected? then 'connected'
|
152
|
+
elsif connecting? then 'connecting'
|
153
|
+
else 'not connected'
|
154
|
+
end
|
155
|
+
end
|
156
|
+
%<NodeConnection(#{@host}:#{@port}, #{state})>
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
EVENT_STREAM_ID = -1
|
162
|
+
|
163
|
+
def connecting_timed_out?
|
164
|
+
(Time.now - @connection_started_at) > @connection_timeout
|
165
|
+
end
|
166
|
+
|
167
|
+
def succeed_connection!
|
168
|
+
@connected_future.complete!(connection_id)
|
169
|
+
end
|
170
|
+
|
171
|
+
def fail_connection!(e)
|
172
|
+
case e
|
173
|
+
when ConnectionError
|
174
|
+
error = e
|
175
|
+
else
|
176
|
+
message = "Could not connect to #{@host}:#{@port}: #{e.message} (#{e.class.name})"
|
177
|
+
error = ConnectionError.new(message)
|
178
|
+
error.set_backtrace(e.backtrace)
|
179
|
+
end
|
180
|
+
@connected_future.fail!(error)
|
181
|
+
force_close(error)
|
182
|
+
end
|
183
|
+
|
184
|
+
def force_close(e)
|
185
|
+
case e
|
186
|
+
when CqlError
|
187
|
+
error = e
|
188
|
+
else
|
189
|
+
error = IoError.new(e.message)
|
190
|
+
error.set_backtrace(e.backtrace)
|
191
|
+
end
|
192
|
+
@response_tasks.each do |listener|
|
193
|
+
listener.fail!(error) if listener
|
194
|
+
end
|
195
|
+
close
|
196
|
+
end
|
197
|
+
|
198
|
+
def next_stream_id
|
199
|
+
@response_tasks.each_with_index do |task, index|
|
200
|
+
return index if task.nil?
|
201
|
+
end
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -49,6 +49,32 @@ module Cql
|
|
49
49
|
COMPRESSION = 'COMPRESSION'.freeze
|
50
50
|
end
|
51
51
|
|
52
|
+
class CredentialsRequest < RequestBody
|
53
|
+
attr_reader :credentials
|
54
|
+
|
55
|
+
def initialize(credentials)
|
56
|
+
super(4)
|
57
|
+
@credentials = credentials.dup.freeze
|
58
|
+
end
|
59
|
+
|
60
|
+
def write(io)
|
61
|
+
write_string_map(io, @credentials)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
%(CREDENTIALS #{@credentials})
|
66
|
+
end
|
67
|
+
|
68
|
+
def eql?(rq)
|
69
|
+
self.class === rq && rq.credentials.eql?(@credentials)
|
70
|
+
end
|
71
|
+
alias_method :==, :eql?
|
72
|
+
|
73
|
+
def hash
|
74
|
+
@h ||= @credentials.hash
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
52
78
|
class OptionsRequest < RequestBody
|
53
79
|
def initialize
|
54
80
|
super(5)
|
@@ -55,6 +55,7 @@ module Cql
|
|
55
55
|
case @headers.opcode
|
56
56
|
when 0x00 then ErrorResponse
|
57
57
|
when 0x02 then ReadyResponse
|
58
|
+
when 0x03 then AuthenticateResponse
|
58
59
|
when 0x06 then SupportedResponse
|
59
60
|
when 0x08 then ResultResponse
|
60
61
|
when 0x0c then EventResponse
|
@@ -214,6 +215,22 @@ module Cql
|
|
214
215
|
end
|
215
216
|
end
|
216
217
|
|
218
|
+
class AuthenticateResponse < ResponseBody
|
219
|
+
attr_reader :authentication_class
|
220
|
+
|
221
|
+
def self.decode!(buffer)
|
222
|
+
new(read_string!(buffer))
|
223
|
+
end
|
224
|
+
|
225
|
+
def initialize(authentication_class)
|
226
|
+
@authentication_class = authentication_class
|
227
|
+
end
|
228
|
+
|
229
|
+
def to_s
|
230
|
+
%(AUTHENTICATE #{authentication_class})
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
217
234
|
class SupportedResponse < ResponseBody
|
218
235
|
attr_reader :options
|
219
236
|
|
data/lib/cql/version.rb
CHANGED
data/spec/cql/client_spec.rb
CHANGED
@@ -33,114 +33,153 @@ module Cql
|
|
33
33
|
requests.last
|
34
34
|
end
|
35
35
|
|
36
|
-
describe '
|
36
|
+
describe '.connect' do
|
37
|
+
it 'connects and returns the client' do
|
38
|
+
client = described_class.connect(connection_options)
|
39
|
+
client.should be_connected
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#connect' do
|
37
44
|
it 'connects' do
|
38
|
-
client.
|
45
|
+
client.connect
|
39
46
|
connections.should have(1).item
|
40
47
|
end
|
41
48
|
|
42
49
|
it 'connects only once' do
|
43
|
-
client.
|
44
|
-
client.
|
50
|
+
client.connect
|
51
|
+
client.connect
|
45
52
|
connections.should have(1).item
|
46
53
|
end
|
47
54
|
|
48
55
|
it 'connects to all hosts' do
|
49
|
-
client.
|
56
|
+
client.close
|
50
57
|
io_reactor.stop.get
|
51
58
|
io_reactor.start.get
|
52
59
|
|
53
60
|
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
54
|
-
c.
|
61
|
+
c.connect
|
55
62
|
connections.should have(3).items
|
56
63
|
end
|
57
64
|
|
58
65
|
it 'returns itself' do
|
59
|
-
client.
|
66
|
+
client.connect.should equal(client)
|
60
67
|
end
|
61
68
|
|
62
69
|
it 'forwards the host and port' do
|
63
|
-
client.
|
70
|
+
client.connect
|
64
71
|
connection[:host].should == 'example.com'
|
65
72
|
connection[:port].should == 12321
|
66
73
|
end
|
67
74
|
|
68
75
|
it 'sends a startup request' do
|
69
|
-
client.
|
76
|
+
client.connect
|
70
77
|
last_request.should be_a(Protocol::StartupRequest)
|
71
78
|
end
|
72
79
|
|
73
80
|
it 'sends a startup request to each connection' do
|
74
|
-
client.
|
81
|
+
client.close
|
75
82
|
io_reactor.stop.get
|
76
83
|
io_reactor.start.get
|
77
84
|
|
78
85
|
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
79
|
-
c.
|
86
|
+
c.connect
|
80
87
|
connections.each do |cc|
|
81
88
|
cc[:requests].last.should be_a(Protocol::StartupRequest)
|
82
89
|
end
|
83
90
|
end
|
84
91
|
|
85
92
|
it 'is not in a keyspace' do
|
86
|
-
client.
|
93
|
+
client.connect
|
87
94
|
client.keyspace.should be_nil
|
88
95
|
end
|
89
96
|
|
90
97
|
it 'changes to the keyspace given as an option' do
|
91
98
|
c = described_class.new(connection_options.merge(:keyspace => 'hello_world'))
|
92
|
-
c.
|
99
|
+
c.connect
|
93
100
|
last_request.should == Protocol::QueryRequest.new('USE hello_world', :one)
|
94
101
|
end
|
95
102
|
|
96
103
|
it 'validates the keyspace name before sending the USE command' do
|
97
104
|
c = described_class.new(connection_options.merge(:keyspace => 'system; DROP KEYSPACE system'))
|
98
|
-
expect { c.
|
105
|
+
expect { c.connect }.to raise_error(Client::InvalidKeyspaceNameError)
|
99
106
|
requests.should_not include(Protocol::QueryRequest.new('USE system; DROP KEYSPACE system', :one))
|
100
107
|
end
|
101
108
|
|
102
109
|
it 're-raises any errors raised' do
|
103
110
|
io_reactor.stub(:add_connection).and_raise(ArgumentError)
|
104
|
-
expect { client.
|
111
|
+
expect { client.connect }.to raise_error(ArgumentError)
|
105
112
|
end
|
106
113
|
|
107
114
|
it 'is not connected if an error is raised' do
|
108
115
|
io_reactor.stub(:add_connection).and_raise(ArgumentError)
|
109
|
-
client.
|
116
|
+
client.connect rescue nil
|
110
117
|
client.should_not be_connected
|
118
|
+
io_reactor.should_not be_running
|
111
119
|
end
|
112
120
|
|
113
|
-
it 'is connected after #
|
114
|
-
client.
|
121
|
+
it 'is connected after #connect returns' do
|
122
|
+
client.connect
|
115
123
|
client.should be_connected
|
116
124
|
end
|
125
|
+
|
126
|
+
context 'when the server requests authentication' do
|
127
|
+
before do
|
128
|
+
io_reactor.queue_response(Protocol::AuthenticateResponse.new('com.example.Auth'))
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'sends credentials' do
|
132
|
+
client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
|
133
|
+
client.connect
|
134
|
+
last_request.should == Protocol::CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'raises an error when no credentials have been given' do
|
138
|
+
client = described_class.new(connection_options)
|
139
|
+
expect { client.connect }.to raise_error(AuthenticationError)
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'raises an error when the server responds with an error to the credentials request' do
|
143
|
+
io_reactor.queue_response(Protocol::ErrorResponse.new(256, 'No way, José'))
|
144
|
+
client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
|
145
|
+
expect { client.connect }.to raise_error(AuthenticationError)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'shuts down the client when there is an authentication error' do
|
149
|
+
io_reactor.queue_response(Protocol::ErrorResponse.new(256, 'No way, José'))
|
150
|
+
client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
|
151
|
+
client.connect rescue nil
|
152
|
+
client.should_not be_connected
|
153
|
+
io_reactor.should_not be_running
|
154
|
+
end
|
155
|
+
end
|
117
156
|
end
|
118
157
|
|
119
|
-
describe '#
|
158
|
+
describe '#close' do
|
120
159
|
it 'closes the connection' do
|
121
|
-
client.
|
122
|
-
client.
|
160
|
+
client.connect
|
161
|
+
client.close
|
123
162
|
io_reactor.should_not be_running
|
124
163
|
end
|
125
164
|
|
126
|
-
it 'does nothing when called before #
|
127
|
-
client.
|
165
|
+
it 'does nothing when called before #connect' do
|
166
|
+
client.close
|
128
167
|
end
|
129
168
|
|
130
|
-
it 'accepts multiple calls to #
|
131
|
-
client.
|
132
|
-
client.
|
133
|
-
client.
|
169
|
+
it 'accepts multiple calls to #close' do
|
170
|
+
client.connect
|
171
|
+
client.close
|
172
|
+
client.close
|
134
173
|
end
|
135
174
|
|
136
175
|
it 'returns itself' do
|
137
|
-
client.
|
176
|
+
client.connect.close.should equal(client)
|
138
177
|
end
|
139
178
|
end
|
140
179
|
|
141
180
|
describe '#use' do
|
142
181
|
before do
|
143
|
-
client.
|
182
|
+
client.connect
|
144
183
|
end
|
145
184
|
|
146
185
|
it 'executes a USE query' do
|
@@ -150,12 +189,12 @@ module Cql
|
|
150
189
|
end
|
151
190
|
|
152
191
|
it 'executes a USE query for each connection' do
|
153
|
-
client.
|
192
|
+
client.close
|
154
193
|
io_reactor.stop.get
|
155
194
|
io_reactor.start.get
|
156
195
|
|
157
196
|
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
158
|
-
c.
|
197
|
+
c.connect
|
159
198
|
|
160
199
|
c.use('system')
|
161
200
|
last_requests = connections.select { |c| c[:host] =~ /^h\d\.example\.com$/ }.sort_by { |c| c[:host] }.map { |c| c[:requests].last }
|
@@ -179,7 +218,7 @@ module Cql
|
|
179
218
|
|
180
219
|
describe '#execute' do
|
181
220
|
before do
|
182
|
-
client.
|
221
|
+
client.connect
|
183
222
|
end
|
184
223
|
|
185
224
|
it 'asks the connection to execute the query' do
|
@@ -214,12 +253,12 @@ module Cql
|
|
214
253
|
end
|
215
254
|
|
216
255
|
it 'detects that one connection changed to a keyspace and changes the others too' do
|
217
|
-
client.
|
256
|
+
client.close
|
218
257
|
io_reactor.stop.get
|
219
258
|
io_reactor.start.get
|
220
259
|
|
221
260
|
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
222
|
-
c.
|
261
|
+
c.connect
|
223
262
|
|
224
263
|
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h1.example.com' }[:host])
|
225
264
|
io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h2.example.com' }[:host])
|
@@ -299,7 +338,7 @@ module Cql
|
|
299
338
|
end
|
300
339
|
|
301
340
|
before do
|
302
|
-
client.
|
341
|
+
client.connect
|
303
342
|
end
|
304
343
|
|
305
344
|
it 'sends a prepare request' do
|
@@ -334,12 +373,12 @@ module Cql
|
|
334
373
|
end
|
335
374
|
|
336
375
|
it 'executes a prepared statement using the right connection' do
|
337
|
-
client.
|
376
|
+
client.close
|
338
377
|
io_reactor.stop.get
|
339
378
|
io_reactor.start.get
|
340
379
|
|
341
380
|
c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
|
342
|
-
c.
|
381
|
+
c.connect
|
343
382
|
|
344
383
|
io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, metadata))
|
345
384
|
io_reactor.queue_response(Protocol::PreparedResultResponse.new('B' * 32, metadata))
|
@@ -363,51 +402,51 @@ module Cql
|
|
363
402
|
end
|
364
403
|
|
365
404
|
context 'when not connected' do
|
366
|
-
it 'is not connected before #
|
405
|
+
it 'is not connected before #connect has been called' do
|
367
406
|
client.should_not be_connected
|
368
407
|
end
|
369
408
|
|
370
|
-
it 'is not connected after #
|
371
|
-
client.
|
372
|
-
client.
|
409
|
+
it 'is not connected after #close has been called' do
|
410
|
+
client.connect
|
411
|
+
client.close
|
373
412
|
client.should_not be_connected
|
374
413
|
end
|
375
414
|
|
376
|
-
it 'complains when #use is called before #
|
415
|
+
it 'complains when #use is called before #connect' do
|
377
416
|
expect { client.use('system') }.to raise_error(Client::NotConnectedError)
|
378
417
|
end
|
379
418
|
|
380
|
-
it 'complains when #use is called after #
|
381
|
-
client.
|
382
|
-
client.
|
419
|
+
it 'complains when #use is called after #close' do
|
420
|
+
client.connect
|
421
|
+
client.close
|
383
422
|
expect { client.use('system') }.to raise_error(Client::NotConnectedError)
|
384
423
|
end
|
385
424
|
|
386
|
-
it 'complains when #execute is called before #
|
425
|
+
it 'complains when #execute is called before #connect' do
|
387
426
|
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
388
427
|
end
|
389
428
|
|
390
|
-
it 'complains when #execute is called after #
|
391
|
-
client.
|
392
|
-
client.
|
429
|
+
it 'complains when #execute is called after #close' do
|
430
|
+
client.connect
|
431
|
+
client.close
|
393
432
|
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
394
433
|
end
|
395
434
|
|
396
|
-
it 'complains when #prepare is called before #
|
435
|
+
it 'complains when #prepare is called before #connect' do
|
397
436
|
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
398
437
|
end
|
399
438
|
|
400
|
-
it 'complains when #prepare is called after #
|
401
|
-
client.
|
402
|
-
client.
|
439
|
+
it 'complains when #prepare is called after #close' do
|
440
|
+
client.connect
|
441
|
+
client.close
|
403
442
|
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
404
443
|
end
|
405
444
|
|
406
|
-
it 'complains when #execute of a prepared statement is called after #
|
407
|
-
client.
|
445
|
+
it 'complains when #execute of a prepared statement is called after #close' do
|
446
|
+
client.connect
|
408
447
|
io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, []))
|
409
448
|
statement = client.prepare('DELETE FROM stuff WHERE id = 3')
|
410
|
-
client.
|
449
|
+
client.close
|
411
450
|
expect { statement.execute }.to raise_error(Client::NotConnectedError)
|
412
451
|
end
|
413
452
|
end
|