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.
- data/lib/cql/byte_buffer.rb +22 -0
- data/lib/cql/client.rb +79 -310
- data/lib/cql/client/asynchronous_client.rb +254 -0
- data/lib/cql/client/asynchronous_prepared_statement.rb +19 -0
- data/lib/cql/client/column_metadata.rb +22 -0
- data/lib/cql/client/query_result.rb +34 -0
- data/lib/cql/client/result_metadata.rb +31 -0
- data/lib/cql/client/synchronous_client.rb +47 -0
- data/lib/cql/client/synchronous_prepared_statement.rb +47 -0
- data/lib/cql/future.rb +7 -3
- data/lib/cql/io.rb +1 -0
- data/lib/cql/io/io_reactor.rb +9 -3
- data/lib/cql/io/node_connection.rb +2 -2
- data/lib/cql/protocol.rb +5 -4
- data/lib/cql/protocol/request.rb +23 -0
- data/lib/cql/protocol/requests/credentials_request.rb +1 -1
- data/lib/cql/protocol/requests/execute_request.rb +13 -84
- data/lib/cql/protocol/requests/options_request.rb +1 -1
- data/lib/cql/protocol/requests/prepare_request.rb +2 -1
- data/lib/cql/protocol/requests/query_request.rb +3 -1
- data/lib/cql/protocol/requests/register_request.rb +1 -1
- data/lib/cql/protocol/requests/startup_request.rb +1 -2
- data/lib/cql/protocol/{response_body.rb → response.rb} +1 -1
- data/lib/cql/protocol/responses/authenticate_response.rb +1 -1
- data/lib/cql/protocol/responses/error_response.rb +1 -1
- data/lib/cql/protocol/responses/ready_response.rb +1 -1
- data/lib/cql/protocol/responses/result_response.rb +1 -1
- data/lib/cql/protocol/responses/rows_result_response.rb +1 -1
- data/lib/cql/protocol/responses/supported_response.rb +1 -1
- data/lib/cql/protocol/type_converter.rb +226 -46
- data/lib/cql/version.rb +1 -1
- data/spec/cql/byte_buffer_spec.rb +38 -0
- data/spec/cql/client/asynchronous_client_spec.rb +472 -0
- data/spec/cql/client/client_shared.rb +27 -0
- data/spec/cql/client/synchronous_client_spec.rb +104 -0
- data/spec/cql/client/synchronous_prepared_statement_spec.rb +65 -0
- data/spec/cql/future_spec.rb +4 -0
- data/spec/cql/io/io_reactor_spec.rb +39 -20
- data/spec/cql/protocol/request_spec.rb +17 -0
- data/spec/cql/protocol/requests/credentials_request_spec.rb +82 -0
- data/spec/cql/protocol/requests/execute_request_spec.rb +174 -0
- data/spec/cql/protocol/requests/options_request_spec.rb +24 -0
- data/spec/cql/protocol/requests/prepare_request_spec.rb +70 -0
- data/spec/cql/protocol/requests/query_request_spec.rb +95 -0
- data/spec/cql/protocol/requests/register_request_spec.rb +24 -0
- data/spec/cql/protocol/requests/startup_request_spec.rb +29 -0
- data/spec/integration/client_spec.rb +26 -19
- data/spec/integration/protocol_spec.rb +2 -2
- data/spec/integration/regression_spec.rb +1 -1
- metadata +35 -9
- data/lib/cql/protocol/request_body.rb +0 -15
- data/lib/cql/protocol/request_frame.rb +0 -20
- data/spec/cql/client_spec.rb +0 -454
- 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
|