cql-rb 1.0.0.pre0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +13 -0
- data/bin/cqlexec +135 -0
- data/lib/cql.rb +11 -0
- data/lib/cql/client.rb +196 -0
- data/lib/cql/future.rb +176 -0
- data/lib/cql/io.rb +13 -0
- data/lib/cql/io/io_reactor.rb +351 -0
- data/lib/cql/protocol.rb +39 -0
- data/lib/cql/protocol/decoding.rb +156 -0
- data/lib/cql/protocol/encoding.rb +109 -0
- data/lib/cql/protocol/request_frame.rb +228 -0
- data/lib/cql/protocol/response_frame.rb +551 -0
- data/lib/cql/uuid.rb +46 -0
- data/lib/cql/version.rb +5 -0
- data/spec/cql/client_spec.rb +368 -0
- data/spec/cql/future_spec.rb +297 -0
- data/spec/cql/io/io_reactor_spec.rb +290 -0
- data/spec/cql/protocol/decoding_spec.rb +464 -0
- data/spec/cql/protocol/encoding_spec.rb +338 -0
- data/spec/cql/protocol/request_frame_spec.rb +359 -0
- data/spec/cql/protocol/response_frame_spec.rb +746 -0
- data/spec/cql/uuid_spec.rb +40 -0
- data/spec/integration/client_spec.rb +101 -0
- data/spec/integration/protocol_spec.rb +326 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/fake_io_reactor.rb +55 -0
- data/spec/support/fake_server.rb +95 -0
- metadata +87 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
|
6
|
+
module Cql
|
7
|
+
describe Uuid do
|
8
|
+
describe '#initialize' do
|
9
|
+
it 'can be created from a string' do
|
10
|
+
Uuid.new('a4a70900-24e1-11df-8924-001ff3591711').to_s.should == 'a4a70900-24e1-11df-8924-001ff3591711'
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be created from a number' do
|
14
|
+
Uuid.new(276263553384940695775376958868900023510).to_s.should == 'cfd66ccc-d857-4e90-b1e5-df98a3d40cd6'.force_encoding(::Encoding::ASCII)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#eql?' do
|
19
|
+
it 'is equal to another Uuid with the same value' do
|
20
|
+
Uuid.new(276263553384940695775376958868900023510).should eql(Uuid.new('cfd66ccc-d857-4e90-b1e5-df98a3d40cd6'))
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'aliases #== to #eql?' do
|
24
|
+
Uuid.new(276263553384940695775376958868900023510).should == Uuid.new('cfd66ccc-d857-4e90-b1e5-df98a3d40cd6')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#to_s' do
|
29
|
+
it 'returns a UUID standard format' do
|
30
|
+
Uuid.new('a4a70900-24e1-11df-8924-001ff3591711').to_s.should == 'a4a70900-24e1-11df-8924-001ff3591711'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#value' do
|
35
|
+
it 'returns the numeric value' do
|
36
|
+
Uuid.new('cfd66ccc-d857-4e90-b1e5-df98a3d40cd6').value.should == 276263553384940695775376958868900023510
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
|
6
|
+
describe 'A CQL client' do
|
7
|
+
let :connection_options do
|
8
|
+
{:host => ENV['CASSANDRA_HOST']}
|
9
|
+
end
|
10
|
+
|
11
|
+
let :client do
|
12
|
+
Cql::Client.new(connection_options)
|
13
|
+
end
|
14
|
+
|
15
|
+
before do
|
16
|
+
client.start!
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
client.shutdown!
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'executes a query and returns the result' do
|
24
|
+
result = client.execute('SELECT * FROM system.schema_keyspaces')
|
25
|
+
result.should_not be_empty
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'knows which keyspace it\'s in' do
|
29
|
+
client.use('system')
|
30
|
+
client.keyspace.should == 'system'
|
31
|
+
client.use('system_auth')
|
32
|
+
client.keyspace.should == 'system_auth'
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is not in a keyspace initially' do
|
36
|
+
client.keyspace.should be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'can be initialized with a keyspace' do
|
40
|
+
c = Cql::Client.new(connection_options.merge(:keyspace => 'system'))
|
41
|
+
c.start!
|
42
|
+
begin
|
43
|
+
c.keyspace.should == 'system'
|
44
|
+
expect { c.execute('SELECT * FROM schema_keyspaces') }.to_not raise_error
|
45
|
+
ensure
|
46
|
+
c.shutdown!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'prepares a statement' do
|
51
|
+
statement = client.prepare('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?')
|
52
|
+
statement.should_not be_nil
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'executes a prepared statement' do
|
56
|
+
statement = client.prepare('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?')
|
57
|
+
result = statement.execute('system')
|
58
|
+
result.should have(1).item
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with multiple connections' do
|
62
|
+
let :multi_client do
|
63
|
+
opts = connection_options.dup
|
64
|
+
opts[:host] = ([opts[:host]] * 10).join(',')
|
65
|
+
Cql::Client.new(opts)
|
66
|
+
end
|
67
|
+
|
68
|
+
before do
|
69
|
+
client.shutdown!
|
70
|
+
multi_client.start!
|
71
|
+
end
|
72
|
+
|
73
|
+
after do
|
74
|
+
multi_client.shutdown!
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'handles keyspace changes with #use' do
|
78
|
+
multi_client.use('system')
|
79
|
+
100.times do
|
80
|
+
result = multi_client.execute(%<SELECT * FROM schema_keyspaces WHERE keyspace_name = 'system'>)
|
81
|
+
result.should have(1).item
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'handles keyspace changes with #execute' do
|
86
|
+
multi_client.execute('USE system')
|
87
|
+
100.times do
|
88
|
+
result = multi_client.execute(%<SELECT * FROM schema_keyspaces WHERE keyspace_name = 'system'>)
|
89
|
+
result.should have(1).item
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'executes a prepared statement' do
|
94
|
+
statement = multi_client.prepare('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?')
|
95
|
+
100.times do
|
96
|
+
result = statement.execute('system')
|
97
|
+
result.should have(1).item
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
|
6
|
+
describe 'Protocol parsing and communication' do
|
7
|
+
let :io_reactor do
|
8
|
+
ir = Cql::Io::IoReactor.new
|
9
|
+
ir.start
|
10
|
+
ir.add_connection(ENV['CASSANDRA_HOST'], 9042).get
|
11
|
+
ir
|
12
|
+
end
|
13
|
+
|
14
|
+
let :keyspace_name do
|
15
|
+
"cql_rb_#{rand(1000)}"
|
16
|
+
end
|
17
|
+
|
18
|
+
after do
|
19
|
+
io_reactor.stop.get if io_reactor.running?
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute_request(request)
|
23
|
+
io_reactor.queue_request(request).get.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def query(cql, consistency=:one)
|
27
|
+
response = execute_request(Cql::Protocol::QueryRequest.new(cql, consistency))
|
28
|
+
raise response.to_s if response.is_a?(Cql::Protocol::ErrorResponse)
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_keyspace!
|
33
|
+
query("CREATE KEYSPACE #{keyspace_name} WITH REPLICATION = {'CLASS': 'SimpleStrategy', 'replication_factor': 1}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def use_keyspace!
|
37
|
+
query("USE #{keyspace_name}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def drop_keyspace!
|
41
|
+
query("DROP KEYSPACE #{keyspace_name}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_table!
|
45
|
+
query('CREATE TABLE users (user_name VARCHAR, password VARCHAR, email VARCHAR, PRIMARY KEY (user_name))')
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_counters_table!
|
49
|
+
query('CREATE TABLE counters (id VARCHAR, c1 COUNTER, c2 COUNTER, PRIMARY KEY (id))')
|
50
|
+
end
|
51
|
+
|
52
|
+
def in_keyspace
|
53
|
+
create_keyspace!
|
54
|
+
use_keyspace!
|
55
|
+
begin
|
56
|
+
yield
|
57
|
+
ensure
|
58
|
+
begin
|
59
|
+
drop_keyspace!
|
60
|
+
rescue Errno::EPIPE => e
|
61
|
+
# ignore since we're shutting down
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def in_keyspace_with_table
|
67
|
+
in_keyspace do
|
68
|
+
create_table!
|
69
|
+
yield
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def in_keyspace_with_counters_table
|
74
|
+
in_keyspace do
|
75
|
+
create_counters_table!
|
76
|
+
yield
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'when setting up' do
|
81
|
+
it 'sends OPTIONS and receives SUPPORTED' do
|
82
|
+
response = execute_request(Cql::Protocol::OptionsRequest.new)
|
83
|
+
response.options.should include('CQL_VERSION' => ['3.0.0'])
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'sends STARTUP and receives READY' do
|
87
|
+
response = execute_request(Cql::Protocol::StartupRequest.new)
|
88
|
+
response.should be_a(Cql::Protocol::ReadyResponse)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'sends a bad STARTUP and receives ERROR' do
|
92
|
+
response = execute_request(Cql::Protocol::StartupRequest.new('9.9.9'))
|
93
|
+
response.code.should == 10
|
94
|
+
response.message.should include('not supported')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when set up' do
|
99
|
+
before do
|
100
|
+
response = execute_request(Cql::Protocol::StartupRequest.new)
|
101
|
+
response
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'with events' do
|
105
|
+
it 'sends a REGISTER request and receives READY' do
|
106
|
+
response = execute_request(Cql::Protocol::RegisterRequest.new('TOPOLOGY_CHANGE', 'STATUS_CHANGE', 'SCHEMA_CHANGE'))
|
107
|
+
response.should be_a(Cql::Protocol::ReadyResponse)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'passes events to listeners' do
|
111
|
+
semaphore = Queue.new
|
112
|
+
event = nil
|
113
|
+
execute_request(Cql::Protocol::RegisterRequest.new('SCHEMA_CHANGE'))
|
114
|
+
io_reactor.add_event_listener do |event_response|
|
115
|
+
event = event_response
|
116
|
+
semaphore << :ping
|
117
|
+
end
|
118
|
+
begin
|
119
|
+
create_keyspace!
|
120
|
+
semaphore.pop
|
121
|
+
event.change.should == 'CREATED'
|
122
|
+
event.keyspace.should == keyspace_name
|
123
|
+
ensure
|
124
|
+
drop_keyspace!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when running queries' do
|
130
|
+
context 'with QUERY requests' do
|
131
|
+
it 'sends a USE command' do
|
132
|
+
response = query('USE system', :one)
|
133
|
+
response.keyspace.should == 'system'
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'sends a bad CQL string and receives ERROR' do
|
137
|
+
response = execute_request(Cql::Protocol::QueryRequest.new('HELLO WORLD', :any))
|
138
|
+
response.should be_a(Cql::Protocol::ErrorResponse)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'sends a CREATE KEYSPACE command' do
|
142
|
+
response = query("CREATE KEYSPACE #{keyspace_name} WITH REPLICATION = {'CLASS': 'SimpleStrategy', 'replication_factor': 1}")
|
143
|
+
begin
|
144
|
+
response.change.should == 'CREATED'
|
145
|
+
response.keyspace.should == keyspace_name
|
146
|
+
ensure
|
147
|
+
drop_keyspace!
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'sends a DROP KEYSPACE command' do
|
152
|
+
create_keyspace!
|
153
|
+
use_keyspace!
|
154
|
+
response = query("DROP KEYSPACE #{keyspace_name}")
|
155
|
+
response.change.should == 'DROPPED'
|
156
|
+
response.keyspace.should == keyspace_name
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'sends an ALTER KEYSPACE command' do
|
160
|
+
create_keyspace!
|
161
|
+
begin
|
162
|
+
response = query("ALTER KEYSPACE #{keyspace_name} WITH DURABLE_WRITES = false")
|
163
|
+
response.change.should == 'UPDATED'
|
164
|
+
response.keyspace.should == keyspace_name
|
165
|
+
ensure
|
166
|
+
drop_keyspace!
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'sends a CREATE TABLE command' do
|
171
|
+
in_keyspace do
|
172
|
+
response = query('CREATE TABLE users (user_name VARCHAR, password VARCHAR, email VARCHAR, PRIMARY KEY (user_name))')
|
173
|
+
response.change.should == 'CREATED'
|
174
|
+
response.keyspace.should == keyspace_name
|
175
|
+
response.table.should == 'users'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'sends a DROP TABLE command' do
|
180
|
+
in_keyspace_with_table do
|
181
|
+
response = query('DROP TABLE users')
|
182
|
+
response.change.should == 'DROPPED'
|
183
|
+
response.keyspace.should == keyspace_name
|
184
|
+
response.table.should == 'users'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'sends an ALTER TABLE command' do
|
189
|
+
in_keyspace_with_table do
|
190
|
+
response = query('ALTER TABLE users ADD age INT')
|
191
|
+
response.change.should == 'UPDATED'
|
192
|
+
response.keyspace.should == keyspace_name
|
193
|
+
response.table.should == 'users'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'sends an INSERT command' do
|
198
|
+
in_keyspace_with_table do
|
199
|
+
response = query(%<INSERT INTO users (user_name, email) VALUES ('phil', 'phil@heck.com')>)
|
200
|
+
response.should be_void
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'sends an UPDATE command' do
|
205
|
+
in_keyspace_with_table do
|
206
|
+
query(%<INSERT INTO users (user_name, email) VALUES ('phil', 'phil@heck.com')>)
|
207
|
+
response = query(%<UPDATE users SET email = 'sue@heck.com' WHERE user_name = 'phil'>)
|
208
|
+
response.should be_void
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'increments a counter' do
|
213
|
+
in_keyspace_with_counters_table do
|
214
|
+
response = query(%<UPDATE counters SET c1 = c1 + 1, c2 = c2 - 2 WHERE id = 'stuff'>)
|
215
|
+
response.should be_void
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'sends a DELETE command' do
|
220
|
+
in_keyspace_with_table do
|
221
|
+
response = query(%<DELETE email FROM users WHERE user_name = 'sue'>)
|
222
|
+
response.should be_void
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'sends a TRUNCATE command' do
|
227
|
+
in_keyspace_with_table do
|
228
|
+
response = query(%<TRUNCATE users>)
|
229
|
+
response.should be_void
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'sends a BATCH command' do
|
234
|
+
in_keyspace_with_table do
|
235
|
+
response = query(<<-EOQ)
|
236
|
+
BEGIN BATCH
|
237
|
+
INSERT INTO users (user_name, email) VALUES ('phil', 'phil@heck.com')
|
238
|
+
INSERT INTO users (user_name, email) VALUES ('sue', 'sue@inter.net')
|
239
|
+
APPLY BATCH
|
240
|
+
EOQ
|
241
|
+
response.should be_void
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'sends a SELECT command' do
|
246
|
+
in_keyspace_with_table do
|
247
|
+
query(%<INSERT INTO users (user_name, email) VALUES ('phil', 'phil@heck.com')>)
|
248
|
+
query(%<INSERT INTO users (user_name, email) VALUES ('sue', 'sue@inter.net')>)
|
249
|
+
response = query(%<SELECT * FROM users>, :quorum)
|
250
|
+
response.rows.should == [
|
251
|
+
{'user_name' => 'phil', 'email' => 'phil@heck.com', 'password' => nil},
|
252
|
+
{'user_name' => 'sue', 'email' => 'sue@inter.net', 'password' => nil}
|
253
|
+
]
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
context 'with PREPARE requests' do
|
259
|
+
it 'sends a PREPARE request and receives RESULT' do
|
260
|
+
in_keyspace_with_table do
|
261
|
+
response = execute_request(Cql::Protocol::PrepareRequest.new('SELECT * FROM users WHERE user_name = ?'))
|
262
|
+
response.id.should_not be_nil
|
263
|
+
response.metadata.should_not be_nil
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'sends an EXECUTE request and receives RESULT' do
|
268
|
+
in_keyspace do
|
269
|
+
create_table_cql = %<CREATE TABLE stuff (id1 UUID, id2 VARINT, id3 TIMESTAMP, value1 DOUBLE, value2 TIMEUUID, value3 BLOB, PRIMARY KEY (id1, id2, id3))>
|
270
|
+
insert_cql = %<INSERT INTO stuff (id1, id2, id3, value1, value2, value3) VALUES (?, ?, ?, ?, ?, ?)>
|
271
|
+
create_response = execute_request(Cql::Protocol::QueryRequest.new(create_table_cql, :one))
|
272
|
+
create_response.should_not be_a(Cql::Protocol::ErrorResponse)
|
273
|
+
prepare_response = execute_request(Cql::Protocol::PrepareRequest.new(insert_cql))
|
274
|
+
prepare_response.should_not be_a(Cql::Protocol::ErrorResponse)
|
275
|
+
execute_response = execute_request(Cql::Protocol::ExecuteRequest.new(prepare_response.id, prepare_response.metadata, [Cql::Uuid.new('cfd66ccc-d857-4e90-b1e5-df98a3d40cd6'), -12312312312, Time.now, 345345.234234, Cql::Uuid.new('a4a70900-24e1-11df-8924-001ff3591711'), "\xab\xcd\xef".force_encoding(::Encoding::BINARY)], :one))
|
276
|
+
execute_response.should_not be_a(Cql::Protocol::ErrorResponse)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
context 'with pipelining' do
|
282
|
+
it 'handles multiple concurrent requests' do
|
283
|
+
in_keyspace_with_table do
|
284
|
+
futures = 10.times.map do
|
285
|
+
io_reactor.queue_request(Cql::Protocol::QueryRequest.new('SELECT * FROM users', :quorum))
|
286
|
+
end
|
287
|
+
|
288
|
+
futures << io_reactor.queue_request(Cql::Protocol::QueryRequest.new(%<INSERT INTO users (user_name, email) VALUES ('sam', 'sam@ham.com')>, :one))
|
289
|
+
|
290
|
+
Cql::Future.combine(*futures).get
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
it 'handles lots of concurrent requests' do
|
295
|
+
in_keyspace_with_table do
|
296
|
+
futures = 2000.times.map do
|
297
|
+
io_reactor.queue_request(Cql::Protocol::QueryRequest.new('SELECT * FROM users', :quorum))
|
298
|
+
end
|
299
|
+
|
300
|
+
Cql::Future.combine(*futures).get
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context 'in special circumstances' do
|
308
|
+
it 'raises an exception when it cannot connect to Cassandra' do
|
309
|
+
io_reactor = Cql::Io::IoReactor.new(connection_timeout: 0.1)
|
310
|
+
io_reactor.start.get
|
311
|
+
expect { io_reactor.add_connection('example.com', 9042).get }.to raise_error(Cql::Io::ConnectionError)
|
312
|
+
expect { io_reactor.add_connection('blackhole', 9042).get }.to raise_error(Cql::Io::ConnectionError)
|
313
|
+
io_reactor.stop.get
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'does nothing the second time #start is called' do
|
317
|
+
io_reactor = Cql::Io::IoReactor.new
|
318
|
+
io_reactor.start.get
|
319
|
+
io_reactor.add_connection('localhost', 9042)
|
320
|
+
io_reactor.queue_request(Cql::Protocol::StartupRequest.new).get
|
321
|
+
io_reactor.start.get
|
322
|
+
response = io_reactor.queue_request(Cql::Protocol::QueryRequest.new('USE system', :any)).get
|
323
|
+
response.should_not be_a(Cql::Protocol::ErrorResponse)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|