cql-rb 1.0.0.pre7 → 1.0.0.pre8

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 (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'