cassandra 0.2.3 → 0.4

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.
@@ -0,0 +1,8 @@
1
+
2
+ class Array
3
+ def _flatten_once
4
+ result = []
5
+ each { |el| result.concat(Array(el)) }
6
+ result
7
+ end
8
+ end
@@ -0,0 +1,315 @@
1
+
2
+ class Cassandra
3
+ include Helper
4
+ class AccessError < StandardError; end
5
+
6
+ MAX_INT = 2**31 - 1
7
+
8
+ module Consistency
9
+ include CassandraThrift::ConsistencyLevel
10
+ NONE = ZERO
11
+ WEAK = ONE
12
+ STRONG = QUORUM
13
+ PERFECT = ALL
14
+ end
15
+
16
+ attr_reader :keyspace, :host, :port, :serializer, :transport, :client, :schema
17
+
18
+ # Instantiate a new Cassandra and open the connection.
19
+ def initialize(keyspace, host = '127.0.0.1', port = 9160, serializer = Cassandra::Serialization::JSON)
20
+ @is_super = {}
21
+ @column_name_class = {}
22
+ @sub_column_name_class = {}
23
+
24
+ @keyspace = keyspace
25
+ @host = host
26
+ @port = port
27
+ @serializer = serializer
28
+
29
+ @transport = Thrift::BufferedTransport.new(Thrift::Socket.new(@host, @port))
30
+ @transport.open
31
+ @client = CassandraThrift::Cassandra::SafeClient.new(
32
+ CassandraThrift::Cassandra::Client.new(Thrift::BinaryProtocol.new(@transport)),
33
+ @transport)
34
+
35
+ keyspaces = @client.get_string_list_property("tables")
36
+ unless keyspaces.include?(@keyspace)
37
+ raise AccessError, "Keyspace #{@keyspace.inspect} not found. Available: #{keyspaces.inspect}"
38
+ end
39
+
40
+ @schema = @client.describe_table(@keyspace)
41
+ end
42
+
43
+ def inspect
44
+ "#<Cassandra:#{object_id}, @keyspace=#{keyspace.inspect}, @schema={#{
45
+ schema.map {|name, hash| ":#{name} => #{hash['type'].inspect}"}.join(', ')
46
+ }}, @host=#{host.inspect}, @port=#{port}, @serializer=#{serializer.name}>"
47
+ end
48
+
49
+ ## Write
50
+
51
+ # Insert a row for a key. Pass a flat hash for a regular column family, and
52
+ # a nested hash for a super column family.
53
+ def insert(column_family, key, hash, consistency = Consistency::WEAK, timestamp = Time.stamp)
54
+ column_family = column_family.to_s
55
+ mutation = if is_super(column_family)
56
+ CassandraThrift::BatchMutationSuper.new(:key => key, :cfmap => {column_family.to_s => hash_to_super_columns(column_family, hash, timestamp)})
57
+ else
58
+ CassandraThrift::BatchMutation.new(:key => key, :cfmap => {column_family.to_s => hash_to_columns(column_family, hash, timestamp)})
59
+ end
60
+ # FIXME Batched operations discard the consistency argument
61
+ @batch ? @batch << mutation : _insert(mutation, consistency)
62
+ end
63
+
64
+ private
65
+
66
+ def _insert(mutation, consistency = Consistency::WEAK)
67
+ case mutation
68
+ when CassandraThrift::BatchMutationSuper then @client.batch_insert_super_column(@keyspace, mutation, consistency)
69
+ when CassandraThrift::BatchMutation then @client.batch_insert(@keyspace, mutation, consistency)
70
+ end
71
+ end
72
+
73
+ public
74
+
75
+ ## Delete
76
+
77
+ # Remove the element at the column_family:key:[column]:[sub_column]
78
+ # path you request.
79
+ def remove(column_family, key, column = nil, sub_column = nil, consistency = Consistency::WEAK, timestamp = Time.stamp)
80
+ column_family = column_family.to_s
81
+ assert_column_name_classes(column_family, column, sub_column)
82
+
83
+ column = column.to_s if column
84
+ sub_column = sub_column.to_s if sub_column
85
+ args = [column_family, key, column, sub_column, consistency, timestamp]
86
+ @batch ? @batch << args : _remove(*args)
87
+ end
88
+
89
+ private
90
+
91
+ def _remove(column_family, key, column, sub_column, consistency, timestamp)
92
+ column_path_or_parent = if is_super(column_family)
93
+ CassandraThrift::ColumnPathOrParent.new(:column_family => column_family, :super_column => column, :column => sub_column)
94
+ else
95
+ CassandraThrift::ColumnPathOrParent.new(:column_family => column_family, :column => column)
96
+ end
97
+ @client.remove(@keyspace, key, column_path_or_parent, timestamp, consistency)
98
+ end
99
+
100
+ public
101
+
102
+ # Remove all rows in the column family you request.
103
+ def clear_column_family!(column_family)
104
+ # Does not support consistency argument
105
+ get_key_range(column_family).each do |key|
106
+ remove(column_family, key)
107
+ end
108
+ end
109
+
110
+ # Remove all rows in the keyspace
111
+ def clear_keyspace!
112
+ # Does not support consistency argument
113
+ @schema.keys.each do |column_family|
114
+ clear_column_family!(column_family)
115
+ end
116
+ end
117
+
118
+ ## Read
119
+
120
+ # Count the elements at the column_family:key:[super_column] path you
121
+ # request.
122
+ def count_columns(column_family, key, super_column = nil, consistency = Consistency::WEAK)
123
+ column_family = column_family.to_s
124
+ assert_column_name_classes(column_family, super_column)
125
+
126
+ super_column = super_column.to_s if super_column
127
+ @client.get_column_count(@keyspace, key,
128
+ CassandraThrift::ColumnParent.new(:column_family => column_family, :super_column => super_column),
129
+ consistency
130
+ )
131
+ end
132
+
133
+ # Multi-key version of Cassandra#count_columns.
134
+ def multi_count_columns(column_family, keys, super_column = nil, consistency = Consistency::WEAK)
135
+ OrderedHash[*keys.map do |key|
136
+ [key, count_columns(column_family, key, super_column)]
137
+ end._flatten_once]
138
+ end
139
+
140
+ # Return a list of single values for the elements at the
141
+ # column_family:key:column[s]:[sub_columns] path you request.
142
+ def get_columns(column_family, key, columns, sub_columns = nil, consistency = Consistency::WEAK)
143
+ column_family = column_family.to_s
144
+ assert_column_name_classes(column_family, columns, sub_columns)
145
+
146
+ result = if is_super(column_family)
147
+ if sub_columns
148
+ columns_to_hash(column_family, @client.get_slice_by_names(@keyspace, key,
149
+ CassandraThrift::ColumnParent.new(:column_family => column_family, :super_column => columns),
150
+ sub_columns, consistency))
151
+ else
152
+ columns_to_hash(column_family, @client.get_slice_super_by_names(@keyspace, key, column_family, columns, consistency))
153
+ end
154
+ else
155
+ columns_to_hash(column_family, @client.get_slice_by_names(@keyspace, key,
156
+ CassandraThrift::ColumnParent.new(:column_family => column_family), columns, consistency))
157
+ end
158
+ sub_columns || columns.map { |name| result[name] }
159
+ end
160
+
161
+ # Multi-key version of Cassandra#get_columns.
162
+ def multi_get_columns(column_family, keys, columns, sub_columns = nil, consistency = Consistency::WEAK)
163
+ OrderedHash[*keys.map do |key|
164
+ [key, get_columns(column_family, key, columns, sub_columns, consistency)]
165
+ end._flatten_once]
166
+ end
167
+
168
+ # Return a hash (actually, a Cassandra::OrderedHash) or a single value
169
+ # representing the element at the column_family:key:[column]:[sub_column]
170
+ # path you request.
171
+ def get(column_family, key, column = nil, sub_column = nil, limit = 100, column_range = ''..'', reversed = false, consistency = Consistency::WEAK)
172
+ column_family = column_family.to_s
173
+ assert_column_name_classes(column_family, column, sub_column)
174
+ _get(column_family, key, column, sub_column, limit = 100, column_range, reversed, consistency)
175
+ rescue CassandraThrift::NotFoundException
176
+ is_super(column_family) && !sub_column ? OrderedHash.new : nil
177
+ end
178
+
179
+ # Multi-key version of Cassandra#get.
180
+ def multi_get(column_family, keys, column = nil, sub_column = nil, limit = 100, column_range = ''..'', reversed = false, consistency = Consistency::WEAK)
181
+ OrderedHash[*keys.map do |key|
182
+ [key, get(column_family, key, column, sub_column, limit, column_range, reversed, consistency)]
183
+ end._flatten_once]
184
+ end
185
+
186
+ # Return true if the column_family:key:[column]:[sub_column] path you
187
+ # request exists.
188
+ def exists?(column_family, key, column = nil, sub_column = nil, consistency = Consistency::WEAK)
189
+ column_family = column_family.to_s
190
+ assert_column_name_classes(column_family, column, sub_column)
191
+ _get(column_family, key, column, sub_column, 1, ''..'', false, consistency)
192
+ true
193
+ rescue CassandraThrift::NotFoundException
194
+ end
195
+
196
+ private
197
+
198
+ def _get(column_family, key, column = nil, sub_column = nil, limit = 100, column_range = ''..'', reversed = false, consistency = Consistency::WEAK)
199
+ column = column.to_s if column
200
+ sub_column = sub_column.to_s if sub_column
201
+ # FIXME Comparable types in a are not checked
202
+ column_range = (column_range.begin.to_s)..(column_range.end.to_s)
203
+
204
+ # You have got to be kidding
205
+ if is_super(column_family)
206
+ if sub_column
207
+ # Limit and column_range parameters have no effect
208
+ load(@client.get_column(@keyspace, key,
209
+ CassandraThrift::ColumnPath.new(:column_family => column_family, :super_column => column, :column => sub_column),
210
+ consistency).value)
211
+ elsif column
212
+ sub_columns_to_hash(column_family,
213
+ @client.get_slice(@keyspace, key,
214
+ CassandraThrift::ColumnParent.new(:column_family => column_family, :super_column => column),
215
+ column_range.begin, column_range.end, !reversed, limit, consistency))
216
+ else
217
+ columns_to_hash(column_family,
218
+ @client.get_slice_super(@keyspace, key, column_family, column_range.begin, column_range.end, !reversed, limit, consistency))
219
+ end
220
+ else
221
+ if column
222
+ # Limit and column_range parameters have no effect
223
+ load(@client.get_column(@keyspace, key,
224
+ CassandraThrift::ColumnPath.new(:column_family => column_family, :column => column),
225
+ consistency).value)
226
+ else
227
+ columns_to_hash(column_family,
228
+ @client.get_slice(@keyspace, key,
229
+ CassandraThrift::ColumnParent.new(:column_family => column_family),
230
+ column_range.begin, column_range.end, !reversed, limit, consistency))
231
+ end
232
+ end
233
+ end
234
+
235
+ public
236
+
237
+ # Return a list of keys in the column_family you request. Requires the
238
+ # table to be partitioned with OrderPreservingHash.
239
+ def get_key_range(column_family, key_range = ''..'', limit = 100, consistency = Consistency::WEAK)
240
+ column_family = column_family.to_s
241
+ @client.get_key_range(@keyspace, column_family, key_range.begin, key_range.end, limit)
242
+ end
243
+
244
+ # Count all rows in the column_family you request. Requires the table
245
+ # to be partitioned with OrderPreservingHash.
246
+ def count(column_family, key_range = ''..'', limit = MAX_INT, consistency = Consistency::WEAK)
247
+ get_key_range(column_family, key_range, limit, consistency).size
248
+ end
249
+
250
+ def batch
251
+ @batch = []
252
+ yield
253
+ compact_mutations!
254
+ dispatch_mutations!
255
+ @batch = nil
256
+ end
257
+
258
+ private
259
+
260
+ def compact_mutations!
261
+ compact_batch = []
262
+ mutations = {}
263
+
264
+ @batch << nil # Close it
265
+ @batch.each do |m|
266
+ case m
267
+ when Array, nil
268
+ # Flush compacted mutations
269
+ compact_batch.concat(mutations.values.map {|x| x.values}.flatten)
270
+ mutations = {}
271
+ # Insert delete operation
272
+ compact_batch << m
273
+ else # BatchMutation, BatchMutationSuper
274
+ # Do a nested hash merge
275
+ if mutation_class = mutations[m.class]
276
+ if mutation = mutation_class[m.key]
277
+ if columns = mutation.cfmap[m.cfmap.keys.first]
278
+ columns.concat(m.cfmap.values.first)
279
+ else
280
+ mutation.cfmap.merge!(m.cfmap)
281
+ end
282
+ else
283
+ mutation_class[m.key] = m
284
+ end
285
+ else
286
+ mutations[m.class] = {m.key => m}
287
+ end
288
+ end
289
+ end
290
+
291
+ @batch = compact_batch
292
+ end
293
+
294
+ def dispatch_mutations!
295
+ @batch.each do |args|
296
+ case args
297
+ when Array
298
+ _remove(*args)
299
+ when CassandraThrift::BatchMutationSuper, CassandraThrift::BatchMutation
300
+ _insert(*args)
301
+ end
302
+ end
303
+ end
304
+
305
+ def dump(object)
306
+ # Special-case nil as the empty byte array
307
+ return "" if object == nil
308
+ @serializer.dump(object)
309
+ end
310
+
311
+ def load(object)
312
+ return nil if object == ""
313
+ @serializer.load(object)
314
+ end
315
+ end
@@ -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
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
+ self.to_i == other.to_i
22
+ end
23
+
24
+ def to_s
25
+ @bytes
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,8 @@
1
+
2
+ class Cassandra
3
+ module Constants
4
+ UUID = Cassandra::UUID
5
+ Long = Cassandra::Long
6
+ OrderedHash = Cassandra::OrderedHash
7
+ end
8
+ end
@@ -0,0 +1,85 @@
1
+ class Cassandra
2
+
3
+ # A bunch of crap, mostly related to introspecting on column types
4
+ module Helper
5
+
6
+ private
7
+
8
+ def is_super(column_family)
9
+ @is_super[column_family] ||= column_family_property(column_family, 'Type') == "Super"
10
+ end
11
+
12
+ def column_name_class(column_family)
13
+ @column_name_class[column_family] ||= column_name_class_for_key(column_family, "CompareWith")
14
+ end
15
+
16
+ def sub_column_name_class(column_family)
17
+ @sub_column_name_class[column_family] ||= column_name_class_for_key(column_family, "CompareSubcolumnsWith")
18
+ end
19
+
20
+ def column_name_class_for_key(column_family, comparator_key)
21
+ property = column_family_property(column_family, comparator_key)
22
+ property =~ /.*\.(.*?)Type/
23
+ self.class.const_get($1) # Long, UUID
24
+ rescue NameError
25
+ String # UTF8, Ascii, Bytes, anything else
26
+ rescue TypeError
27
+ nil # Called sub_column_name_class on a standard column family
28
+ end
29
+
30
+ def column_family_property(column_family, key)
31
+ @schema[column_family][key]
32
+ rescue NoMethodError
33
+ raise AccessError, "Invalid column family \"#{column_family}\""
34
+ end
35
+
36
+ def assert_column_name_classes(column_family, columns, sub_columns = nil)
37
+ {Array(columns) => column_name_class(column_family),
38
+ Array(sub_columns) => sub_column_name_class(column_family)}.each do |columns, klass|
39
+ columns.each do |column|
40
+ raise Comparable::TypeError, "Expected #{column.inspect} to be a #{klass}" if !column.is_a?(klass)
41
+ end
42
+ end
43
+ end
44
+
45
+ def columns_to_hash(column_family, columns)
46
+ columns_to_hash_for_classes(columns, column_name_class(column_family), sub_column_name_class(column_family))
47
+ end
48
+
49
+ def sub_columns_to_hash(column_family, columns)
50
+ columns_to_hash_for_classes(columns, sub_column_name_class(column_family))
51
+ end
52
+
53
+ def columns_to_hash_for_classes(columns, column_name_class, sub_column_name_class = nil)
54
+ hash = OrderedHash.new
55
+ Array(columns).each do |c|
56
+ hash[column_name_class.new(c.name)] = if c.is_a?(CassandraThrift::SuperColumn)
57
+ # Pop the class stack, and recurse
58
+ columns_to_hash_for_classes(c.columns, sub_column_name_class)
59
+ else
60
+ load(c.value)
61
+ end
62
+ end
63
+ hash
64
+ end
65
+
66
+ def hash_to_columns(column_family, hash, timestamp)
67
+ assert_column_name_classes(column_family, hash.keys)
68
+ hash_to_columns_without_assertion(column_family, hash, timestamp)
69
+ end
70
+
71
+ def hash_to_columns_without_assertion(column_family, hash, timestamp)
72
+ hash.map do |column, value|
73
+ CassandraThrift::Column.new(:name => column.to_s, :value => dump(value), :timestamp => timestamp)
74
+ end
75
+ end
76
+
77
+ def hash_to_super_columns(column_family, hash, timestamp)
78
+ assert_column_name_classes(column_family, hash.keys)
79
+ hash.map do |column, sub_hash|
80
+ assert_column_name_classes(column_family, nil, sub_hash.keys)
81
+ CassandraThrift::SuperColumn.new(:name => column.to_s, :columns => hash_to_columns_without_assertion(column_family, sub_hash, timestamp))
82
+ end
83
+ end
84
+ end
85
+ end