jamesgolick-cassandra 0.8.2
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/CHANGELOG +51 -0
- data/LICENSE +202 -0
- data/Manifest +31 -0
- data/README.rdoc +71 -0
- data/Rakefile +79 -0
- data/bin/cassandra_helper +16 -0
- data/cassandra.gemspec +45 -0
- data/conf/cassandra.in.sh +47 -0
- data/conf/log4j.properties +38 -0
- data/conf/storage-conf.xml +340 -0
- data/lib/cassandra.rb +22 -0
- data/lib/cassandra/array.rb +8 -0
- data/lib/cassandra/cassandra.rb +314 -0
- data/lib/cassandra/columns.rb +106 -0
- data/lib/cassandra/comparable.rb +28 -0
- data/lib/cassandra/constants.rb +11 -0
- data/lib/cassandra/debug.rb +7 -0
- data/lib/cassandra/helpers.rb +40 -0
- data/lib/cassandra/long.rb +58 -0
- data/lib/cassandra/mock.rb +322 -0
- data/lib/cassandra/ordered_hash.rb +141 -0
- data/lib/cassandra/protocol.rb +92 -0
- data/lib/cassandra/time.rb +11 -0
- data/test/cassandra_client_test.rb +20 -0
- data/test/cassandra_mock_test.rb +73 -0
- data/test/cassandra_test.rb +370 -0
- data/test/comparable_types_test.rb +45 -0
- data/test/eventmachine_test.rb +42 -0
- data/test/ordered_hash_test.rb +201 -0
- data/test/test_helper.rb +14 -0
- data/vendor/gen-rb/cassandra.rb +1477 -0
- data/vendor/gen-rb/cassandra_constants.rb +12 -0
- data/vendor/gen-rb/cassandra_types.rb +479 -0
- metadata +173 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
class Cassandra
|
3
|
+
# Abstract base class for comparable numeric column name types
|
4
|
+
class Comparable
|
5
|
+
class TypeError < ::TypeError #:nodoc:
|
6
|
+
end
|
7
|
+
|
8
|
+
def <=>(other)
|
9
|
+
self.to_i <=> other.to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def hash
|
13
|
+
@bytes.hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def eql?(other)
|
17
|
+
other.is_a?(Comparable) and @bytes == other.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
other.respond_to?(:to_i) && self.to_i == other.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
@bytes
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
class Cassandra
|
3
|
+
# A helper module you can include in your own class. Makes it easier
|
4
|
+
# to work with Cassandra subclasses.
|
5
|
+
module Constants
|
6
|
+
include Cassandra::Consistency
|
7
|
+
|
8
|
+
Long = Cassandra::Long
|
9
|
+
OrderedHash = Cassandra::OrderedHash
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Cassandra
|
2
|
+
module Helpers
|
3
|
+
def extract_and_validate_params(column_family, keys, args, options)
|
4
|
+
options = options.dup
|
5
|
+
column_family = column_family.to_s
|
6
|
+
# Keys
|
7
|
+
[keys].flatten.each do |key|
|
8
|
+
raise ArgumentError, "Key #{key.inspect} must be a String for #{caller[2].inspect}." unless key.is_a?(String)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Options
|
12
|
+
if args.last.is_a?(Hash)
|
13
|
+
extras = args.last.keys - options.keys
|
14
|
+
raise ArgumentError, "Invalid options #{extras.inspect[1..-2]} for #{caller[1]}" if extras.any?
|
15
|
+
options.merge!(args.pop)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Ranges
|
19
|
+
column, sub_column = args[0], args[1]
|
20
|
+
klass, sub_klass = column_name_class(column_family), sub_column_name_class(column_family)
|
21
|
+
range_class = column ? sub_klass : klass
|
22
|
+
|
23
|
+
[:start, :finish].each do |opt|
|
24
|
+
options[opt] = options[opt] ? range_class.new(options[opt]).to_s : ''
|
25
|
+
end
|
26
|
+
|
27
|
+
[column_family, s_map(column, klass), s_map(sub_column, sub_klass), options]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert stuff to strings.
|
31
|
+
def s_map(el, klass)
|
32
|
+
case el
|
33
|
+
when Array then el.map { |i| s_map(i, klass) }
|
34
|
+
when NilClass then nil
|
35
|
+
else
|
36
|
+
klass.new(el).to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
class Cassandra
|
3
|
+
# A temporally-ordered Long class for use in Cassandra column names
|
4
|
+
class Long < Comparable
|
5
|
+
|
6
|
+
# FIXME Should unify with or subclass Cassandra::UUID
|
7
|
+
def initialize(bytes = nil)
|
8
|
+
case bytes
|
9
|
+
when self.class # Long
|
10
|
+
@bytes = bytes.to_s
|
11
|
+
when String
|
12
|
+
case bytes.size
|
13
|
+
when 8 # Raw byte array
|
14
|
+
@bytes = bytes
|
15
|
+
when 18 # Human-readable UUID-like representation; inverse of #to_guid
|
16
|
+
elements = bytes.split("-")
|
17
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (malformed UUID-like representation)" if elements.size != 3
|
18
|
+
@bytes = [elements.join].pack('H32')
|
19
|
+
else
|
20
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (invalid bytecount)"
|
21
|
+
end
|
22
|
+
when Integer
|
23
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (integer out of range)" if bytes < 0 or bytes > 2**64
|
24
|
+
@bytes = [bytes >> 32, bytes % 2**32].pack("NN")
|
25
|
+
when NilClass, Time
|
26
|
+
# Time.stamp is 52 bytes, so we have 12 bytes of entropy left over
|
27
|
+
int = ((bytes || Time).stamp << 12) + rand(2**12)
|
28
|
+
@bytes = [int >> 32, int % 2**32].pack("NN")
|
29
|
+
else
|
30
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (unknown source class)"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_i
|
35
|
+
@to_i ||= begin
|
36
|
+
ints = @bytes.unpack("NN")
|
37
|
+
(ints[0] << 32) +
|
38
|
+
ints[1]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_guid
|
43
|
+
"%08x-%04x-%04x" % @bytes.unpack("Nnn")
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
"<Cassandra::Long##{object_id} time: #{
|
48
|
+
Time.at((to_i >> 12) / 1_000_000).utc.inspect
|
49
|
+
}, usecs: #{
|
50
|
+
(to_i >> 12) % 1_000_000
|
51
|
+
}, jitter: #{
|
52
|
+
to_i % 2**12
|
53
|
+
}, guid: #{
|
54
|
+
to_guid
|
55
|
+
}>"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,322 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class SimpleUUID::UUID
|
4
|
+
def >=(other)
|
5
|
+
(self <=> other) >= 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def <=(other)
|
9
|
+
(self <=> other) <= 0
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Cassandra
|
14
|
+
class Mock
|
15
|
+
include ::Cassandra::Helpers
|
16
|
+
include ::Cassandra::Columns
|
17
|
+
|
18
|
+
def initialize(keyspace, storage_xml)
|
19
|
+
@keyspace = keyspace
|
20
|
+
@column_name_class = {}
|
21
|
+
@sub_column_name_class = {}
|
22
|
+
@storage_xml = storage_xml
|
23
|
+
clear_keyspace!
|
24
|
+
end
|
25
|
+
|
26
|
+
def disconnect!
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear_keyspace!
|
30
|
+
@data = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear_column_family!(column_family)
|
34
|
+
@data[column_family.to_sym] = OrderedHash.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def insert(column_family, key, hash_or_array, options = {})
|
38
|
+
if @batch
|
39
|
+
@batch << [:insert, column_family, key, hash_or_array, options]
|
40
|
+
else
|
41
|
+
raise ArgumentError if key.nil?
|
42
|
+
if column_family_type(column_family) == 'Standard'
|
43
|
+
insert_standard(column_family, key, hash_or_array)
|
44
|
+
else
|
45
|
+
insert_super(column_family, key, hash_or_array)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def insert_standard(column_family, key, hash_or_array)
|
51
|
+
old = cf(column_family)[key] || OrderedHash.new
|
52
|
+
cf(column_family)[key] = merge_and_sort(old, hash_or_array)
|
53
|
+
end
|
54
|
+
|
55
|
+
def insert_super(column_family, key, hash)
|
56
|
+
raise ArgumentError unless hash.is_a?(Hash)
|
57
|
+
cf(column_family)[key] ||= OrderedHash.new
|
58
|
+
|
59
|
+
hash.keys.each do |sub_key|
|
60
|
+
old = cf(column_family)[key][sub_key] || OrderedHash.new
|
61
|
+
cf(column_family)[key][sub_key] = merge_and_sort(old, hash[sub_key])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def batch
|
66
|
+
@batch = []
|
67
|
+
yield
|
68
|
+
b = @batch
|
69
|
+
@batch = nil
|
70
|
+
b.each do |mutation|
|
71
|
+
send(*mutation)
|
72
|
+
end
|
73
|
+
ensure
|
74
|
+
@batch = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def get(column_family, key, *columns_and_options)
|
78
|
+
column_family, column, sub_column, options =
|
79
|
+
extract_and_validate_params_for_real(column_family, [key], columns_and_options, READ_DEFAULTS)
|
80
|
+
if column_family_type(column_family) == 'Standard'
|
81
|
+
get_standard(column_family, key, column, options)
|
82
|
+
else
|
83
|
+
get_super(column_family, key, column, sub_column, options)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_standard(column_family, key, column, options)
|
88
|
+
row = cf(column_family)[key] || OrderedHash.new
|
89
|
+
if column
|
90
|
+
row[column]
|
91
|
+
else
|
92
|
+
apply_count(row, options[:count], options[:reversed])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_super(column_family, key, column, sub_column, options)
|
97
|
+
if column
|
98
|
+
if sub_column
|
99
|
+
if cf(column_family)[key] &&
|
100
|
+
cf(column_family)[key][column] &&
|
101
|
+
cf(column_family)[key][column][sub_column]
|
102
|
+
cf(column_family)[key][column][sub_column]
|
103
|
+
else
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
else
|
107
|
+
row = cf(column_family)[key] && cf(column_family)[key][column] ?
|
108
|
+
cf(column_family)[key][column] :
|
109
|
+
OrderedHash.new
|
110
|
+
if options[:start] || options[:finish]
|
111
|
+
start = to_compare_with_type(options[:start], column_family, false)
|
112
|
+
finish = to_compare_with_type(options[:finish], column_family, false)
|
113
|
+
ret = OrderedHash.new
|
114
|
+
row.keys.each do |key|
|
115
|
+
if (start.nil? || key >= start) && (finish.nil? || key <= finish)
|
116
|
+
ret[key] = row[key]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
row = ret
|
120
|
+
end
|
121
|
+
apply_count(row, options[:count], options[:reversed])
|
122
|
+
end
|
123
|
+
elsif cf(column_family)[key]
|
124
|
+
cf(column_family)[key]
|
125
|
+
else
|
126
|
+
OrderedHash.new
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def exists?(column_family, key, column=nil)
|
131
|
+
!!get(column_family, key, column)
|
132
|
+
end
|
133
|
+
|
134
|
+
def multi_get(column_family, keys, *columns_and_options)
|
135
|
+
column_family, column, sub_column, options = extract_and_validate_params_for_real(column_family, keys, columns_and_options, READ_DEFAULTS)
|
136
|
+
keys.inject(OrderedHash.new) do |hash, key|
|
137
|
+
hash[key] = get(column_family, key)
|
138
|
+
hash
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def remove(column_family, key, *columns_and_options)
|
143
|
+
column_family, column, sub_column, options = extract_and_validate_params_for_real(column_family, key, columns_and_options, WRITE_DEFAULTS)
|
144
|
+
if @batch
|
145
|
+
@batch << [:remove, column_family, key, column]
|
146
|
+
else
|
147
|
+
if column
|
148
|
+
if sub_column
|
149
|
+
cf(column_family)[key][column].delete(sub_column)
|
150
|
+
else
|
151
|
+
cf(column_family)[key].delete(column)
|
152
|
+
end
|
153
|
+
else
|
154
|
+
cf(column_family).delete(key)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_columns(column_family, key, *columns_and_options)
|
160
|
+
column_family, columns, sub_columns, options = extract_and_validate_params_for_real(column_family, key, columns_and_options, READ_DEFAULTS)
|
161
|
+
d = get(column_family, key)
|
162
|
+
|
163
|
+
|
164
|
+
if sub_columns
|
165
|
+
sub_columns.collect do |sub_column|
|
166
|
+
d[columns][sub_column]
|
167
|
+
end
|
168
|
+
else
|
169
|
+
columns.collect do |column|
|
170
|
+
d[column]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def count_columns(column_family, key, column=nil)
|
176
|
+
get(column_family, key, column).keys.length
|
177
|
+
end
|
178
|
+
|
179
|
+
def multi_get_columns(column_family, keys, columns)
|
180
|
+
keys.inject(OrderedHash.new) do |hash, key|
|
181
|
+
hash[key] = get_columns(column_family, key, columns)
|
182
|
+
hash
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def multi_count_columns(column_family, keys)
|
187
|
+
keys.inject(OrderedHash.new) do |hash, key|
|
188
|
+
hash[key] = count_columns(column_family, key)
|
189
|
+
hash
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def get_range(column_family, options = {})
|
194
|
+
column_family, _, _, options = extract_and_validate_params_for_real(column_family, "", [options], READ_DEFAULTS)
|
195
|
+
_get_range(column_family, options[:start], options[:finish], options[:count]).keys
|
196
|
+
end
|
197
|
+
|
198
|
+
def count_range(column_family, options={})
|
199
|
+
count = 0
|
200
|
+
l = []
|
201
|
+
start_key = ''
|
202
|
+
while (l = get_range(column_family, options.merge(:count => 1000, :start => start_key))).size > 0
|
203
|
+
count += l.size
|
204
|
+
start_key = l.last.succ
|
205
|
+
end
|
206
|
+
count
|
207
|
+
end
|
208
|
+
|
209
|
+
def schema(load=true)
|
210
|
+
if !load && !@schema
|
211
|
+
[]
|
212
|
+
else
|
213
|
+
@schema ||= schema_for_keyspace(@keyspace)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
def _get_range(column_family, start, finish, count)
|
220
|
+
ret = OrderedHash.new
|
221
|
+
start = to_compare_with_type(start, column_family)
|
222
|
+
finish = to_compare_with_type(finish, column_family)
|
223
|
+
cf(column_family).keys.sort.each do |key|
|
224
|
+
break if ret.keys.size >= count
|
225
|
+
if (start.nil? || key >= start) && (finish.nil? || key <= finish)
|
226
|
+
ret[key] = cf(column_family)[key]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
ret
|
230
|
+
end
|
231
|
+
|
232
|
+
def schema_for_keyspace(keyspace)
|
233
|
+
doc = read_storage_xml
|
234
|
+
ret = {}
|
235
|
+
doc.css("Keyspaces Keyspace[@Name='#{keyspace}']").css('ColumnFamily').each do |cf|
|
236
|
+
ret[cf['Name']] = {}
|
237
|
+
if cf['CompareSubcolumnsWith']
|
238
|
+
ret[cf['Name']]['CompareSubcolumnsWith'] = 'org.apache.cassandra.db.marshal.' + cf['CompareSubcolumnsWith']
|
239
|
+
end
|
240
|
+
if cf['CompareWith']
|
241
|
+
ret[cf['Name']]['CompareWith'] = 'org.apache.cassandra.db.marshal.' + cf['CompareWith']
|
242
|
+
end
|
243
|
+
if cf['ColumnType'] == 'Super'
|
244
|
+
ret[cf['Name']]['Type'] = 'Super'
|
245
|
+
else
|
246
|
+
ret[cf['Name']]['Type'] = 'Standard'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
ret
|
250
|
+
end
|
251
|
+
|
252
|
+
def read_storage_xml
|
253
|
+
@doc ||= Nokogiri::XML(open(@storage_xml))
|
254
|
+
end
|
255
|
+
|
256
|
+
def extract_and_validate_params_for_real(column_family, keys, args, options)
|
257
|
+
column_family, columns, sub_column, options = extract_and_validate_params(column_family, keys, args, options)
|
258
|
+
options[:start] = nil if options[:start] == ''
|
259
|
+
options[:finish] = nil if options[:finish] == ''
|
260
|
+
[column_family, to_compare_with_types(columns, column_family), to_compare_with_types(sub_column, column_family, false), options]
|
261
|
+
end
|
262
|
+
|
263
|
+
def to_compare_with_types(column_names, column_family, standard=true)
|
264
|
+
if column_names.is_a?(Array)
|
265
|
+
column_names.collect do |name|
|
266
|
+
to_compare_with_type(name, column_family, standard)
|
267
|
+
end
|
268
|
+
else
|
269
|
+
to_compare_with_type(column_names, column_family, standard)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def to_compare_with_type(column_name, column_family, standard=true)
|
274
|
+
return column_name if column_name.nil?
|
275
|
+
klass = if standard
|
276
|
+
schema[column_family.to_s]["CompareWith"]
|
277
|
+
else
|
278
|
+
schema[column_family.to_s]["CompareSubcolumnsWith"]
|
279
|
+
end
|
280
|
+
|
281
|
+
case klass
|
282
|
+
when "org.apache.cassandra.db.marshal.UTF8Type", "org.apache.cassandra.db.marshal.BytesType"
|
283
|
+
column_name
|
284
|
+
when "org.apache.cassandra.db.marshal.TimeUUIDType"
|
285
|
+
SimpleUUID::UUID.new(column_name)
|
286
|
+
when "org.apache.cassandra.db.marshal.LongType"
|
287
|
+
Long.new(column_name)
|
288
|
+
else
|
289
|
+
raise "Unknown column family type: #{klass.inspect}"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def column_family_type(column_family)
|
294
|
+
schema[column_family.to_s]['Type']
|
295
|
+
end
|
296
|
+
|
297
|
+
def cf(column_family)
|
298
|
+
@data[column_family.to_sym] ||= OrderedHash.new
|
299
|
+
end
|
300
|
+
|
301
|
+
def merge_and_sort(old_stuff, new_stuff)
|
302
|
+
if new_stuff.is_a?(Array)
|
303
|
+
new_stuff = new_stuff.inject({}){|h,k| h[k] = nil; h }
|
304
|
+
end
|
305
|
+
OrderedHash[old_stuff.merge(new_stuff).sort{|a,b| a[0] <=> b[0]}]
|
306
|
+
end
|
307
|
+
|
308
|
+
def apply_count(row, count, reversed=false)
|
309
|
+
if count
|
310
|
+
keys = row.keys.sort
|
311
|
+
keys = keys.reverse if reversed
|
312
|
+
keys = keys[0...count]
|
313
|
+
keys.inject(OrderedHash.new) do |memo, key|
|
314
|
+
memo[key] = row[key]
|
315
|
+
memo
|
316
|
+
end
|
317
|
+
else
|
318
|
+
row
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|