cql-rb 1.0.0.pre0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +13 -0
- data/bin/cqlexec +135 -0
- data/lib/cql.rb +11 -0
- data/lib/cql/client.rb +196 -0
- data/lib/cql/future.rb +176 -0
- data/lib/cql/io.rb +13 -0
- data/lib/cql/io/io_reactor.rb +351 -0
- data/lib/cql/protocol.rb +39 -0
- data/lib/cql/protocol/decoding.rb +156 -0
- data/lib/cql/protocol/encoding.rb +109 -0
- data/lib/cql/protocol/request_frame.rb +228 -0
- data/lib/cql/protocol/response_frame.rb +551 -0
- data/lib/cql/uuid.rb +46 -0
- data/lib/cql/version.rb +5 -0
- data/spec/cql/client_spec.rb +368 -0
- data/spec/cql/future_spec.rb +297 -0
- data/spec/cql/io/io_reactor_spec.rb +290 -0
- data/spec/cql/protocol/decoding_spec.rb +464 -0
- data/spec/cql/protocol/encoding_spec.rb +338 -0
- data/spec/cql/protocol/request_frame_spec.rb +359 -0
- data/spec/cql/protocol/response_frame_spec.rb +746 -0
- data/spec/cql/uuid_spec.rb +40 -0
- data/spec/integration/client_spec.rb +101 -0
- data/spec/integration/protocol_spec.rb +326 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/fake_io_reactor.rb +55 -0
- data/spec/support/fake_server.rb +95 -0
- metadata +87 -0
data/README.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Ruby CQL 3 driver
|
2
|
+
|
3
|
+
This is a work in progress, no usable version exists yet.
|
4
|
+
|
5
|
+
## Copyright
|
6
|
+
|
7
|
+
Copyright 2013 Theo Hultberg
|
8
|
+
|
9
|
+
_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
|
+
|
11
|
+
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
12
|
+
|
13
|
+
_Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License._
|
data/bin/cqlexec
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
$: << File.expand_path('../../lib', __FILE__)
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
require 'set'
|
8
|
+
require 'cql'
|
9
|
+
|
10
|
+
|
11
|
+
class CqlExecutor
|
12
|
+
def initialize(args)
|
13
|
+
@options = parse_options!(args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(io)
|
17
|
+
@client = Cql::Client.new(host: @options[:host], port: @options[:port]).start!
|
18
|
+
|
19
|
+
# TODO register for events
|
20
|
+
|
21
|
+
begin
|
22
|
+
buffer = ''
|
23
|
+
while (line = io.gets)
|
24
|
+
buffer << line
|
25
|
+
if semi_index = buffer.index(';')
|
26
|
+
query = buffer.slice!(0, semi_index + 1)
|
27
|
+
prepare_and_execute_request(query)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
prepare_and_execute_request(buffer)
|
31
|
+
rescue Cql::CqlError => e
|
32
|
+
abort("Error: #{e.message} (#{e.class})")
|
33
|
+
rescue Interrupt
|
34
|
+
exit
|
35
|
+
ensure
|
36
|
+
@client.shutdown!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def parse_options!(args)
|
43
|
+
options = {}
|
44
|
+
|
45
|
+
option_parser = OptionParser.new do |opts|
|
46
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
|
47
|
+
opts.separator('')
|
48
|
+
opts.on('--help', 'Show this message') do
|
49
|
+
$stderr.puts(opts)
|
50
|
+
exit!
|
51
|
+
end
|
52
|
+
opts.on('-h', '--host [HOST]', 'Connect to HOST, defaults to localhost') do |host|
|
53
|
+
options[:host] = host
|
54
|
+
end
|
55
|
+
opts.on('-p', '--port [PORT]', Integer, 'Connect to PORT, defaults to 9042') do |port|
|
56
|
+
options[:port] = port
|
57
|
+
end
|
58
|
+
opts.on('-v', '--verbose', 'Print requests to STDERR') do |port|
|
59
|
+
options[:verbose] = true
|
60
|
+
end
|
61
|
+
opts.separator('')
|
62
|
+
opts.separator('Pass CQL commands on STDIN, prints results on STDOUT')
|
63
|
+
opts.separator('')
|
64
|
+
end
|
65
|
+
|
66
|
+
option_parser.parse!
|
67
|
+
|
68
|
+
options
|
69
|
+
end
|
70
|
+
|
71
|
+
def prepare_and_execute_request(query)
|
72
|
+
query.chomp!(';')
|
73
|
+
query.strip!
|
74
|
+
unless query.empty?
|
75
|
+
format_table_output(@client.execute(query, :one))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_table_output(rows)
|
80
|
+
table_names = []
|
81
|
+
header = ''
|
82
|
+
row_format = ''
|
83
|
+
divider = ''
|
84
|
+
column_widths = {}
|
85
|
+
if rows && rows.any?
|
86
|
+
rows.metadata.each do |ks, table, column, type|
|
87
|
+
table_names << [ks, table].join('.')
|
88
|
+
column_width = [format_value(rows.first[column]).length, column.length].max
|
89
|
+
column_widths[column] = column_width
|
90
|
+
format = "%-#{column_width}.#{column_width}s"
|
91
|
+
header << "#{format} | " % [column]
|
92
|
+
row_format << "#{format} | "
|
93
|
+
divider << ('-' * column_width) << '-+-'
|
94
|
+
end
|
95
|
+
row_format.sub!(/ \| $/, '')
|
96
|
+
divider = divider[0..-4]
|
97
|
+
table_name = table_names.uniq.join(', ')
|
98
|
+
|
99
|
+
$stdout.puts(table_name)
|
100
|
+
$stdout.puts('=' * table_name.length)
|
101
|
+
$stdout.puts(header[0, divider.length])
|
102
|
+
$stdout.puts(divider)
|
103
|
+
|
104
|
+
rows.each do |row|
|
105
|
+
values = rows.metadata.map do |_, _, column, _|
|
106
|
+
limit_width(format_value(row[column]), column_widths[column])
|
107
|
+
end
|
108
|
+
$stdout.puts(row_format % values)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
$stdout.puts('(empty result set)')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def format_value(value)
|
116
|
+
case value
|
117
|
+
when Set
|
118
|
+
value.to_a.to_s
|
119
|
+
when nil
|
120
|
+
'(nil)'
|
121
|
+
else
|
122
|
+
value.to_s
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def limit_width(value, width)
|
127
|
+
if value.length > width
|
128
|
+
value[0, width - 1] << '…'
|
129
|
+
else
|
130
|
+
value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
CqlExecutor.new(ARGV).run(STDIN)
|
data/lib/cql.rb
ADDED
data/lib/cql/client.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
NotConnectedError = Class.new(CqlError)
|
5
|
+
InvalidKeyspaceNameError = Class.new(CqlError)
|
6
|
+
|
7
|
+
class QueryError < CqlError
|
8
|
+
attr_reader :code
|
9
|
+
|
10
|
+
def initialize(code, message)
|
11
|
+
super(message)
|
12
|
+
@code = code
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Client
|
17
|
+
def initialize(options={})
|
18
|
+
connection_timeout = options[:connection_timeout]
|
19
|
+
@host = options[:host] || 'localhost'
|
20
|
+
@port = options[:port] || 9042
|
21
|
+
@io_reactor = options[:io_reactor] || Io::IoReactor.new(connection_timeout: connection_timeout)
|
22
|
+
@lock = Mutex.new
|
23
|
+
@started = false
|
24
|
+
@shut_down = false
|
25
|
+
@initial_keyspace = options[:keyspace]
|
26
|
+
@connection_keyspaces = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def start!
|
30
|
+
@lock.synchronize do
|
31
|
+
return if @started
|
32
|
+
@started = true
|
33
|
+
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 }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
@connection_ids = Future.combine(*connection_futures).get
|
43
|
+
use(@initial_keyspace) if @initial_keyspace
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def shutdown!
|
48
|
+
@lock.synchronize do
|
49
|
+
return if @shut_down
|
50
|
+
@shut_down = true
|
51
|
+
@started = false
|
52
|
+
end
|
53
|
+
@io_reactor.stop.get
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def keyspace
|
58
|
+
@lock.synchronize do
|
59
|
+
return @connection_ids.map { |id| @connection_keyspaces[id] }.first
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def use(keyspace, connection_ids=@connection_ids)
|
64
|
+
raise NotConnectedError unless @started
|
65
|
+
if check_keyspace_name!(keyspace)
|
66
|
+
@lock.synchronize do
|
67
|
+
connection_ids = connection_ids.select { |id| @connection_keyspaces[id] != keyspace }
|
68
|
+
end
|
69
|
+
if connection_ids.any?
|
70
|
+
futures = connection_ids.map do |connection_id|
|
71
|
+
execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection_id)
|
72
|
+
end
|
73
|
+
futures.compact!
|
74
|
+
Future.combine(*futures).get
|
75
|
+
end
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def execute(cql, consistency=:quorum)
|
81
|
+
result = execute_request(Protocol::QueryRequest.new(cql, consistency)).value
|
82
|
+
ensure_keyspace!
|
83
|
+
result
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute_statement(connection_id, statement_id, metadata, values, consistency)
|
87
|
+
execute_request(Protocol::ExecuteRequest.new(statement_id, metadata, values, consistency), connection_id).value
|
88
|
+
end
|
89
|
+
|
90
|
+
def prepare(cql)
|
91
|
+
execute_request(Protocol::PrepareRequest.new(cql)).value
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$/
|
97
|
+
|
98
|
+
def check_keyspace_name!(name)
|
99
|
+
if name !~ KEYSPACE_NAME_PATTERN
|
100
|
+
raise InvalidKeyspaceNameError, %("#{name}" is not a valid keyspace name)
|
101
|
+
end
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def execute_request(request, connection_id=nil)
|
106
|
+
raise NotConnectedError unless @started
|
107
|
+
@io_reactor.queue_request(request, connection_id).map do |response, connection_id|
|
108
|
+
interpret_response!(response, connection_id)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def interpret_response!(response, connection_id)
|
113
|
+
case response
|
114
|
+
when Protocol::ErrorResponse
|
115
|
+
raise QueryError.new(response.code, response.message)
|
116
|
+
when Protocol::RowsResultResponse
|
117
|
+
QueryResult.new(response.metadata, response.rows)
|
118
|
+
when Protocol::PreparedResultResponse
|
119
|
+
PreparedStatement.new(self, connection_id, response.id, response.metadata)
|
120
|
+
when Protocol::SetKeyspaceResultResponse
|
121
|
+
@lock.synchronize do
|
122
|
+
@last_keyspace_change = @connection_keyspaces[connection_id] = response.keyspace
|
123
|
+
end
|
124
|
+
nil
|
125
|
+
else
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def ensure_keyspace!
|
131
|
+
ks = nil
|
132
|
+
@lock.synchronize do
|
133
|
+
ks = @last_keyspace_change
|
134
|
+
return unless @last_keyspace_change
|
135
|
+
end
|
136
|
+
use(ks, @connection_ids) if ks
|
137
|
+
end
|
138
|
+
|
139
|
+
class PreparedStatement
|
140
|
+
def initialize(*args)
|
141
|
+
@client, @connection_id, @statement_id, @metadata = args
|
142
|
+
end
|
143
|
+
|
144
|
+
def execute(*args)
|
145
|
+
@client.execute_statement(@connection_id, @statement_id, @metadata, args, :quorum)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class QueryResult
|
150
|
+
include Enumerable
|
151
|
+
|
152
|
+
attr_reader :metadata
|
153
|
+
|
154
|
+
def initialize(metadata, rows)
|
155
|
+
@metadata = ResultMetadata.new(metadata)
|
156
|
+
@rows = rows
|
157
|
+
end
|
158
|
+
|
159
|
+
def empty?
|
160
|
+
@rows.empty?
|
161
|
+
end
|
162
|
+
|
163
|
+
def each(&block)
|
164
|
+
@rows.each(&block)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class ResultMetadata
|
169
|
+
include Enumerable
|
170
|
+
|
171
|
+
def initialize(metadata)
|
172
|
+
@metadata = Hash[metadata.map { |m| mm = ColumnMetadata.new(*m); [mm.column_name, mm] }]
|
173
|
+
end
|
174
|
+
|
175
|
+
def [](column_name)
|
176
|
+
@metadata[column_name]
|
177
|
+
end
|
178
|
+
|
179
|
+
def each(&block)
|
180
|
+
@metadata.each_value(&block)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class ColumnMetadata
|
185
|
+
attr_reader :keyspace, :table, :table, :column_name, :type
|
186
|
+
|
187
|
+
def initialize(*args)
|
188
|
+
@keyspace, @table, @column_name, @type = args
|
189
|
+
end
|
190
|
+
|
191
|
+
def to_ary
|
192
|
+
[@keyspace, @table, @column_name, @type]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
data/lib/cql/future.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
FutureError = Class.new(CqlError)
|
5
|
+
|
6
|
+
class Future
|
7
|
+
def initialize
|
8
|
+
@complete_listeners = []
|
9
|
+
@failure_listeners = []
|
10
|
+
@value_barrier = Queue.new
|
11
|
+
@state_lock = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.combine(*futures)
|
15
|
+
CombinedFuture.new(*futures)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.completed(value=nil)
|
19
|
+
CompletedFuture.new(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.failed(error)
|
23
|
+
FailedFuture.new(error)
|
24
|
+
end
|
25
|
+
|
26
|
+
def complete!(v=nil)
|
27
|
+
@state_lock.synchronize do
|
28
|
+
raise FutureError, 'Future already completed' if complete? || failed?
|
29
|
+
@value = v
|
30
|
+
@complete_listeners.each do |listener|
|
31
|
+
listener.call(@value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
ensure
|
35
|
+
@state_lock.synchronize do
|
36
|
+
@value_barrier << :ping
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def complete?
|
41
|
+
defined? @value
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_complete(&listener)
|
45
|
+
@state_lock.synchronize do
|
46
|
+
if complete?
|
47
|
+
listener.call(value)
|
48
|
+
else
|
49
|
+
@complete_listeners << listener
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def value
|
55
|
+
raise @error if @error
|
56
|
+
return @value if defined? @value
|
57
|
+
@value_barrier.pop
|
58
|
+
raise @error if @error
|
59
|
+
return @value
|
60
|
+
end
|
61
|
+
alias_method :get, :value
|
62
|
+
|
63
|
+
def fail!(error)
|
64
|
+
@state_lock.synchronize do
|
65
|
+
raise FutureError, 'Future already completed' if failed? || complete?
|
66
|
+
@error = error
|
67
|
+
@failure_listeners.each do |listener|
|
68
|
+
listener.call(error)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
@state_lock.synchronize do
|
73
|
+
@value_barrier << :ping
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def failed?
|
78
|
+
!!@error
|
79
|
+
end
|
80
|
+
|
81
|
+
def on_failure(&listener)
|
82
|
+
@state_lock.synchronize do
|
83
|
+
if failed?
|
84
|
+
listener.call(@error)
|
85
|
+
else
|
86
|
+
@failure_listeners << listener
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def map(&block)
|
92
|
+
fp = Future.new
|
93
|
+
on_failure { |e| fp.fail!(e) }
|
94
|
+
on_complete do |v|
|
95
|
+
begin
|
96
|
+
vv = block.call(v)
|
97
|
+
fp.complete!(vv)
|
98
|
+
rescue => e
|
99
|
+
fp.fail!(e)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
fp
|
103
|
+
end
|
104
|
+
|
105
|
+
def flat_map(&block)
|
106
|
+
fp = Future.new
|
107
|
+
on_failure { |e| fp.fail!(e) }
|
108
|
+
on_complete do |v|
|
109
|
+
begin
|
110
|
+
fpp = block.call(v)
|
111
|
+
fpp.on_failure { |e| fp.fail!(e) }
|
112
|
+
fpp.on_complete do |vv|
|
113
|
+
fp.complete!(vv)
|
114
|
+
end
|
115
|
+
rescue => e
|
116
|
+
fp.fail!(e)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
fp
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class CompletedFuture < Future
|
124
|
+
def initialize(value=nil)
|
125
|
+
super()
|
126
|
+
complete!(value)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class FailedFuture < Future
|
131
|
+
def initialize(error)
|
132
|
+
super()
|
133
|
+
fail!(error)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class CombinedFuture < Future
|
138
|
+
def initialize(*futures)
|
139
|
+
super()
|
140
|
+
values = [nil] * futures.size
|
141
|
+
completed = [false] * futures.size
|
142
|
+
futures.each_with_index do |f, i|
|
143
|
+
f.on_complete do |v|
|
144
|
+
all_done = false
|
145
|
+
@state_lock.synchronize do
|
146
|
+
values[i] = v
|
147
|
+
completed[i] = true
|
148
|
+
all_done = completed.all?
|
149
|
+
end
|
150
|
+
if all_done
|
151
|
+
combined_complete!(values)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
f.on_failure do |e|
|
155
|
+
unless failed?
|
156
|
+
combined_fail!(e)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
alias_method :combined_complete!, :complete!
|
163
|
+
private :combined_complete!
|
164
|
+
|
165
|
+
alias_method :combined_fail!, :fail!
|
166
|
+
private :combined_fail!
|
167
|
+
|
168
|
+
def complete!(v=nil)
|
169
|
+
raise FutureError, 'Cannot complete a combined future'
|
170
|
+
end
|
171
|
+
|
172
|
+
def fail!(e)
|
173
|
+
raise FutureError, 'Cannot fail a combined future'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|