cql-rb 1.0.0.pre0 → 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,10 +1,146 @@
1
- # Ruby CQL 3 driver
1
+ # Ruby CQL3 driver
2
2
 
3
- This is a work in progress, no usable version exists yet.
3
+ _There has not yet been a stable release of this project._
4
+
5
+
6
+ # Requirements
7
+
8
+ 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.
9
+
10
+ # Installation
11
+
12
+ gem install --prerelease cql-rb
13
+
14
+ ## Configure Cassandra
15
+
16
+ The native transport protocol (sometimes called binary protocol, or CQL protocol) is not on by default in Cassandra 1.2, to enable it edit the `cassandra.yaml` file on all nodes in your cluster and set `start_native_transport` to `true`. You need to restart the nodes for this to have effect.
17
+
18
+ # Quick start
19
+
20
+ require 'cql'
21
+
22
+ client = Cql::Client.new(host: 'cassandra.example.com')
23
+ client.start!
24
+ client.use('system')
25
+ rows = client.execute('SELECT keyspace_name, columnfamily_name FROM schema_columnfamilies')
26
+ rows.each do |row|
27
+ puts "The keyspace #{row['keyspace_name']} has a table called #{row['columnfamily_name']}""
28
+ end
29
+
30
+ when you're done you can call `#shutdown!` to disconnect from Cassandra. You can connect to multiple Cassandra nodes by passing multiple comma separated host names to the `:host` option.
31
+
32
+ ## Changing keyspaces
33
+
34
+ client.use('measurements')
35
+
36
+ or using CQL:
37
+
38
+ client.execute('USE measurements')
39
+
40
+ ## Running queries
41
+
42
+ You run CQL statements by passing them to `#execute`. Most statements don't have any result and the call will return nil.
43
+
44
+ client.execute("INSERT INTO events (id, date, description) VALUES (23462, '2013-02-24T10:14:23+0000', 'Rang bell, ate food')")
45
+
46
+ client.execute("UPDATE events SET description = 'Oh, my' WHERE id = 13126")
47
+
48
+
49
+ If the CQL statement passed to `#execute` returns a result (e.g. it's a `SELECT` statement) the call returns an enumerable of rows:
50
+
51
+ rows = client.execute('SELECT date, description FROM events')
52
+ rows.each do |row|
53
+ row.each do |key, value|
54
+ puts "#{key} = #{value}"
55
+ end
56
+ end
57
+
58
+ The enumerable also has an accessor called `metadata` which returns a description of the rows and columns:
59
+
60
+ rows = client.execute('SELECT date, description FROM events'
61
+ rows.metadata['date'].type # => :date
62
+
63
+ ## Creating keyspaces and tables
64
+
65
+ There is no special facility for creating keyspaces and tables, they are created by executing CQL:
66
+
67
+ keyspace_definition = <<-KSDEF
68
+ CREATE KEYSPACE measurements
69
+ WITH replication = {
70
+ 'class': 'SimpleStrategy',
71
+ 'replication_factor': 3
72
+ }
73
+ KSDEF
74
+
75
+ table_definition = <<- TABLEDEF
76
+ CREATE TABLE events (
77
+ id INT,
78
+ date DATE,
79
+ comment VARCHAR,
80
+ PRIMARY KEY (id)
81
+ )
82
+ TABLEDEF
83
+
84
+ client.execute(keyspace_definition)
85
+ client.use(measurements)
86
+ client.execute(table_definition)
87
+
88
+ You can also `ALTER` keyspaces and tables.
89
+
90
+ ## Prepared statements
91
+
92
+ The driver supports prepared statements. Use `#prepare` to create a statement object, and then call `#execute` on that object to run a statement. You must supply values for all bound parameters when you call `#execute`.
93
+
94
+ statement = client.prepare('SELECT date, description FROM events WHERE id = ?')
95
+ rows = statement.execute(1235)
96
+
97
+ A prepared statement can be run many times, but the CQL parsing will only be done once. Use prepared statements for queries you run over and over again.
98
+
99
+ `INSERT`, `UPDATE`, `DELETE` and `SELECT` statements can be prepared, other statements may raise `QueryError`.
100
+
101
+ At this time prepared statements are local to a single connection. Even if you connect to multiple nodes a prepared statement is only ever going to be executed against one of the nodes.
102
+
103
+ # Consistency levels
104
+
105
+ The `#execute` method supports setting the desired consistency level for the statement:
106
+
107
+ client.execute('SELECT * FROM peers', :local_quorum)
108
+
109
+ The possible values are:
110
+
111
+ * `:any`
112
+ * `:one`
113
+ * `:two`
114
+ * `:three`
115
+ * `:quorum`
116
+ * `:all`
117
+ * `:local_quorum`
118
+ * `:each_quorum`
119
+
120
+ The default consistency level is `:quorum`.
121
+
122
+ Consistency level is ignored for `USE`, `TRUNCATE`, `CREATE` and `ALTER` statements, and some (like `:any`) aren't allowed in all situations.
123
+
124
+ ## CQL3
125
+
126
+ This is just a driver for the Cassandra native CQL protocol, it doesn't really know anything about CQL. You can run any CQL3 statement and the driver will return whatever Cassandra replies with.
127
+
128
+ Read more about CQL3 in the [CQL3 syntax documentation](https://github.com/apache/cassandra/blob/cassandra-1.2/doc/cql3/CQL.textile) and the [Cassandra query documentation](http://www.datastax.com/docs/1.2/cql_cli/querying_cql).
129
+
130
+ # Known bugs & limitations
131
+
132
+ * If any connection raises an error the whole IO reactor shuts down.
133
+ * JRuby 1.6.8 is not supported, although it should be. The only known issue is that connection failures aren't handled gracefully.
134
+ * No automatic peer discovery.
135
+ * You can't specify consistency level when executing prepared statements.
136
+ * Authentication is not supported.
137
+ * Compression is not supported.
138
+ * Large results are buffered in memory until the whole response has been loaded, the protocol makes it possible to start to deliver rows to the client code as soon as the metadata is loaded, but this is not supported yet.
139
+ * There is no cluster introspection utilities (like the `DESCRIBE` commands in `cqlsh`).
4
140
 
5
141
  ## Copyright
6
142
 
7
- Copyright 2013 Theo Hultberg
143
+ Copyright 2013 Theo Hultberg/Iconara
8
144
 
9
145
  _Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License You may obtain a copy of the License at_
10
146
 
data/lib/cql/client.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
- NotConnectedError = Class.new(CqlError)
5
- InvalidKeyspaceNameError = Class.new(CqlError)
6
-
7
4
  class QueryError < CqlError
8
5
  attr_reader :code
9
6
 
@@ -13,7 +10,54 @@ module Cql
13
10
  end
14
11
  end
15
12
 
13
+ ClientError = Class.new(CqlError)
14
+
15
+ # A CQL client manages connections to one or more Cassandra nodes and you use
16
+ # it run queries, insert and update data.
17
+ #
18
+ # @example Connecting and changing to a keyspace
19
+ # # create a client that will connect to two Cassandra nodes
20
+ # client = Cql::Client.new(host: 'node01.cassandra.local,node02.cassandra.local')
21
+ # # establish node connections
22
+ # client.start!
23
+ # # change to a keyspace
24
+ # client.use('stuff')
25
+ #
26
+ # @example Query for data
27
+ # rows = client.execute('SELECT * FROM things WHERE id = 2')
28
+ # rows.each do |row|
29
+ # p row
30
+ # end
31
+ #
32
+ # @example Inserting and updating data
33
+ # client.execute("INSERT INTO things (id, value) VALUES (4, 'foo')")
34
+ # client.execute("UPDATE things SET value = 'bar' WHERE id = 5")
35
+ #
36
+ # @example Prepared statements
37
+ # statement = client.prepare('INSERT INTO things (id, value) VALUES (?, ?)')
38
+ # statement.execute(9, 'qux')
39
+ # statement.execute(8, 'baz')
40
+ #
41
+ # Client instances are threadsafe.
42
+ #
16
43
  class Client
44
+ NotConnectedError = Class.new(ClientError)
45
+ InvalidKeyspaceNameError = Class.new(ClientError)
46
+
47
+ # Create a new client.
48
+ #
49
+ # Creating a client does not automatically connect to Cassandra, you need to
50
+ # call {#start!} to connect. `#start!` returns `self` so you can chain that
51
+ # call after `new`.
52
+ #
53
+ # @param [Hash] options
54
+ # @option options [String] :host ('localhost') One or more (comma separated)
55
+ # hostnames for the Cassandra nodes you want to connect to.
56
+ # @option options [String] :port (9042) The port to connect to
57
+ # @option options [Integer] :connection_timeout (5) Max time to wait for a
58
+ # connection, in seconds
59
+ # @option options [String] :keyspace The keyspace to change to immediately
60
+ # after all connections have been established, this is optional.
17
61
  def initialize(options={})
18
62
  connection_timeout = options[:connection_timeout]
19
63
  @host = options[:host] || 'localhost'
@@ -26,24 +70,44 @@ module Cql
26
70
  @connection_keyspaces = {}
27
71
  end
28
72
 
73
+ # Connect to all nodes.
74
+ #
75
+ # You must call this method before you call any of the other methods of a
76
+ # client. Calling it again will have no effect.
77
+ #
78
+ # If `:keyspace` was specified when the client was created the current
79
+ # keyspace will also be changed (otherwise the current keyspace will not
80
+ # be set).
81
+ #
82
+ # @return self
83
+ #
29
84
  def start!
30
85
  @lock.synchronize do
31
86
  return if @started
32
87
  @started = true
33
88
  end
34
- @io_reactor.start
35
- hosts = @host.split(',')
36
- start_request = Protocol::StartupRequest.new
37
- connection_futures = hosts.map do |host|
38
- @io_reactor.add_connection(host, @port).flat_map do |connection_id|
39
- execute_request(start_request, connection_id).map { connection_id }
89
+ begin
90
+ @io_reactor.start
91
+ hosts = @host.split(',')
92
+ start_request = Protocol::StartupRequest.new
93
+ connection_futures = hosts.map do |host|
94
+ @io_reactor.add_connection(host, @port).flat_map do |connection_id|
95
+ execute_request(start_request, connection_id).map { connection_id }
96
+ end
40
97
  end
98
+ @connection_ids = Future.combine(*connection_futures).get
99
+ use(@initial_keyspace) if @initial_keyspace
100
+ rescue
101
+ @started = false
102
+ raise
41
103
  end
42
- @connection_ids = Future.combine(*connection_futures).get
43
- use(@initial_keyspace) if @initial_keyspace
44
104
  self
45
105
  end
46
106
 
107
+ # Disconnect from all nodes.
108
+ #
109
+ # @return self
110
+ #
47
111
  def shutdown!
48
112
  @lock.synchronize do
49
113
  return if @shut_down
@@ -54,12 +118,27 @@ module Cql
54
118
  self
55
119
  end
56
120
 
121
+ # Returns whether or not the client is connected.
122
+ #
123
+ def connected?
124
+ @started
125
+ end
126
+
127
+ # Returns the name of the current keyspace, or `nil` if no keyspace has been
128
+ # set yet.
129
+ #
57
130
  def keyspace
58
131
  @lock.synchronize do
59
132
  return @connection_ids.map { |id| @connection_keyspaces[id] }.first
60
133
  end
61
134
  end
62
135
 
136
+ # Changes keyspace by sending a `USE` statement to all connections.
137
+ #
138
+ # The the second parameter is meant for internal use only.
139
+ #
140
+ # @raise [Cql::NotConnectedError] raised when the client is not connected
141
+ #
63
142
  def use(keyspace, connection_ids=@connection_ids)
64
143
  raise NotConnectedError unless @started
65
144
  if check_keyspace_name!(keyspace)
@@ -77,16 +156,32 @@ module Cql
77
156
  end
78
157
  end
79
158
 
159
+ # Execute a CQL statement
160
+ #
161
+ # @raise [Cql::NotConnectedError] raised when the client is not connected
162
+ # @raise [Cql::QueryError] raised when the CQL has syntax errors or for
163
+ # other situations when the server complains.
164
+ # @return [nil, Enumerable<Hash>] Most statements have no result and return
165
+ # `nil`, but `SELECT` statements return an `Enumerable` of rows
166
+ # (see {QueryResult}).
167
+ #
80
168
  def execute(cql, consistency=:quorum)
81
169
  result = execute_request(Protocol::QueryRequest.new(cql, consistency)).value
82
170
  ensure_keyspace!
83
171
  result
84
172
  end
85
173
 
174
+ # @private
86
175
  def execute_statement(connection_id, statement_id, metadata, values, consistency)
87
176
  execute_request(Protocol::ExecuteRequest.new(statement_id, metadata, values, consistency), connection_id).value
88
177
  end
89
178
 
179
+ # Returns a prepared statement that can be run over and over again with
180
+ # different values.
181
+ #
182
+ # @raise [Cql::NotConnectedError] raised when the client is not connected
183
+ # @return [Cql::PreparedStatement] an object encapsulating the prepared statement
184
+ #
90
185
  def prepare(cql)
91
186
  execute_request(Protocol::PrepareRequest.new(cql)).value
92
187
  end
@@ -136,58 +231,92 @@ module Cql
136
231
  use(ks, @connection_ids) if ks
137
232
  end
138
233
 
234
+ public
235
+
236
+ #
139
237
  class PreparedStatement
238
+ # @return [ResultMetadata]
239
+ attr_reader :metadata
240
+
140
241
  def initialize(*args)
141
- @client, @connection_id, @statement_id, @metadata = args
242
+ @client, @connection_id, @statement_id, @raw_metadata = args
243
+ @metadata = ResultMetadata.new(@raw_metadata)
142
244
  end
143
245
 
246
+ # Execute the prepared statement with a list of values for the bound parameters.
247
+ #
144
248
  def execute(*args)
145
- @client.execute_statement(@connection_id, @statement_id, @metadata, args, :quorum)
249
+ @client.execute_statement(@connection_id, @statement_id, @raw_metadata, args, :quorum)
146
250
  end
147
251
  end
148
252
 
149
253
  class QueryResult
150
254
  include Enumerable
151
255
 
256
+ # @return [ResultMetadata]
152
257
  attr_reader :metadata
153
258
 
259
+ # @private
154
260
  def initialize(metadata, rows)
155
261
  @metadata = ResultMetadata.new(metadata)
156
262
  @rows = rows
157
263
  end
158
264
 
265
+ # Returns whether or not there are any rows in this result set
266
+ #
159
267
  def empty?
160
268
  @rows.empty?
161
269
  end
162
270
 
271
+ # Iterates over each row in the result set.
272
+ #
273
+ # @yieldparam [Hash] row each row in the result set as a hash
274
+ # @return [Enumerable<Hash>]
275
+ #
163
276
  def each(&block)
164
277
  @rows.each(&block)
165
278
  end
279
+ alias_method :each_row, :each
166
280
  end
167
281
 
168
282
  class ResultMetadata
169
283
  include Enumerable
170
284
 
285
+ # @private
171
286
  def initialize(metadata)
172
- @metadata = Hash[metadata.map { |m| mm = ColumnMetadata.new(*m); [mm.column_name, mm] }]
287
+ @metadata = metadata.each_with_object({}) { |m, h| h[m[2]] = ColumnMetadata.new(*m) }
173
288
  end
174
289
 
290
+ # Returns the column metadata
291
+ #
292
+ # @return [ColumnMetadata] column_metadata the metadata for the column
293
+ #
175
294
  def [](column_name)
176
295
  @metadata[column_name]
177
296
  end
178
297
 
298
+ # Iterates over the metadata for each column
299
+ #
300
+ # @yieldparam [ColumnMetadata] metadata the metadata for each column
301
+ # @return [Enumerable<ColumnMetadata>]
302
+ #
179
303
  def each(&block)
180
304
  @metadata.each_value(&block)
181
305
  end
182
306
  end
183
307
 
308
+ # Represents metadata about a column in a query result set or prepared
309
+ # statement. Apart from the keyspace, table and column names there's also
310
+ # the type as a symbol (e.g. `:varchar`, `:int`, `:date`).
184
311
  class ColumnMetadata
185
- attr_reader :keyspace, :table, :table, :column_name, :type
312
+ attr_reader :keyspace, :table, :column_name, :type
186
313
 
314
+ # @private
187
315
  def initialize(*args)
188
316
  @keyspace, @table, @column_name, @type = args
189
317
  end
190
318
 
319
+ # @private
191
320
  def to_ary
192
321
  [@keyspace, @table, @column_name, @type]
193
322
  end
data/lib/cql/future.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module Cql
4
4
  FutureError = Class.new(CqlError)
5
5
 
6
+ # A future represents the value of a process that may not yet have completed.
7
+ #
6
8
  class Future
7
9
  def initialize
8
10
  @complete_listeners = []
@@ -11,18 +13,43 @@ module Cql
11
13
  @state_lock = Mutex.new
12
14
  end
13
15
 
16
+ # Combine multiple futures into a new future which completes when all
17
+ # constituent futures complete, or fail when one or more of them fails.
18
+ #
19
+ # The value of the combined future is an array of the values of the
20
+ # constituent futures.
21
+ #
22
+ # @param [Array<Future>] futures the futures to combine
23
+ # @return [Future<Array>] an array of the values of the constituent futures
24
+ #
14
25
  def self.combine(*futures)
15
26
  CombinedFuture.new(*futures)
16
27
  end
17
28
 
29
+ # Creates a new future which is completed.
30
+ #
31
+ # @param [Object, nil] value the value of the created future
32
+ # @return [Future] a completed future
33
+ #
18
34
  def self.completed(value=nil)
19
35
  CompletedFuture.new(value)
20
36
  end
21
37
 
38
+ # Creates a new future which is failed.
39
+ #
40
+ # @param [Error] the error of the created future
41
+ # @return [Future] a failed future
42
+ #
22
43
  def self.failed(error)
23
44
  FailedFuture.new(error)
24
45
  end
25
46
 
47
+ # Completes the future.
48
+ #
49
+ # This will trigger all completion listeners in the calling thread.
50
+ #
51
+ # @param [Object] v the value of the future
52
+ #
26
53
  def complete!(v=nil)
27
54
  @state_lock.synchronize do
28
55
  raise FutureError, 'Future already completed' if complete? || failed?
@@ -37,10 +64,16 @@ module Cql
37
64
  end
38
65
  end
39
66
 
67
+ # Returns whether or not the future is complete
68
+ #
40
69
  def complete?
41
70
  defined? @value
42
71
  end
43
72
 
73
+ # Registers a listener for when this future completes
74
+ #
75
+ # @yieldparam [Object] value the value of the completed future
76
+ #
44
77
  def on_complete(&listener)
45
78
  @state_lock.synchronize do
46
79
  if complete?
@@ -51,6 +84,12 @@ module Cql
51
84
  end
52
85
  end
53
86
 
87
+ # Returns the value of this future, blocking until it is available, if necessary.
88
+ #
89
+ # If the future fails this method will raise the error that failed the future.
90
+ #
91
+ # @returns [Object] the value of this future
92
+ #
54
93
  def value
55
94
  raise @error if @error
56
95
  return @value if defined? @value
@@ -60,6 +99,12 @@ module Cql
60
99
  end
61
100
  alias_method :get, :value
62
101
 
102
+ # Fails the future.
103
+ #
104
+ # This will trigger all failure listeners in the calling thread.
105
+ #
106
+ # @param [Error] error the error which prevented the value from being determined
107
+ #
63
108
  def fail!(error)
64
109
  @state_lock.synchronize do
65
110
  raise FutureError, 'Future already completed' if failed? || complete?
@@ -74,10 +119,16 @@ module Cql
74
119
  end
75
120
  end
76
121
 
122
+ # Returns whether or not the future is failed.
123
+ #
77
124
  def failed?
78
125
  !!@error
79
126
  end
80
127
 
128
+ # Registers a listener for when this future fails
129
+ #
130
+ # @yieldparam [Error] error the error that failed the future
131
+ #
81
132
  def on_failure(&listener)
82
133
  @state_lock.synchronize do
83
134
  if failed?
@@ -88,6 +139,15 @@ module Cql
88
139
  end
89
140
  end
90
141
 
142
+ # Returns a new future representing a transformation of this future's value.
143
+ #
144
+ # @example
145
+ # future2 = future1.map { |value| value * 2 }
146
+ #
147
+ # @yieldparam [Object] value the value of this future
148
+ # @yieldreturn [Object] the transformed value
149
+ # @return [Future] a new future representing the transformed value
150
+ #
91
151
  def map(&block)
92
152
  fp = Future.new
93
153
  on_failure { |e| fp.fail!(e) }
@@ -102,6 +162,18 @@ module Cql
102
162
  fp
103
163
  end
104
164
 
165
+ # Returns a new future representing a transformation of this future's value,
166
+ # but where the transformation itself may be asynchronous.
167
+ #
168
+ # @example
169
+ # future2 = future1.flat_map { |value| method_returning_a_future(value) }
170
+ #
171
+ # This method is useful when you want to chain asynchronous operations.
172
+ #
173
+ # @yieldparam [Object] value the value of this future
174
+ # @yieldreturn [Future] a future representing the transformed value
175
+ # @return [Future] a new future representing the transformed value
176
+ #
105
177
  def flat_map(&block)
106
178
  fp = Future.new
107
179
  on_failure { |e| fp.fail!(e) }
@@ -120,6 +192,7 @@ module Cql
120
192
  end
121
193
  end
122
194
 
195
+ # @private
123
196
  class CompletedFuture < Future
124
197
  def initialize(value=nil)
125
198
  super()
@@ -127,6 +200,7 @@ module Cql
127
200
  end
128
201
  end
129
202
 
203
+ # @private
130
204
  class FailedFuture < Future
131
205
  def initialize(error)
132
206
  super()
@@ -134,6 +208,7 @@ module Cql
134
208
  end
135
209
  end
136
210
 
211
+ # @private
137
212
  class CombinedFuture < Future
138
213
  def initialize(*futures)
139
214
  super()
data/lib/cql/io.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
+ IoError = Class.new(CqlError)
5
+
4
6
  module Io
5
- IoError = Class.new(CqlError)
6
7
  ConnectionError = Class.new(IoError)
7
8
  NotRunningError = Class.new(CqlError)
8
9
  ConnectionNotFoundError = Class.new(CqlError)
@@ -6,7 +6,17 @@ require 'resolv-replace'
6
6
 
7
7
  module Cql
8
8
  module Io
9
+ # An instance of IO reactor manages the connections used by a client.
10
+ #
11
+ # The reactor starts a thread in which all IO is performed. The IO reactor
12
+ # instances are thread safe.
13
+ #
9
14
  class IoReactor
15
+ #
16
+ # @param [Hash] options
17
+ # @option options [Integer] :connection_timeout (5) Max time to wait for a
18
+ # connection, in seconds
19
+ #
10
20
  def initialize(options={})
11
21
  @connection_timeout = options[:connection_timeout] || 5
12
22
  @lock = Mutex.new
@@ -18,10 +28,19 @@ module Cql
18
28
  @running = false
19
29
  end
20
30
 
31
+ # Returns whether or not the reactor is running
32
+ #
21
33
  def running?
22
34
  @running
23
35
  end
24
36
 
37
+ # Starts the reactor.
38
+ #
39
+ # Calling this method when the reactor is connecting or is connected has
40
+ # no effect.
41
+ #
42
+ # @return [Future<nil>] a future which completes when the reactor has started
43
+ #
25
44
  def start
26
45
  @lock.synchronize do
27
46
  unless @running
@@ -42,11 +61,25 @@ module Cql
42
61
  @started_future
43
62
  end
44
63
 
64
+ # Stops the reactor.
65
+ #
66
+ # Calling this method when the reactor is stopping or has stopped has
67
+ # no effect.
68
+ #
69
+ # @return [Future<nil>] a future which completes when the reactor has stopped
70
+ #
45
71
  def stop
46
72
  @running = false
47
73
  @stopped_future
48
74
  end
49
75
 
76
+ # Establish a new connection.
77
+ #
78
+ # @param [String] host The hostname to connect to
79
+ # @param [Integer] port The port to connect to
80
+ # @return [Future<Object>] a future representing the ID of the newly
81
+ # established connection, or connection error if the connection fails.
82
+ #
50
83
  def add_connection(host, port)
51
84
  connection = NodeConnection.new(host, port, @connection_timeout)
52
85
  future = connection.open
@@ -65,12 +98,23 @@ module Cql
65
98
  future
66
99
  end
67
100
 
101
+ # Sends a request over a random, or specific connection.
102
+ #
103
+ # @param [Cql::Protocol::RequestBody] request the request to send
104
+ # @param [Object] connection_id the ID of the connection which should be
105
+ # used to send the request
106
+ # @return [Future<ResultResponse>] a future representing the result of the request
107
+ #
68
108
  def queue_request(request, connection_id=nil)
69
109
  future = Future.new
70
110
  command_queue_push(:request, request, future, connection_id)
71
111
  future
72
112
  end
73
113
 
114
+ # Registers a listener to receive server sent events.
115
+ #
116
+ # @yieldparam [Cql::Protocol::EventResponse] event the event sent by the server
117
+ #
74
118
  def add_event_listener(&listener)
75
119
  command_queue_push(:event_listener, listener)
76
120
  end
@@ -108,6 +152,7 @@ module Cql
108
152
  end
109
153
  end
110
154
 
155
+ # @private
111
156
  class NodeConnection
112
157
  def initialize(*args)
113
158
  @host, @port, @connection_timeout = args
@@ -171,6 +216,9 @@ module Cql
171
216
  stream_id = next_stream_id
172
217
  Protocol::RequestFrame.new(request, stream_id).write(@write_buffer)
173
218
  @response_tasks[stream_id] = future
219
+ rescue => e
220
+ @response_tasks.delete(stream_id)
221
+ future.fail!(e)
174
222
  end
175
223
 
176
224
  def handle_read
@@ -201,7 +249,12 @@ module Cql
201
249
 
202
250
  def close
203
251
  if @io
204
- @io.close
252
+ begin
253
+ @io.close
254
+ rescue SystemCallError
255
+ # nothing to do, it wasn't open
256
+ end
257
+ @io = nil
205
258
  if connecting?
206
259
  succeed_connection!
207
260
  end
@@ -223,7 +276,7 @@ module Cql
223
276
  EVENT_STREAM_ID = -1
224
277
 
225
278
  def connecting?
226
- !@connected_future.complete?
279
+ !(@connected_future.complete? || @connected_future.failed?)
227
280
  end
228
281
 
229
282
  def handle_connected
@@ -246,8 +299,7 @@ module Cql
246
299
  error = ConnectionError.new(message)
247
300
  error.set_backtrace(e.backtrace) if e
248
301
  @connected_future.fail!(error)
249
- @io.close if @io
250
- @io = nil
302
+ close
251
303
  end
252
304
 
253
305
  def next_stream_id
@@ -258,6 +310,7 @@ module Cql
258
310
  end
259
311
  end
260
312
 
313
+ # @private
261
314
  class CommandDispatcher
262
315
  def initialize(*args)
263
316
  @io, @command_queue, @queue_lock, @node_connections = args
data/lib/cql/protocol.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
+ ProtocolError = Class.new(CqlError)
5
+
6
+ # @private
4
7
  module Protocol
5
- ProtocolError = Class.new(CqlError)
6
8
  DecodingError = Class.new(ProtocolError)
7
9
  EncodingError = Class.new(ProtocolError)
8
10
  InvalidStreamIdError = Class.new(ProtocolError)
data/lib/cql/uuid.rb CHANGED
@@ -1,7 +1,18 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
+ # Represents a UUID value.
5
+ #
6
+ # This is a very basic implementation of UUIDs and exists more or less just
7
+ # to encode and decode UUIDs from and to Cassandra.
8
+ #
4
9
  class Uuid
10
+ # Creates a new UUID either from a string (expected to be on the standard
11
+ # 8-4-4-4-12 form, or just 32 characters without hyphens), or from a
12
+ # 128 bit number.
13
+ #
14
+ # @raise [ArgumentError] if the string does not conform to the expected format
15
+ #
5
16
  def initialize(n)
6
17
  case n
7
18
  when String
@@ -11,6 +22,8 @@ module Cql
11
22
  end
12
23
  end
13
24
 
25
+ # Returns a string representation of this UUID in the standard 8-4-4-4-12 form.
26
+ #
14
27
  def to_s
15
28
  @s ||= begin
16
29
  parts = []
@@ -23,10 +36,15 @@ module Cql
23
36
  end
24
37
  end
25
38
 
39
+ # Returns the numerical representation of this UUID
40
+ #
41
+ # @return [Bignum] the 128 bit numerical representation
42
+ #
26
43
  def value
27
44
  @n
28
45
  end
29
46
 
47
+ # @private
30
48
  def eql?(other)
31
49
  self.value == other.value
32
50
  end
@@ -34,8 +52,12 @@ module Cql
34
52
 
35
53
  private
36
54
 
55
+ HEX_STRING_PATTERN = /^[0-9a-fA-F]+$/
56
+
37
57
  def from_s(str)
38
58
  str = str.gsub('-', '')
59
+ raise ArgumentError, "Expected 32 chars but got #{str.length}" unless str.length == 32
60
+ raise ArgumentError, "Expected only characters 0-9, a-f, A-F" unless str =~ HEX_STRING_PATTERN
39
61
  n = 0
40
62
  (str.length/2).times do |i|
41
63
  n = (n << 8) | str[i * 2, 2].to_i(16)
data/lib/cql/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
- VERSION = '1.0.0.pre0'.freeze
4
+ VERSION = '1.0.0.pre1'.freeze
5
5
  end
@@ -95,9 +95,25 @@ module Cql
95
95
 
96
96
  it 'validates the keyspace name before sending the USE command' do
97
97
  c = described_class.new(connection_options.merge(:keyspace => 'system; DROP KEYSPACE system'))
98
- expect { c.start! }.to raise_error(InvalidKeyspaceNameError)
98
+ expect { c.start! }.to raise_error(Client::InvalidKeyspaceNameError)
99
99
  requests.should_not include(Protocol::QueryRequest.new('USE system; DROP KEYSPACE system', :one))
100
100
  end
101
+
102
+ it 're-raises any errors raised' do
103
+ io_reactor.stub(:add_connection).and_raise(ArgumentError)
104
+ expect { client.start! }.to raise_error(ArgumentError)
105
+ end
106
+
107
+ it 'is not connected if an error is raised' do
108
+ io_reactor.stub(:add_connection).and_raise(ArgumentError)
109
+ client.start! rescue nil
110
+ client.should_not be_connected
111
+ end
112
+
113
+ it 'is connected after #start! returns' do
114
+ client.start!
115
+ client.should be_connected
116
+ end
101
117
  end
102
118
 
103
119
  describe '#shutdown!' do
@@ -153,7 +169,7 @@ module Cql
153
169
  end
154
170
 
155
171
  it 'raises an error if the keyspace name is not valid' do
156
- expect { client.use('system; DROP KEYSPACE system') }.to raise_error(InvalidKeyspaceNameError)
172
+ expect { client.use('system; DROP KEYSPACE system') }.to raise_error(Client::InvalidKeyspaceNameError)
157
173
  end
158
174
  end
159
175
 
@@ -294,6 +310,14 @@ module Cql
294
310
  last_request.should == Protocol::ExecuteRequest.new(id, metadata, ['foo'], :quorum)
295
311
  end
296
312
 
313
+ it 'returns a prepared statement that knows the metadata' do
314
+ id = 'A' * 32
315
+ metadata = [['stuff', 'things', 'item', :varchar]]
316
+ io_reactor.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
317
+ statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?')
318
+ statement.metadata['item'].type == :varchar
319
+ end
320
+
297
321
  it 'executes a prepared statement using the right connection' do
298
322
  client.shutdown!
299
323
  io_reactor.stop.get
@@ -326,34 +350,44 @@ module Cql
326
350
  end
327
351
 
328
352
  context 'when not connected' do
353
+ it 'is not connected before #start! has been called' do
354
+ client.should_not be_connected
355
+ end
356
+
357
+ it 'is not connected after #shutdown! has been called' do
358
+ client.start!
359
+ client.shutdown!
360
+ client.should_not be_connected
361
+ end
362
+
329
363
  it 'complains when #use is called before #start!' do
330
- expect { client.use('system') }.to raise_error(NotConnectedError)
364
+ expect { client.use('system') }.to raise_error(Client::NotConnectedError)
331
365
  end
332
366
 
333
367
  it 'complains when #use is called after #shutdown!' do
334
368
  client.start!
335
369
  client.shutdown!
336
- expect { client.use('system') }.to raise_error(NotConnectedError)
370
+ expect { client.use('system') }.to raise_error(Client::NotConnectedError)
337
371
  end
338
372
 
339
373
  it 'complains when #execute is called before #start!' do
340
- expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
374
+ expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
341
375
  end
342
376
 
343
377
  it 'complains when #execute is called after #shutdown!' do
344
378
  client.start!
345
379
  client.shutdown!
346
- expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
380
+ expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
347
381
  end
348
382
 
349
383
  it 'complains when #prepare is called before #start!' do
350
- expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
384
+ expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
351
385
  end
352
386
 
353
387
  it 'complains when #prepare is called after #shutdown!' do
354
388
  client.start!
355
389
  client.shutdown!
356
- expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
390
+ expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
357
391
  end
358
392
 
359
393
  it 'complains when #execute of a prepared statement is called after #shutdown!' do
@@ -361,7 +395,7 @@ module Cql
361
395
  io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, []))
362
396
  statement = client.prepare('DELETE FROM stuff WHERE id = 3')
363
397
  client.shutdown!
364
- expect { statement.execute }.to raise_error(NotConnectedError)
398
+ expect { statement.execute }.to raise_error(Client::NotConnectedError)
365
399
  end
366
400
  end
367
401
  end
@@ -27,8 +27,13 @@ module Cql
27
27
  end
28
28
 
29
29
  after do
30
- io_reactor.stop.get if io_reactor.running?
31
- server.stop!
30
+ begin
31
+ if io_reactor.running?
32
+ io_reactor.stop.get
33
+ end
34
+ ensure
35
+ server.stop!
36
+ end
32
37
  end
33
38
 
34
39
  describe '#initialize' do
@@ -76,7 +81,9 @@ module Cql
76
81
  end
77
82
 
78
83
  it 'succeeds connection futures when stopping while connecting' do
79
- f = io_reactor.add_connection(host, port + 9)
84
+ server.stop!
85
+ server.start!(accept_delay: 2)
86
+ f = io_reactor.add_connection(host, port)
80
87
  io_reactor.start
81
88
  io_reactor.stop
82
89
  f.get
@@ -135,7 +142,7 @@ module Cql
135
142
  io_reactor.start
136
143
  io_reactor.add_connection(host, port).get
137
144
  io_reactor.queue_request(Cql::Protocol::StartupRequest.new)
138
- sleep(0.01) until server.received_bytes.size > 0
145
+ await { server.received_bytes.size > 0 }
139
146
  server.received_bytes[3, 1].should == "\x01"
140
147
  end
141
148
 
@@ -143,7 +150,7 @@ module Cql
143
150
  io_reactor.queue_request(Cql::Protocol::StartupRequest.new)
144
151
  io_reactor.start
145
152
  io_reactor.add_connection(host, port).get
146
- sleep(0.01) until server.received_bytes.size > 0
153
+ await { server.received_bytes.size > 0 }
147
154
  server.received_bytes[3, 1].should == "\x01"
148
155
  end
149
156
 
@@ -226,6 +233,13 @@ module Cql
226
233
  pending 'as it is the reactor doesn\'t try to deliver requests when all connections are busy'
227
234
  end
228
235
 
236
+ it 'fails if there is an error when encoding the request' do
237
+ io_reactor.start
238
+ io_reactor.add_connection(host, port).get
239
+ f = io_reactor.queue_request(Cql::Protocol::QueryRequest.new('USE test', :foobar))
240
+ expect { f.get }.to raise_error(Cql::ProtocolError)
241
+ end
242
+
229
243
  it 'yields the response when completed' do
230
244
  response = nil
231
245
  io_reactor.start
@@ -234,8 +248,9 @@ module Cql
234
248
  f.on_complete do |r, _|
235
249
  response = r
236
250
  end
251
+ sleep(0.1)
237
252
  server.broadcast!("\x81\x00\x00\x02\x00\x00\x00\x00")
238
- sleep(0.01) until response
253
+ await { response }
239
254
  response.should == Cql::Protocol::ReadyResponse.new
240
255
  end
241
256
 
@@ -247,8 +262,9 @@ module Cql
247
262
  f.on_complete do |_, c|
248
263
  connection = c
249
264
  end
265
+ sleep(0.1)
250
266
  server.broadcast!("\x81\x00\x00\x02\x00\x00\x00\x00")
251
- sleep(0.01) until connection
267
+ await { connection }
252
268
  connection.should_not be_nil
253
269
  end
254
270
  end
@@ -259,32 +275,12 @@ module Cql
259
275
  io_reactor.start
260
276
  io_reactor.add_connection(host, port).get
261
277
  io_reactor.add_event_listener { |e| event = e }
278
+ sleep(0.1)
262
279
  server.broadcast!("\x81\x00\xFF\f\x00\x00\x00+\x00\rSCHEMA_CHANGE\x00\aDROPPED\x00\nkeyspace01\x00\x05users")
263
- sleep(0.01) until event
280
+ await { event }
264
281
  event.should == Cql::Protocol::SchemaChangeEventResponse.new('DROPPED', 'keyspace01', 'users')
265
282
  end
266
283
  end
267
-
268
- context 'when errors occur' do
269
- context 'in the IO loop' do
270
- before do
271
- bad_request = stub(:request)
272
- bad_request.stub(:opcode).and_raise(StandardError.new('Blurgh'))
273
- io_reactor.start
274
- io_reactor.add_connection(host, port).get
275
- io_reactor.queue_request(bad_request)
276
- end
277
-
278
- it 'stops' do
279
- sleep(0.1)
280
- io_reactor.should_not be_running
281
- end
282
-
283
- it 'fails the future returned from #stop' do
284
- expect { io_reactor.stop.get }.to raise_error('Blurgh')
285
- end
286
- end
287
- end
288
284
  end
289
285
  end
290
286
  end
@@ -10,6 +10,22 @@ module Cql
10
10
  Uuid.new('a4a70900-24e1-11df-8924-001ff3591711').to_s.should == 'a4a70900-24e1-11df-8924-001ff3591711'
11
11
  end
12
12
 
13
+ it 'can be created from a string without hyphens' do
14
+ Uuid.new('a4a7090024e111df8924001ff3591711').to_s.should == 'a4a70900-24e1-11df-8924-001ff3591711'
15
+ end
16
+
17
+ it 'raises an error if the string is shorter than 32 chars' do
18
+ expect { Uuid.new('a4a7090024e111df8924001ff359171') }.to raise_error(ArgumentError)
19
+ end
20
+
21
+ it 'raises an error if the string is longer than 32 chars' do
22
+ expect { Uuid.new('a4a7090024e111df8924001ff35917111') }.to raise_error(ArgumentError)
23
+ end
24
+
25
+ it 'raises an error if the string is not a hexadecimal number' do
26
+ expect { Uuid.new('a4a7090024e111df8924001ff359171x') }.to raise_error(ArgumentError)
27
+ end
28
+
13
29
  it 'can be created from a number' do
14
30
  Uuid.new(276263553384940695775376958868900023510).to_s.should == 'cfd66ccc-d857-4e90-b1e5-df98a3d40cd6'.force_encoding(::Encoding::ASCII)
15
31
  end
@@ -98,4 +98,14 @@ describe 'A CQL client' do
98
98
  end
99
99
  end
100
100
  end
101
+
102
+ context 'with error conditions' do
103
+ it 'raises an error for CQL syntax errors' do
104
+ expect { client.execute('BAD cql') }.to raise_error(Cql::CqlError)
105
+ end
106
+
107
+ it 'raises an error for bad consistency levels' do
108
+ expect { client.execute('SELECT * FROM system.peers', :helloworld) }.to raise_error(Cql::CqlError)
109
+ end
110
+ end
101
111
  end
data/spec/spec_helper.rb CHANGED
@@ -7,5 +7,6 @@ require 'cql'
7
7
  ENV['CASSANDRA_HOST'] ||= 'localhost'
8
8
 
9
9
 
10
+ require 'support/await_helper'
10
11
  require 'support/fake_server'
11
12
  require 'support/fake_io_reactor'
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module AwaitHelper
4
+ def await(timeout=5, &test)
5
+ started_at = Time.now
6
+ until test.call
7
+ yield
8
+ time_taken = Time.now - started_at
9
+ if time_taken > timeout
10
+ fail('Test took more than %.1fs' % [time_taken.to_f])
11
+ else
12
+ sleep(0.01)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec.configure do |c|
19
+ c.include(AwaitHelper)
20
+ end
@@ -13,7 +13,7 @@ class FakeServer
13
13
  @received_bytes = ''
14
14
  end
15
15
 
16
- def start!
16
+ def start!(options={})
17
17
  @lock.synchronize do
18
18
  return if @running
19
19
  @running = true
@@ -22,6 +22,7 @@ class FakeServer
22
22
  @started = Cql::Future.new
23
23
  @thread = Thread.start do
24
24
  Thread.current.abort_on_exception = true
25
+ sleep(options[:accept_delay] || 0)
25
26
  @started.complete!
26
27
  io_loop
27
28
  end
@@ -34,8 +35,10 @@ class FakeServer
34
35
  return unless @running
35
36
  @running = false
36
37
  end
37
- @thread.join
38
- @sockets.each(&:close)
38
+ if defined? @started
39
+ @thread.join
40
+ @sockets.each(&:close)
41
+ end
39
42
  end
40
43
 
41
44
  def broadcast!(bytes)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cql-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre0
4
+ version: 1.0.0.pre1
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-20 00:00:00.000000000 Z
12
+ date: 2013-02-24 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: A pure Ruby CQL3 driver for Cassandra
15
15
  email:
@@ -43,6 +43,7 @@ files:
43
43
  - spec/integration/client_spec.rb
44
44
  - spec/integration/protocol_spec.rb
45
45
  - spec/spec_helper.rb
46
+ - spec/support/await_helper.rb
46
47
  - spec/support/fake_io_reactor.rb
47
48
  - spec/support/fake_server.rb
48
49
  homepage: http://github.com/iconara/cql-rb
@@ -82,6 +83,7 @@ test_files:
82
83
  - spec/integration/client_spec.rb
83
84
  - spec/integration/protocol_spec.rb
84
85
  - spec/spec_helper.rb
86
+ - spec/support/await_helper.rb
85
87
  - spec/support/fake_io_reactor.rb
86
88
  - spec/support/fake_server.rb
87
89
  has_rdoc: