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