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
@@ -0,0 +1,254 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class AsynchronousClient < Client
7
+ def initialize(options={})
8
+ connection_timeout = options[:connection_timeout]
9
+ @host = options[:host] || 'localhost'
10
+ @port = options[:port] || 9042
11
+ @io_reactor = options[:io_reactor] || Io::IoReactor.new(connection_timeout: connection_timeout)
12
+ @lock = Mutex.new
13
+ @connected = false
14
+ @connecting = false
15
+ @closing = false
16
+ @initial_keyspace = options[:keyspace]
17
+ @credentials = options[:credentials]
18
+ @connection_keyspaces = {}
19
+ end
20
+
21
+ def connect
22
+ @lock.synchronize do
23
+ return @connected_future if @connected || @connecting
24
+ @connecting = true
25
+ @connected_future = Future.new
26
+ end
27
+ when_not_closing do
28
+ setup_connections
29
+ end
30
+ @connected_future.on_complete do
31
+ @lock.synchronize do
32
+ @connecting = false
33
+ @connected = true
34
+ end
35
+ end
36
+ @connected_future.on_failure do
37
+ @lock.synchronize do
38
+ @connecting = false
39
+ @connected = false
40
+ end
41
+ end
42
+ @connected_future
43
+ end
44
+
45
+ def close
46
+ @lock.synchronize do
47
+ return @closed_future if @closing
48
+ @closing = true
49
+ @closed_future = Future.new
50
+ end
51
+ when_not_connecting do
52
+ f = @io_reactor.stop
53
+ f.on_complete { @closed_future.complete!(self) }
54
+ f.on_failure { @closed_future.fail!(e) }
55
+ end
56
+ @closed_future.on_complete do
57
+ @lock.synchronize do
58
+ @closing = false
59
+ @connected = false
60
+ end
61
+ end
62
+ @closed_future.on_failure do
63
+ @lock.synchronize do
64
+ @closing = false
65
+ @connected = false
66
+ end
67
+ end
68
+ @closed_future
69
+ end
70
+
71
+ def connected?
72
+ @connected
73
+ end
74
+
75
+ def keyspace
76
+ @lock.synchronize do
77
+ return @connection_ids.map { |id| @connection_keyspaces[id] }.first
78
+ end
79
+ end
80
+
81
+ def use(keyspace, connection_ids=nil)
82
+ return Future.failed(NotConnectedError.new) unless @connected || @connecting
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 }
87
+ 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)
91
+ end
92
+ futures.compact!
93
+ return Future.combine(*futures).map { nil }
94
+ else
95
+ Future.completed(nil)
96
+ end
97
+ end
98
+
99
+ def execute(cql, consistency=nil)
100
+ 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)
116
+ rescue => e
117
+ Future.failed(e)
118
+ end
119
+
120
+ def prepare(cql)
121
+ return Future.failed(NotConnectedError.new) unless @connected || @connecting
122
+ execute_request(Protocol::PrepareRequest.new(cql))
123
+ rescue => e
124
+ Future.failed(e)
125
+ end
126
+
127
+ private
128
+
129
+ KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$/
130
+ DEFAULT_CONSISTENCY_LEVEL = :quorum
131
+
132
+ def valid_keyspace_name?(name)
133
+ name =~ KEYSPACE_NAME_PATTERN
134
+ end
135
+
136
+ def when_not_connecting(&callback)
137
+ if @connecting
138
+ @connected_future.on_complete(&callback)
139
+ @connected_future.on_failure(&callback)
140
+ else
141
+ callback.call
142
+ end
143
+ end
144
+
145
+ def when_not_closing(&callback)
146
+ if @closing
147
+ @closed_future.on_complete(&callback)
148
+ @closed_future.on_failure(&callback)
149
+ else
150
+ callback.call
151
+ end
152
+ end
153
+
154
+ def setup_connections
155
+ hosts_connected_future = @io_reactor.start.flat_map do
156
+ hosts = @host.split(',')
157
+ connection_futures = hosts.map { |host| connect_to_host(host) }
158
+ Future.combine(*connection_futures)
159
+ end
160
+ hosts_connected_future.on_complete do |connection_ids|
161
+ @connection_ids = connection_ids
162
+ end
163
+ if @initial_keyspace
164
+ initialized_future = hosts_connected_future.flat_map do |*args|
165
+ use(@initial_keyspace)
166
+ end
167
+ else
168
+ initialized_future = hosts_connected_future
169
+ end
170
+ initialized_future.on_failure do |e|
171
+ close
172
+ if e.is_a?(Cql::QueryError) && e.code == 0x100
173
+ @connected_future.fail!(AuthenticationError.new(e.message))
174
+ else
175
+ @connected_future.fail!(e)
176
+ end
177
+ end
178
+ initialized_future.on_complete do
179
+ @connected_future.complete!(self)
180
+ end
181
+ end
182
+
183
+ 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) }
188
+ end
189
+ end
190
+
191
+ def maybe_authenticate(response, connection_id)
192
+ case response
193
+ when AuthenticationRequired
194
+ if @credentials
195
+ credentials_request = Protocol::CredentialsRequest.new(@credentials)
196
+ execute_request(credentials_request, connection_id).map { connection_id }
197
+ else
198
+ Future.failed(AuthenticationError.new('Server requested authentication, but no credentials given'))
199
+ end
200
+ 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)
208
+ end
209
+ end
210
+
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)
217
+ else
218
+ raise QueryError.new(response.code, response.message)
219
+ 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
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class AsynchronousPreparedStatement < PreparedStatement
7
+ def initialize(*args)
8
+ @client, @connection_id, @statement_id, @raw_metadata = args
9
+ @metadata = ResultMetadata.new(@raw_metadata)
10
+ end
11
+
12
+ def execute(*args)
13
+ 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)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # Represents metadata about a column in a query result set or prepared
6
+ # statement. Apart from the keyspace, table and column names there's also
7
+ # the type as a symbol (e.g. `:varchar`, `:int`, `:date`).
8
+ class ColumnMetadata
9
+ attr_reader :keyspace, :table, :column_name, :type
10
+
11
+ # @private
12
+ def initialize(*args)
13
+ @keyspace, @table, @column_name, @type = args
14
+ end
15
+
16
+ # @private
17
+ def to_ary
18
+ [@keyspace, @table, @column_name, @type]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ class QueryResult
6
+ include Enumerable
7
+
8
+ # @return [ResultMetadata]
9
+ attr_reader :metadata
10
+
11
+ # @private
12
+ def initialize(metadata, rows)
13
+ @metadata = ResultMetadata.new(metadata)
14
+ @rows = rows
15
+ end
16
+
17
+ # Returns whether or not there are any rows in this result set
18
+ #
19
+ def empty?
20
+ @rows.empty?
21
+ end
22
+
23
+ # Iterates over each row in the result set.
24
+ #
25
+ # @yieldparam [Hash] row each row in the result set as a hash
26
+ # @return [Enumerable<Hash>]
27
+ #
28
+ def each(&block)
29
+ @rows.each(&block)
30
+ end
31
+ alias_method :each_row, :each
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ class ResultMetadata
6
+ include Enumerable
7
+
8
+ # @private
9
+ def initialize(metadata)
10
+ @metadata = metadata.each_with_object({}) { |m, h| h[m[2]] = ColumnMetadata.new(*m) }
11
+ end
12
+
13
+ # Returns the column metadata
14
+ #
15
+ # @return [ColumnMetadata] column_metadata the metadata for the column
16
+ #
17
+ def [](column_name)
18
+ @metadata[column_name]
19
+ end
20
+
21
+ # Iterates over the metadata for each column
22
+ #
23
+ # @yieldparam [ColumnMetadata] metadata the metadata for each column
24
+ # @return [Enumerable<ColumnMetadata>]
25
+ #
26
+ def each(&block)
27
+ @metadata.each_value(&block)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class SynchronousClient < Client
7
+ def initialize(async_client)
8
+ @async_client = async_client
9
+ end
10
+
11
+ def connect
12
+ @async_client.connect.get
13
+ self
14
+ end
15
+
16
+ def close
17
+ @async_client.close.get
18
+ self
19
+ end
20
+
21
+ def connected?
22
+ @async_client.connected?
23
+ end
24
+
25
+ def keyspace
26
+ @async_client.keyspace
27
+ end
28
+
29
+ def use(keyspace)
30
+ @async_client.use(keyspace).get
31
+ end
32
+
33
+ def execute(cql, consistency=nil)
34
+ @async_client.execute(cql, consistency).get
35
+ end
36
+
37
+ def prepare(cql)
38
+ async_statement = @async_client.prepare(cql).get
39
+ SynchronousPreparedStatement.new(async_statement)
40
+ end
41
+
42
+ def async
43
+ @async_client
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class SynchronousPreparedStatement < PreparedStatement
7
+ def initialize(async_statement)
8
+ @async_statement = async_statement
9
+ @metadata = async_statement.metadata
10
+ end
11
+
12
+ def execute(*args)
13
+ @async_statement.execute(*args).get
14
+ end
15
+
16
+ def pipeline
17
+ pl = Pipeline.new(@async_statement)
18
+ yield pl
19
+ pl.get
20
+ end
21
+
22
+ def async
23
+ @async_statement
24
+ end
25
+ end
26
+
27
+ # @private
28
+ class Pipeline
29
+ def initialize(async_statement)
30
+ @async_statement = async_statement
31
+ @futures = []
32
+ end
33
+
34
+ def execute(*args)
35
+ @futures << @async_statement.execute(*args)
36
+ end
37
+
38
+ def get
39
+ if @futures.any?
40
+ Future.combine(*@futures).get
41
+ else
42
+ []
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end