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.
Files changed (50) hide show
  1. data/README.md +4 -9
  2. data/lib/cql.rb +1 -0
  3. data/lib/cql/byte_buffer.rb +23 -7
  4. data/lib/cql/client.rb +11 -6
  5. data/lib/cql/client/asynchronous_client.rb +37 -83
  6. data/lib/cql/client/asynchronous_prepared_statement.rb +10 -4
  7. data/lib/cql/client/column_metadata.rb +16 -0
  8. data/lib/cql/client/request_runner.rb +46 -0
  9. data/lib/cql/future.rb +4 -5
  10. data/lib/cql/io.rb +2 -5
  11. data/lib/cql/io/connection.rb +220 -0
  12. data/lib/cql/io/io_reactor.rb +213 -185
  13. data/lib/cql/protocol.rb +1 -0
  14. data/lib/cql/protocol/cql_protocol_handler.rb +201 -0
  15. data/lib/cql/protocol/decoding.rb +6 -31
  16. data/lib/cql/protocol/encoding.rb +1 -5
  17. data/lib/cql/protocol/request.rb +4 -0
  18. data/lib/cql/protocol/responses/schema_change_result_response.rb +15 -0
  19. data/lib/cql/protocol/type_converter.rb +56 -76
  20. data/lib/cql/time_uuid.rb +104 -0
  21. data/lib/cql/uuid.rb +4 -2
  22. data/lib/cql/version.rb +1 -1
  23. data/spec/cql/client/asynchronous_client_spec.rb +47 -71
  24. data/spec/cql/client/asynchronous_prepared_statement_spec.rb +68 -0
  25. data/spec/cql/client/client_shared.rb +3 -3
  26. data/spec/cql/client/column_metadata_spec.rb +80 -0
  27. data/spec/cql/client/request_runner_spec.rb +120 -0
  28. data/spec/cql/future_spec.rb +26 -11
  29. data/spec/cql/io/connection_spec.rb +460 -0
  30. data/spec/cql/io/io_reactor_spec.rb +212 -265
  31. data/spec/cql/protocol/cql_protocol_handler_spec.rb +216 -0
  32. data/spec/cql/protocol/decoding_spec.rb +9 -28
  33. data/spec/cql/protocol/encoding_spec.rb +0 -5
  34. data/spec/cql/protocol/request_spec.rb +16 -0
  35. data/spec/cql/protocol/response_frame_spec.rb +2 -2
  36. data/spec/cql/protocol/responses/schema_change_result_response_spec.rb +70 -0
  37. data/spec/cql/time_uuid_spec.rb +136 -0
  38. data/spec/cql/uuid_spec.rb +1 -5
  39. data/spec/integration/client_spec.rb +34 -38
  40. data/spec/integration/io_spec.rb +283 -0
  41. data/spec/integration/protocol_spec.rb +53 -113
  42. data/spec/integration/regression_spec.rb +124 -0
  43. data/spec/integration/uuid_spec.rb +76 -0
  44. data/spec/spec_helper.rb +12 -9
  45. data/spec/support/fake_io_reactor.rb +52 -21
  46. data/spec/support/fake_server.rb +2 -2
  47. metadata +33 -10
  48. checksums.yaml +0 -15
  49. data/lib/cql/io/node_connection.rb +0 -209
  50. 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. Tested with Ruby 1.9.3 and JRuby and 1.7.x.
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
@@ -5,6 +5,7 @@ module Cql
5
5
  end
6
6
 
7
7
  require 'cql/uuid'
8
+ require 'cql/time_uuid'
8
9
  require 'cql/byte_buffer'
9
10
  require 'cql/future'
10
11
  require 'cql/io'
@@ -19,14 +19,18 @@ module Cql
19
19
  end
20
20
 
21
21
  def append(bytes)
22
- bytes = bytes.to_s
23
- unless bytes.ascii_only?
24
- bytes = bytes.dup.force_encoding(::Encoding::BINARY)
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 self
77
+ # @return [Cql::Client]
78
78
 
79
79
  # @!method close
80
80
  #
81
81
  # Disconnect from all nodes.
82
82
  #
83
- # @return self
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
- # @param [String] keyspace
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(connection_timeout: connection_timeout)
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
- @connection_keyspaces = {}
18
+ @request_runner = RequestRunner.new
19
19
  end
20
20
 
21
21
  def connect
22
22
  @lock.synchronize do
23
- return @connected_future if @connected || @connecting
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
- return @connection_ids.map { |id| @connection_keyspaces[id] }.first
77
+ @connections.first.keyspace
78
78
  end
79
79
  end
80
80
 
81
- def use(keyspace, connection_ids=nil)
82
- return Future.failed(NotConnectedError.new) unless @connected || @connecting
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
- connection_ids ||= @connection_ids
85
- @lock.synchronize do
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 connection_ids.any?
89
- futures = connection_ids.map do |connection_id|
90
- execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection_id)
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
- return Future.combine(*futures).map { nil }
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 @connected || @connecting
102
- f = execute_request(Protocol::QueryRequest.new(cql, consistency))
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 @connected || @connecting
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_]*$|^"\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 |connection_ids|
161
- @connection_ids = connection_ids
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.add_connection(host, @port)
185
- connected.flat_map do |connection_id|
186
- started = execute_request(Protocol::StartupRequest.new, connection_id)
187
- started.flat_map { |response| maybe_authenticate(response, connection_id) }
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, connection_id)
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, connection_id).map { connection_id }
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(connection_id)
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 interpret_response!(request, response, connection_id)
212
- case response
213
- when Protocol::ErrorResponse
214
- case request
215
- when Protocol::QueryRequest
216
- raise QueryError.new(response.code, response.message, request.cql)
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
- raise QueryError.new(response.code, response.message)
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(*args)
8
- @client, @connection_id, @statement_id, @raw_metadata = args
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
- @client.execute_statement(@connection_id, @statement_id, @raw_metadata, bound_args, consistency_level)
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