cql-rb 1.0.6 → 1.1.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 +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
|
+
[](https://travis-ci.org/iconara/cql-rb)
|
4
|
+
[](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
|
-
[](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
|