cql-rb 1.0.0.pre7 → 1.0.0.pre8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/lib/cql/byte_buffer.rb +22 -0
  2. data/lib/cql/client.rb +79 -310
  3. data/lib/cql/client/asynchronous_client.rb +254 -0
  4. data/lib/cql/client/asynchronous_prepared_statement.rb +19 -0
  5. data/lib/cql/client/column_metadata.rb +22 -0
  6. data/lib/cql/client/query_result.rb +34 -0
  7. data/lib/cql/client/result_metadata.rb +31 -0
  8. data/lib/cql/client/synchronous_client.rb +47 -0
  9. data/lib/cql/client/synchronous_prepared_statement.rb +47 -0
  10. data/lib/cql/future.rb +7 -3
  11. data/lib/cql/io.rb +1 -0
  12. data/lib/cql/io/io_reactor.rb +9 -3
  13. data/lib/cql/io/node_connection.rb +2 -2
  14. data/lib/cql/protocol.rb +5 -4
  15. data/lib/cql/protocol/request.rb +23 -0
  16. data/lib/cql/protocol/requests/credentials_request.rb +1 -1
  17. data/lib/cql/protocol/requests/execute_request.rb +13 -84
  18. data/lib/cql/protocol/requests/options_request.rb +1 -1
  19. data/lib/cql/protocol/requests/prepare_request.rb +2 -1
  20. data/lib/cql/protocol/requests/query_request.rb +3 -1
  21. data/lib/cql/protocol/requests/register_request.rb +1 -1
  22. data/lib/cql/protocol/requests/startup_request.rb +1 -2
  23. data/lib/cql/protocol/{response_body.rb → response.rb} +1 -1
  24. data/lib/cql/protocol/responses/authenticate_response.rb +1 -1
  25. data/lib/cql/protocol/responses/error_response.rb +1 -1
  26. data/lib/cql/protocol/responses/ready_response.rb +1 -1
  27. data/lib/cql/protocol/responses/result_response.rb +1 -1
  28. data/lib/cql/protocol/responses/rows_result_response.rb +1 -1
  29. data/lib/cql/protocol/responses/supported_response.rb +1 -1
  30. data/lib/cql/protocol/type_converter.rb +226 -46
  31. data/lib/cql/version.rb +1 -1
  32. data/spec/cql/byte_buffer_spec.rb +38 -0
  33. data/spec/cql/client/asynchronous_client_spec.rb +472 -0
  34. data/spec/cql/client/client_shared.rb +27 -0
  35. data/spec/cql/client/synchronous_client_spec.rb +104 -0
  36. data/spec/cql/client/synchronous_prepared_statement_spec.rb +65 -0
  37. data/spec/cql/future_spec.rb +4 -0
  38. data/spec/cql/io/io_reactor_spec.rb +39 -20
  39. data/spec/cql/protocol/request_spec.rb +17 -0
  40. data/spec/cql/protocol/requests/credentials_request_spec.rb +82 -0
  41. data/spec/cql/protocol/requests/execute_request_spec.rb +174 -0
  42. data/spec/cql/protocol/requests/options_request_spec.rb +24 -0
  43. data/spec/cql/protocol/requests/prepare_request_spec.rb +70 -0
  44. data/spec/cql/protocol/requests/query_request_spec.rb +95 -0
  45. data/spec/cql/protocol/requests/register_request_spec.rb +24 -0
  46. data/spec/cql/protocol/requests/startup_request_spec.rb +29 -0
  47. data/spec/integration/client_spec.rb +26 -19
  48. data/spec/integration/protocol_spec.rb +2 -2
  49. data/spec/integration/regression_spec.rb +1 -1
  50. metadata +35 -9
  51. data/lib/cql/protocol/request_body.rb +0 -15
  52. data/lib/cql/protocol/request_frame.rb +0 -20
  53. data/spec/cql/client_spec.rb +0 -454
  54. data/spec/cql/protocol/request_frame_spec.rb +0 -456
@@ -105,6 +105,28 @@ module Cql
105
105
  b
106
106
  end
107
107
 
108
+ def update(location, bytes)
109
+ absolute_offset = @offset + location
110
+ bytes_length = bytes.bytesize
111
+ if absolute_offset >= @read_buffer.bytesize
112
+ @write_buffer[absolute_offset - @read_buffer.bytesize, bytes_length] = bytes
113
+ else
114
+ overflow = absolute_offset + bytes_length - @read_buffer.bytesize
115
+ read_buffer_portion = bytes_length - overflow
116
+ @read_buffer[absolute_offset, read_buffer_portion] = bytes[0, read_buffer_portion]
117
+ if overflow > 0
118
+ @write_buffer[0, overflow] = bytes[read_buffer_portion, bytes_length - 1]
119
+ end
120
+ end
121
+ end
122
+
123
+ def cheap_peek
124
+ if @offset >= @read_buffer.bytesize
125
+ swap_buffers
126
+ end
127
+ @read_buffer[@offset, @read_buffer.bytesize - @offset]
128
+ end
129
+
108
130
  def eql?(other)
109
131
  self.to_str.eql?(other.to_str)
110
132
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Cql
4
4
  class QueryError < CqlError
5
- attr_reader :code
5
+ attr_reader :code, :cql
6
6
 
7
- def initialize(code, message)
7
+ def initialize(code, message, cql=nil)
8
8
  super(message)
9
9
  @code = code
10
+ @cql = cql
10
11
  end
11
12
  end
12
13
 
@@ -39,15 +40,13 @@ module Cql
39
40
  #
40
41
  # Client instances are threadsafe.
41
42
  #
42
- class Client
43
+ # See {Cql::Client::Client} for the full client API.
44
+ #
45
+ module Client
43
46
  NotConnectedError = Class.new(ClientError)
44
47
  InvalidKeyspaceNameError = Class.new(ClientError)
45
48
 
46
- # Create a new client.
47
- #
48
- # Creating a client does not automatically connect to Cassandra, you need to
49
- # call {#connect} to connect, or use {Client.connect}. `#connect` returns
50
- # `self` so you can chain that call after `new`.
49
+ # Create a new client and connects to Cassandra.
51
50
  #
52
51
  # @param [Hash] options
53
52
  # @option options [String] :host ('localhost') One or more (comma separated)
@@ -57,237 +56,83 @@ module Cql
57
56
  # connection, in seconds
58
57
  # @option options [String] :keyspace The keyspace to change to immediately
59
58
  # after all connections have been established, this is optional.
60
- def initialize(options={})
61
- connection_timeout = options[:connection_timeout]
62
- @host = options[:host] || 'localhost'
63
- @port = options[:port] || 9042
64
- @io_reactor = options[:io_reactor] || Io::IoReactor.new(connection_timeout: connection_timeout)
65
- @lock = Mutex.new
66
- @started = false
67
- @shut_down = false
68
- @initial_keyspace = options[:keyspace]
69
- @credentials = options[:credentials]
70
- @connection_keyspaces = {}
71
- end
72
-
73
- def self.connect(options={})
74
- new(options).connect
75
- end
76
-
77
- # Connect to all nodes.
78
- #
79
- # You must call this method before you call any of the other methods of a
80
- # client. Calling it again will have no effect.
81
- #
82
- # If `:keyspace` was specified when the client was created the current
83
- # keyspace will also be changed (otherwise the current keyspace will not
84
- # be set).
85
- #
86
- # @return self
87
- #
88
- def connect
89
- @lock.synchronize do
90
- return if @started
91
- @started = true
92
- @io_reactor.start
93
- hosts = @host.split(',')
94
- connection_futures = hosts.map { |host| connect_to_host(host) }
95
- @connection_ids = Future.combine(*connection_futures).get
96
- end
97
- use(@initial_keyspace) if @initial_keyspace
98
- self
99
- rescue => e
100
- close
101
- if e.is_a?(Cql::QueryError) && e.code == 0x100
102
- raise AuthenticationError, e.message, e.backtrace
103
- else
104
- raise
105
- end
106
- end
107
-
108
- # @deprecated Use {#connect} or {.connect}
109
- def start!
110
- $stderr.puts('Client#start! is deprecated, use Client#connect, or Client.connect')
111
- connect
112
- end
113
-
114
- # Disconnect from all nodes.
115
- #
116
- # @return self
117
- #
118
- def close
119
- @lock.synchronize do
120
- return if @shut_down || !@started
121
- @shut_down = true
122
- @started = false
123
- end
124
- @io_reactor.stop.get
125
- self
126
- end
127
-
128
- # @deprecated Use {#close}
129
- def shutdown!
130
- $stderr.puts('Client#shutdown! is deprecated, use Client#close')
131
- close
132
- end
133
-
134
- # Returns whether or not the client is connected.
135
- #
136
- def connected?
137
- @started
138
- end
139
-
140
- # Returns the name of the current keyspace, or `nil` if no keyspace has been
141
- # set yet.
142
- #
143
- def keyspace
144
- @lock.synchronize do
145
- return @connection_ids.map { |id| @connection_keyspaces[id] }.first
146
- end
147
- end
148
-
149
- # Changes keyspace by sending a `USE` statement to all connections.
150
- #
151
- # The the second parameter is meant for internal use only.
152
- #
153
- # @raise [Cql::NotConnectedError] raised when the client is not connected
154
- #
155
- def use(keyspace, connection_ids=@connection_ids)
156
- raise NotConnectedError unless connected?
157
- if check_keyspace_name!(keyspace)
158
- @lock.synchronize do
159
- connection_ids = connection_ids.select { |id| @connection_keyspaces[id] != keyspace }
160
- end
161
- if connection_ids.any?
162
- futures = connection_ids.map do |connection_id|
163
- execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection_id)
164
- end
165
- futures.compact!
166
- Future.combine(*futures).get
167
- end
168
- nil
169
- end
170
- end
171
-
172
- # Execute a CQL statement
173
- #
174
- # @raise [Cql::NotConnectedError] raised when the client is not connected
175
- # @raise [Cql::QueryError] raised when the CQL has syntax errors or for
176
- # other situations when the server complains.
177
- # @return [nil, Enumerable<Hash>] Most statements have no result and return
178
- # `nil`, but `SELECT` statements return an `Enumerable` of rows
179
- # (see {QueryResult}).
180
- #
181
- def execute(cql, consistency=DEFAULT_CONSISTENCY_LEVEL)
182
- raise NotConnectedError unless connected?
183
- result = execute_request(Protocol::QueryRequest.new(cql, consistency)).value
184
- ensure_keyspace!
185
- result
186
- end
187
-
188
- # @private
189
- def execute_statement(connection_id, statement_id, metadata, values, consistency)
190
- raise NotConnectedError unless connected?
191
- execute_request(Protocol::ExecuteRequest.new(statement_id, metadata, values, consistency || DEFAULT_CONSISTENCY_LEVEL), connection_id).value
192
- end
193
-
194
- # Returns a prepared statement that can be run over and over again with
195
- # different values.
196
- #
197
- # @raise [Cql::NotConnectedError] raised when the client is not connected
198
- # @return [Cql::PreparedStatement] an object encapsulating the prepared statement
59
+ # @return [Cql::Client::Client]
199
60
  #
200
- def prepare(cql)
201
- raise NotConnectedError unless connected?
202
- execute_request(Protocol::PrepareRequest.new(cql)).value
61
+ def self.connect(options={})
62
+ SynchronousClient.new(AsynchronousClient.new(options)).connect
203
63
  end
204
64
 
205
- private
206
-
207
- KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$/
208
- DEFAULT_CONSISTENCY_LEVEL = :quorum
209
-
210
- def check_keyspace_name!(name)
211
- if name !~ KEYSPACE_NAME_PATTERN
212
- raise InvalidKeyspaceNameError, %("#{name}" is not a valid keyspace name)
213
- end
214
- true
215
- end
65
+ class Client
66
+ # @!method connect
67
+ #
68
+ # Connect to all nodes.
69
+ #
70
+ # You must call this method before you call any of the other methods of a
71
+ # client. Calling it again will have no effect.
72
+ #
73
+ # If `:keyspace` was specified when the client was created the current
74
+ # keyspace will also be changed (otherwise the current keyspace will not
75
+ # be set).
76
+ #
77
+ # @return self
216
78
 
217
- def connect_to_host(host)
218
- connected = @io_reactor.add_connection(host, @port)
219
- connected.flat_map do |connection_id|
220
- started = execute_request(Protocol::StartupRequest.new, connection_id)
221
- started.flat_map { |response| maybe_authenticate(response, connection_id) }
222
- end
223
- end
79
+ # @!method close
80
+ #
81
+ # Disconnect from all nodes.
82
+ #
83
+ # @return self
224
84
 
225
- def maybe_authenticate(response, connection_id)
226
- case response
227
- when AuthenticationRequired
228
- if @credentials
229
- credentials_request = Protocol::CredentialsRequest.new(@credentials)
230
- execute_request(credentials_request, connection_id).map { connection_id }
231
- else
232
- Future.failed(AuthenticationError.new('Server requested authentication, but no credentials given'))
233
- end
234
- else
235
- Future.completed(connection_id)
236
- end
237
- end
85
+ # @!method connected?
86
+ #
87
+ # Returns whether or not the client is connected.
88
+ #
89
+ # @return [true, false]
238
90
 
239
- def execute_request(request, connection_id=nil)
240
- @io_reactor.queue_request(request, connection_id).map do |response, connection_id|
241
- interpret_response!(response, connection_id)
242
- end
243
- end
91
+ # @!method keyspace
92
+ #
93
+ # Returns the name of the current keyspace, or `nil` if no keyspace has been
94
+ # set yet.
95
+ #
96
+ # @param [String] keyspace
97
+ # @return [nil]
244
98
 
245
- def interpret_response!(response, connection_id)
246
- case response
247
- when Protocol::ErrorResponse
248
- raise QueryError.new(response.code, response.message)
249
- when Protocol::RowsResultResponse
250
- QueryResult.new(response.metadata, response.rows)
251
- when Protocol::PreparedResultResponse
252
- PreparedStatement.new(self, connection_id, response.id, response.metadata)
253
- when Protocol::SetKeyspaceResultResponse
254
- @lock.synchronize do
255
- @last_keyspace_change = @connection_keyspaces[connection_id] = response.keyspace
256
- end
257
- nil
258
- when Protocol::AuthenticateResponse
259
- AuthenticationRequired.new(response.authentication_class)
260
- else
261
- nil
262
- end
263
- end
99
+ # @!method use(keyspace)
100
+ #
101
+ # Changes keyspace by sending a `USE` statement to all connections.
102
+ #
103
+ # The the second parameter is meant for internal use only.
104
+ #
105
+ # @param [String] keyspace
106
+ # @raise [Cql::NotConnectedError] raised when the client is not connected
107
+ # @return [nil]
264
108
 
265
- def ensure_keyspace!
266
- ks = nil
267
- @lock.synchronize do
268
- ks = @last_keyspace_change
269
- return unless @last_keyspace_change
270
- end
271
- use(ks, @connection_ids) if ks
109
+ # @!method execute(cql, consistency=:quorum)
110
+ #
111
+ # Execute a CQL statement
112
+ #
113
+ # @param [String] cql
114
+ # @param [Symbol] consistency
115
+ # @raise [Cql::NotConnectedError] raised when the client is not connected
116
+ # @raise [Cql::QueryError] raised when the CQL has syntax errors or for
117
+ # other situations when the server complains.
118
+ # @return [nil, Cql::Client::QueryResult] Most statements have no result and return
119
+ # `nil`, but `SELECT` statements return an `Enumerable` of rows
120
+ # (see {Cql::Client::QueryResult}).
121
+
122
+ # @!method prepare(cql)
123
+ #
124
+ # Returns a prepared statement that can be run over and over again with
125
+ # different values.
126
+ #
127
+ # @param [String] cql
128
+ # @raise [Cql::NotConnectedError] raised when the client is not connected
129
+ # @return [Cql::Client::PreparedStatement] an object encapsulating the prepared statement
272
130
  end
273
131
 
274
- public
275
-
276
- # The representation of a prepared statement.
277
- #
278
- # Prepared statements are parsed once and stored on the server, allowing
279
- # you to execute them over and over again but only send values for the bound
280
- # parameters.
281
- #
282
132
  class PreparedStatement
283
133
  # @return [ResultMetadata]
284
134
  attr_reader :metadata
285
135
 
286
- def initialize(*args)
287
- @client, @connection_id, @statement_id, @raw_metadata = args
288
- @metadata = ResultMetadata.new(@raw_metadata)
289
- end
290
-
291
136
  # Execute the prepared statement with a list of values for the bound parameters.
292
137
  #
293
138
  # The number of arguments must equal the number of bound parameters.
@@ -298,92 +143,16 @@ module Cql
298
143
  #
299
144
  # @param args [Array] the values for the bound parameters, and optionally
300
145
  # the desired consistency level, as a symbol (defaults to :quorum)
301
- #
302
146
  def execute(*args)
303
- bound_args = args.shift(@raw_metadata.size)
304
- consistency_level = args.shift
305
- @client.execute_statement(@connection_id, @statement_id, @raw_metadata, bound_args, consistency_level)
306
- end
307
- end
308
-
309
- class AuthenticationRequired
310
- attr_reader :authentication_class
311
-
312
- def initialize(authentication_class)
313
- @authentication_class = authentication_class
314
- end
315
- end
316
-
317
- class QueryResult
318
- include Enumerable
319
-
320
- # @return [ResultMetadata]
321
- attr_reader :metadata
322
-
323
- # @private
324
- def initialize(metadata, rows)
325
- @metadata = ResultMetadata.new(metadata)
326
- @rows = rows
327
- end
328
-
329
- # Returns whether or not there are any rows in this result set
330
- #
331
- def empty?
332
- @rows.empty?
333
- end
334
-
335
- # Iterates over each row in the result set.
336
- #
337
- # @yieldparam [Hash] row each row in the result set as a hash
338
- # @return [Enumerable<Hash>]
339
- #
340
- def each(&block)
341
- @rows.each(&block)
342
- end
343
- alias_method :each_row, :each
344
- end
345
-
346
- class ResultMetadata
347
- include Enumerable
348
-
349
- # @private
350
- def initialize(metadata)
351
- @metadata = metadata.each_with_object({}) { |m, h| h[m[2]] = ColumnMetadata.new(*m) }
352
- end
353
-
354
- # Returns the column metadata
355
- #
356
- # @return [ColumnMetadata] column_metadata the metadata for the column
357
- #
358
- def [](column_name)
359
- @metadata[column_name]
360
- end
361
-
362
- # Iterates over the metadata for each column
363
- #
364
- # @yieldparam [ColumnMetadata] metadata the metadata for each column
365
- # @return [Enumerable<ColumnMetadata>]
366
- #
367
- def each(&block)
368
- @metadata.each_value(&block)
369
- end
370
- end
371
-
372
- # Represents metadata about a column in a query result set or prepared
373
- # statement. Apart from the keyspace, table and column names there's also
374
- # the type as a symbol (e.g. `:varchar`, `:int`, `:date`).
375
- class ColumnMetadata
376
- attr_reader :keyspace, :table, :column_name, :type
377
-
378
- # @private
379
- def initialize(*args)
380
- @keyspace, @table, @column_name, @type = args
381
- end
382
-
383
- # @private
384
- def to_ary
385
- [@keyspace, @table, @column_name, @type]
386
147
  end
387
148
  end
388
149
  end
389
- end
150
+ end
151
+
152
+ require 'cql/client/column_metadata'
153
+ require 'cql/client/result_metadata'
154
+ require 'cql/client/query_result'
155
+ require 'cql/client/asynchronous_prepared_statement'
156
+ require 'cql/client/synchronous_prepared_statement'
157
+ require 'cql/client/synchronous_client'
158
+ require 'cql/client/asynchronous_client'