cql-rb 1.0.0.pre0
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.
- 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
|