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.
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