cql-rb 1.0.6 → 1.1.0.pre0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +4 -9
- data/lib/cql.rb +1 -0
- data/lib/cql/byte_buffer.rb +23 -7
- data/lib/cql/client.rb +11 -6
- data/lib/cql/client/asynchronous_client.rb +37 -83
- data/lib/cql/client/asynchronous_prepared_statement.rb +10 -4
- data/lib/cql/client/column_metadata.rb +16 -0
- data/lib/cql/client/request_runner.rb +46 -0
- data/lib/cql/future.rb +4 -5
- data/lib/cql/io.rb +2 -5
- data/lib/cql/io/connection.rb +220 -0
- data/lib/cql/io/io_reactor.rb +213 -185
- data/lib/cql/protocol.rb +1 -0
- data/lib/cql/protocol/cql_protocol_handler.rb +201 -0
- data/lib/cql/protocol/decoding.rb +6 -31
- data/lib/cql/protocol/encoding.rb +1 -5
- data/lib/cql/protocol/request.rb +4 -0
- data/lib/cql/protocol/responses/schema_change_result_response.rb +15 -0
- data/lib/cql/protocol/type_converter.rb +56 -76
- data/lib/cql/time_uuid.rb +104 -0
- data/lib/cql/uuid.rb +4 -2
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client/asynchronous_client_spec.rb +47 -71
- data/spec/cql/client/asynchronous_prepared_statement_spec.rb +68 -0
- data/spec/cql/client/client_shared.rb +3 -3
- data/spec/cql/client/column_metadata_spec.rb +80 -0
- data/spec/cql/client/request_runner_spec.rb +120 -0
- data/spec/cql/future_spec.rb +26 -11
- data/spec/cql/io/connection_spec.rb +460 -0
- data/spec/cql/io/io_reactor_spec.rb +212 -265
- data/spec/cql/protocol/cql_protocol_handler_spec.rb +216 -0
- data/spec/cql/protocol/decoding_spec.rb +9 -28
- data/spec/cql/protocol/encoding_spec.rb +0 -5
- data/spec/cql/protocol/request_spec.rb +16 -0
- data/spec/cql/protocol/response_frame_spec.rb +2 -2
- data/spec/cql/protocol/responses/schema_change_result_response_spec.rb +70 -0
- data/spec/cql/time_uuid_spec.rb +136 -0
- data/spec/cql/uuid_spec.rb +1 -5
- data/spec/integration/client_spec.rb +34 -38
- data/spec/integration/io_spec.rb +283 -0
- data/spec/integration/protocol_spec.rb +53 -113
- data/spec/integration/regression_spec.rb +124 -0
- data/spec/integration/uuid_spec.rb +76 -0
- data/spec/spec_helper.rb +12 -9
- data/spec/support/fake_io_reactor.rb +52 -21
- data/spec/support/fake_server.rb +2 -2
- metadata +33 -10
- checksums.yaml +0 -15
- data/lib/cql/io/node_connection.rb +0 -209
- data/spec/cql/protocol/type_converter_spec.rb +0 -52
data/README.md
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
# Ruby CQL3 driver
|
2
2
|
|
3
|
+
[![Build Status](https://travis-ci.org/iconara/cql-rb.png?branch=master)](https://travis-ci.org/iconara/cql-rb)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/iconara/cql-rb/badge.png?branch=io_reactor_rewrite)](https://coveralls.io/r/iconara/cql-rb?branch=io_reactor_rewrite)
|
5
|
+
|
3
6
|
# Requirements
|
4
7
|
|
5
|
-
Cassandra 1.2 with the native transport protocol turned on and a modern Ruby.
|
8
|
+
Cassandra 1.2 with the native transport protocol turned on and a modern Ruby. It's tested continuously in Travis with Ruby 1.9.3, 2.0.0, and JRuby 1.7.x stable and head.
|
6
9
|
|
7
10
|
# Installation
|
8
11
|
|
@@ -43,12 +46,6 @@ or using CQL:
|
|
43
46
|
client.execute('USE measurements')
|
44
47
|
```
|
45
48
|
|
46
|
-
If your keyspace has upper case characters you need to quote the keyspace name _(this is supported in v1.0.2 and later)_:
|
47
|
-
|
48
|
-
```ruby
|
49
|
-
client.execute('USE "Measurements"')
|
50
|
-
```
|
51
|
-
|
52
49
|
## Running queries
|
53
50
|
|
54
51
|
You run CQL statements by passing them to `#execute`. Most statements don't have any result and the call will return nil.
|
@@ -210,8 +207,6 @@ Open an issue and I'll do my best to help you. Please include the gem version, C
|
|
210
207
|
|
211
208
|
# Known bugs & limitations
|
212
209
|
|
213
|
-
[![Build Status](https://travis-ci.org/iconara/cql-rb.png?branch=master)](https://travis-ci.org/iconara/cql-rb)
|
214
|
-
|
215
210
|
* No automatic peer discovery.
|
216
211
|
* No automatic reconnection on connection failures.
|
217
212
|
* JRuby 1.6.8 and earlier is not supported, although it probably works fine. The only known issue is that connection failures aren't handled gracefully.
|
data/lib/cql.rb
CHANGED
data/lib/cql/byte_buffer.rb
CHANGED
@@ -19,14 +19,18 @@ module Cql
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def append(bytes)
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
if bytes.is_a?(self.class)
|
23
|
+
bytes.append_to(self)
|
24
|
+
else
|
25
|
+
bytes = bytes.to_s
|
26
|
+
unless bytes.ascii_only?
|
27
|
+
bytes = bytes.dup.force_encoding(::Encoding::BINARY)
|
28
|
+
end
|
29
|
+
retag = @write_buffer.empty?
|
30
|
+
@write_buffer << bytes
|
31
|
+
@write_buffer.force_encoding(::Encoding::BINARY) if retag
|
32
|
+
@length += bytes.bytesize
|
25
33
|
end
|
26
|
-
retag = @write_buffer.empty?
|
27
|
-
@write_buffer << bytes
|
28
|
-
@write_buffer.force_encoding(::Encoding::BINARY) if retag
|
29
|
-
@length += bytes.bytesize
|
30
34
|
self
|
31
35
|
end
|
32
36
|
alias_method :<<, :append
|
@@ -149,6 +153,18 @@ module Cql
|
|
149
153
|
%(#<#{self.class.name}: #{to_str.inspect}>)
|
150
154
|
end
|
151
155
|
|
156
|
+
protected
|
157
|
+
|
158
|
+
def append_to(other)
|
159
|
+
other.raw_append(cheap_peek)
|
160
|
+
other.raw_append(@write_buffer) unless @write_buffer.empty?
|
161
|
+
end
|
162
|
+
|
163
|
+
def raw_append(bytes)
|
164
|
+
@write_buffer << bytes
|
165
|
+
@length += bytes.bytesize
|
166
|
+
end
|
167
|
+
|
152
168
|
private
|
153
169
|
|
154
170
|
def swap_buffers
|
data/lib/cql/client.rb
CHANGED
@@ -11,6 +11,7 @@ module Cql
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
NotConnectedError = Class.new(CqlError)
|
14
15
|
ClientError = Class.new(CqlError)
|
15
16
|
AuthenticationError = Class.new(ClientError)
|
16
17
|
|
@@ -43,7 +44,6 @@ module Cql
|
|
43
44
|
# See {Cql::Client::Client} for the full client API.
|
44
45
|
#
|
45
46
|
module Client
|
46
|
-
NotConnectedError = Class.new(ClientError)
|
47
47
|
InvalidKeyspaceNameError = Class.new(ClientError)
|
48
48
|
|
49
49
|
# Create a new client and connects to Cassandra.
|
@@ -74,13 +74,13 @@ module Cql
|
|
74
74
|
# keyspace will also be changed (otherwise the current keyspace will not
|
75
75
|
# be set).
|
76
76
|
#
|
77
|
-
# @return
|
77
|
+
# @return [Cql::Client]
|
78
78
|
|
79
79
|
# @!method close
|
80
80
|
#
|
81
81
|
# Disconnect from all nodes.
|
82
82
|
#
|
83
|
-
# @return
|
83
|
+
# @return [Cql::Client]
|
84
84
|
|
85
85
|
# @!method connected?
|
86
86
|
#
|
@@ -93,8 +93,7 @@ module Cql
|
|
93
93
|
# Returns the name of the current keyspace, or `nil` if no keyspace has been
|
94
94
|
# set yet.
|
95
95
|
#
|
96
|
-
# @
|
97
|
-
# @return [nil]
|
96
|
+
# @return [String]
|
98
97
|
|
99
98
|
# @!method use(keyspace)
|
100
99
|
#
|
@@ -143,6 +142,11 @@ module Cql
|
|
143
142
|
#
|
144
143
|
# @param args [Array] the values for the bound parameters, and optionally
|
145
144
|
# the desired consistency level, as a symbol (defaults to :quorum)
|
145
|
+
# @raise [Cql::NotConnectedError] raised when the client is not connected
|
146
|
+
# @raise [Cql::QueryError] raised when there is an error on the server side
|
147
|
+
# @return [nil, Cql::Client::QueryResult] Most statements have no result and return
|
148
|
+
# `nil`, but `SELECT` statements return an `Enumerable` of rows
|
149
|
+
# (see {Cql::Client::QueryResult}).
|
146
150
|
def execute(*args)
|
147
151
|
end
|
148
152
|
end
|
@@ -155,4 +159,5 @@ require 'cql/client/query_result'
|
|
155
159
|
require 'cql/client/asynchronous_prepared_statement'
|
156
160
|
require 'cql/client/synchronous_prepared_statement'
|
157
161
|
require 'cql/client/synchronous_client'
|
158
|
-
require 'cql/client/asynchronous_client'
|
162
|
+
require 'cql/client/asynchronous_client'
|
163
|
+
require 'cql/client/request_runner'
|
@@ -5,22 +5,22 @@ module Cql
|
|
5
5
|
# @private
|
6
6
|
class AsynchronousClient < Client
|
7
7
|
def initialize(options={})
|
8
|
-
connection_timeout = options[:connection_timeout]
|
8
|
+
@connection_timeout = options[:connection_timeout] || 10
|
9
9
|
@host = options[:host] || 'localhost'
|
10
10
|
@port = options[:port] || 9042
|
11
|
-
@io_reactor = options[:io_reactor] || Io::IoReactor.new(
|
11
|
+
@io_reactor = options[:io_reactor] || Io::IoReactor.new(Protocol::CqlProtocolHandler)
|
12
12
|
@lock = Mutex.new
|
13
13
|
@connected = false
|
14
14
|
@connecting = false
|
15
15
|
@closing = false
|
16
16
|
@initial_keyspace = options[:keyspace]
|
17
17
|
@credentials = options[:credentials]
|
18
|
-
@
|
18
|
+
@request_runner = RequestRunner.new
|
19
19
|
end
|
20
20
|
|
21
21
|
def connect
|
22
22
|
@lock.synchronize do
|
23
|
-
return @connected_future if
|
23
|
+
return @connected_future if can_execute?
|
24
24
|
@connecting = true
|
25
25
|
@connected_future = Future.new
|
26
26
|
end
|
@@ -74,23 +74,22 @@ module Cql
|
|
74
74
|
|
75
75
|
def keyspace
|
76
76
|
@lock.synchronize do
|
77
|
-
|
77
|
+
@connections.first.keyspace
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
def use(keyspace
|
82
|
-
return Future.failed(NotConnectedError.new) unless
|
81
|
+
def use(keyspace)
|
82
|
+
return Future.failed(NotConnectedError.new) unless can_execute?
|
83
83
|
return Future.failed(InvalidKeyspaceNameError.new(%("#{keyspace}" is not a valid keyspace name))) unless valid_keyspace_name?(keyspace)
|
84
|
-
|
85
|
-
|
86
|
-
connection_ids = connection_ids.select { |id| @connection_keyspaces[id] != keyspace }
|
84
|
+
connections = @lock.synchronize do
|
85
|
+
@connections.select { |c| c.keyspace != keyspace }
|
87
86
|
end
|
88
|
-
if
|
89
|
-
futures =
|
90
|
-
execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one),
|
87
|
+
if connections.any?
|
88
|
+
futures = connections.map do |connection|
|
89
|
+
execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection)
|
91
90
|
end
|
92
91
|
futures.compact!
|
93
|
-
|
92
|
+
Future.combine(*futures).map { nil }
|
94
93
|
else
|
95
94
|
Future.completed(nil)
|
96
95
|
end
|
@@ -98,27 +97,14 @@ module Cql
|
|
98
97
|
|
99
98
|
def execute(cql, consistency=nil)
|
100
99
|
consistency ||= DEFAULT_CONSISTENCY_LEVEL
|
101
|
-
return Future.failed(NotConnectedError.new) unless
|
102
|
-
|
103
|
-
f.on_complete do
|
104
|
-
ensure_keyspace!
|
105
|
-
end
|
106
|
-
f
|
107
|
-
rescue => e
|
108
|
-
Future.failed(e)
|
109
|
-
end
|
110
|
-
|
111
|
-
# @private
|
112
|
-
def execute_statement(connection_id, statement_id, metadata, values, consistency)
|
113
|
-
return Future.failed(NotConnectedError.new) unless @connected || @connecting
|
114
|
-
request = Protocol::ExecuteRequest.new(statement_id, metadata, values, consistency || DEFAULT_CONSISTENCY_LEVEL)
|
115
|
-
execute_request(request, connection_id)
|
100
|
+
return Future.failed(NotConnectedError.new) unless can_execute?
|
101
|
+
execute_request(Protocol::QueryRequest.new(cql, consistency))
|
116
102
|
rescue => e
|
117
103
|
Future.failed(e)
|
118
104
|
end
|
119
105
|
|
120
106
|
def prepare(cql)
|
121
|
-
return Future.failed(NotConnectedError.new) unless
|
107
|
+
return Future.failed(NotConnectedError.new) unless can_execute?
|
122
108
|
execute_request(Protocol::PrepareRequest.new(cql))
|
123
109
|
rescue => e
|
124
110
|
Future.failed(e)
|
@@ -126,9 +112,13 @@ module Cql
|
|
126
112
|
|
127
113
|
private
|
128
114
|
|
129
|
-
KEYSPACE_NAME_PATTERN = /^\w[\w\d_]
|
115
|
+
KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$/
|
130
116
|
DEFAULT_CONSISTENCY_LEVEL = :quorum
|
131
117
|
|
118
|
+
def can_execute?
|
119
|
+
@connected || @connecting
|
120
|
+
end
|
121
|
+
|
132
122
|
def valid_keyspace_name?(name)
|
133
123
|
name =~ KEYSPACE_NAME_PATTERN
|
134
124
|
end
|
@@ -157,8 +147,8 @@ module Cql
|
|
157
147
|
connection_futures = hosts.map { |host| connect_to_host(host) }
|
158
148
|
Future.combine(*connection_futures)
|
159
149
|
end
|
160
|
-
hosts_connected_future.on_complete do |
|
161
|
-
@
|
150
|
+
hosts_connected_future.on_complete do |connections|
|
151
|
+
@connections = connections
|
162
152
|
end
|
163
153
|
if @initial_keyspace
|
164
154
|
initialized_future = hosts_connected_future.flat_map do |*args|
|
@@ -181,72 +171,36 @@ module Cql
|
|
181
171
|
end
|
182
172
|
|
183
173
|
def connect_to_host(host)
|
184
|
-
connected = @io_reactor.
|
185
|
-
connected.flat_map do |
|
186
|
-
started = execute_request(Protocol::StartupRequest.new,
|
187
|
-
started.flat_map { |response| maybe_authenticate(response,
|
174
|
+
connected = @io_reactor.connect(host, @port, @connection_timeout)
|
175
|
+
connected.flat_map do |connection|
|
176
|
+
started = execute_request(Protocol::StartupRequest.new, connection)
|
177
|
+
started.flat_map { |response| maybe_authenticate(response, connection) }
|
188
178
|
end
|
189
179
|
end
|
190
180
|
|
191
|
-
def maybe_authenticate(response,
|
181
|
+
def maybe_authenticate(response, connection)
|
192
182
|
case response
|
193
183
|
when AuthenticationRequired
|
194
184
|
if @credentials
|
195
185
|
credentials_request = Protocol::CredentialsRequest.new(@credentials)
|
196
|
-
execute_request(credentials_request,
|
186
|
+
execute_request(credentials_request, connection).map { connection }
|
197
187
|
else
|
198
188
|
Future.failed(AuthenticationError.new('Server requested authentication, but no credentials given'))
|
199
189
|
end
|
200
190
|
else
|
201
|
-
Future.completed(
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def execute_request(request, connection_id=nil)
|
206
|
-
@io_reactor.queue_request(request, connection_id).map do |response, connection_id|
|
207
|
-
interpret_response!(request, response, connection_id)
|
191
|
+
Future.completed(connection)
|
208
192
|
end
|
209
193
|
end
|
210
194
|
|
211
|
-
def
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
195
|
+
def execute_request(request, connection=nil)
|
196
|
+
f = @request_runner.execute(connection || @connections.sample, request)
|
197
|
+
f.map do |result|
|
198
|
+
if result.is_a?(KeyspaceChanged)
|
199
|
+
use(result.keyspace)
|
200
|
+
nil
|
217
201
|
else
|
218
|
-
|
202
|
+
result
|
219
203
|
end
|
220
|
-
when Protocol::RowsResultResponse
|
221
|
-
QueryResult.new(response.metadata, response.rows)
|
222
|
-
when Protocol::PreparedResultResponse
|
223
|
-
AsynchronousPreparedStatement.new(self, connection_id, response.id, response.metadata)
|
224
|
-
when Protocol::SetKeyspaceResultResponse
|
225
|
-
@lock.synchronize do
|
226
|
-
@last_keyspace_change = @connection_keyspaces[connection_id] = response.keyspace
|
227
|
-
end
|
228
|
-
nil
|
229
|
-
when Protocol::AuthenticateResponse
|
230
|
-
AuthenticationRequired.new(response.authentication_class)
|
231
|
-
else
|
232
|
-
nil
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def ensure_keyspace!
|
237
|
-
ks = nil
|
238
|
-
@lock.synchronize do
|
239
|
-
ks = @last_keyspace_change
|
240
|
-
return unless @last_keyspace_change
|
241
|
-
end
|
242
|
-
use(ks, @connection_ids) if ks
|
243
|
-
end
|
244
|
-
|
245
|
-
class AuthenticationRequired
|
246
|
-
attr_reader :authentication_class
|
247
|
-
|
248
|
-
def initialize(authentication_class)
|
249
|
-
@authentication_class = authentication_class
|
250
204
|
end
|
251
205
|
end
|
252
206
|
end
|
@@ -4,15 +4,21 @@ module Cql
|
|
4
4
|
module Client
|
5
5
|
# @private
|
6
6
|
class AsynchronousPreparedStatement < PreparedStatement
|
7
|
-
def initialize(
|
8
|
-
@
|
7
|
+
def initialize(connection, statement_id, raw_metadata)
|
8
|
+
@connection = connection
|
9
|
+
@statement_id = statement_id
|
10
|
+
@raw_metadata = raw_metadata
|
9
11
|
@metadata = ResultMetadata.new(@raw_metadata)
|
12
|
+
@request_runner = RequestRunner.new
|
10
13
|
end
|
11
14
|
|
12
15
|
def execute(*args)
|
13
16
|
bound_args = args.shift(@raw_metadata.size)
|
14
|
-
consistency_level = args.shift
|
15
|
-
|
17
|
+
consistency_level = args.shift || :quorum
|
18
|
+
request = Cql::Protocol::ExecuteRequest.new(@statement_id, @raw_metadata, bound_args, consistency_level)
|
19
|
+
@request_runner.execute(@connection, request)
|
20
|
+
rescue => e
|
21
|
+
Future.failed(e)
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
@@ -17,6 +17,22 @@ module Cql
|
|
17
17
|
def to_ary
|
18
18
|
[@keyspace, @table, @column_name, @type]
|
19
19
|
end
|
20
|
+
|
21
|
+
def eql?(other)
|
22
|
+
self.keyspace == other.keyspace && self.table == other.table && self.column_name == other.column_name && self.type == other.type
|
23
|
+
end
|
24
|
+
alias_method :==, :eql?
|
25
|
+
|
26
|
+
def hash
|
27
|
+
@h ||= begin
|
28
|
+
h = 0
|
29
|
+
h = ((h & 33554431) * 31) ^ @keyspace.hash
|
30
|
+
h = ((h & 33554431) * 31) ^ @table.hash
|
31
|
+
h = ((h & 33554431) * 31) ^ @column_name.hash
|
32
|
+
h = ((h & 33554431) * 31) ^ @type.hash
|
33
|
+
h
|
34
|
+
end
|
35
|
+
end
|
20
36
|
end
|
21
37
|
end
|
22
38
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
module Client
|
5
|
+
# @private
|
6
|
+
class RequestRunner
|
7
|
+
def execute(connection, request)
|
8
|
+
connection.send_request(request).map do |response|
|
9
|
+
case response
|
10
|
+
when Protocol::RowsResultResponse
|
11
|
+
QueryResult.new(response.metadata, response.rows)
|
12
|
+
when Protocol::PreparedResultResponse
|
13
|
+
AsynchronousPreparedStatement.new(connection, response.id, response.metadata)
|
14
|
+
when Protocol::ErrorResponse
|
15
|
+
cql = request.is_a?(Protocol::QueryRequest) ? request.cql : nil
|
16
|
+
raise QueryError.new(response.code, response.message, cql)
|
17
|
+
when Protocol::SetKeyspaceResultResponse
|
18
|
+
KeyspaceChanged.new(response.keyspace)
|
19
|
+
when Protocol::AuthenticateResponse
|
20
|
+
AuthenticationRequired.new(response.authentication_class)
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @private
|
29
|
+
class AuthenticationRequired
|
30
|
+
attr_reader :authentication_class
|
31
|
+
|
32
|
+
def initialize(authentication_class)
|
33
|
+
@authentication_class = authentication_class
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
class KeyspaceChanged
|
39
|
+
attr_reader :keyspace
|
40
|
+
|
41
|
+
def initialize(keyspace)
|
42
|
+
@keyspace = keyspace
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|