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