cassandra 0.9.0 → 0.9.1

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 CHANGED
@@ -1,3 +1,7 @@
1
+ v0.9.1 Support for secondary indexing. (jhermes)
2
+ Fix bug in mock where we didn't support range queries. (therealadam)
3
+ Support deletes in batch mutations. [blanquer]
4
+
1
5
  v0.9.0 cassandra 0.7 compat
2
6
 
3
7
  v0.8.2 Renamed :thrift_client_class option, UUID fix, 0.6 update, fixing cassanda install, much Mock fixes
data/README.rdoc CHANGED
@@ -54,6 +54,10 @@ Insert into a column family. You can insert a `Cassandra::OrderedHash`, or a reg
54
54
 
55
55
  client.insert(:Users, "5", {'screen_name' => "buttonscat"})
56
56
 
57
+ The 0.7 API insert() includes support for TTL on columns. The following example inserts into a comlumn family with a time to live of 30 seconds.
58
+
59
+ client.insert(:Users, "5", {'screen_name' => "buttonscat"}, {:ttl=>30})
60
+
57
61
  Insert into a super column family:
58
62
 
59
63
  client.insert(:UserRelationships, "5", {"user_timeline" => {UUID.new => "1"}})
@@ -64,6 +68,17 @@ Query a super column:
64
68
 
65
69
  The returned result will always be a Cassandra::OrderedHash.
66
70
 
71
+ Create and delete a 2ary index:
72
+
73
+ client.create_index("Twitter", "Users", "revenue_generating_units", "LongType")
74
+ client.delete_index("Twitter", "Users", "revenue_generating_units"
75
+
76
+ Create an index clause and query an indexed column family:
77
+
78
+ expr = client.create_idx_expr("revenue_generating_units", 100, ">")
79
+ clause = client.create_idx_clause([expr])
80
+ client.get_indexed_slices(:Users, clause)
81
+
67
82
  See Cassandra for more methods.
68
83
 
69
84
  == Configuration
data/Rakefile CHANGED
@@ -10,14 +10,12 @@ unless ENV['FROM_BIN_CASSANDRA_HELPER']
10
10
  p.dependencies = ['thrift_client >=0.6.0', 'json', 'rake', 'simple_uuid >=0.1.0']
11
11
  p.ignore_pattern = /^(data|vendor\/cassandra|cassandra|vendor\/thrift)/
12
12
  p.rdoc_pattern = /^(lib|bin|tasks|ext)|^README|^CHANGELOG|^TODO|^LICENSE|^COPYING$/
13
- p.url = "http://blog.evanweaver.com/files/doc/fauna/cassandra/"
14
- p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
15
13
  end
16
14
  end
17
15
 
18
16
  CASSANDRA_HOME = ENV['CASSANDRA_HOME'] || "#{ENV['HOME']}/cassandra"
19
17
  DOWNLOAD_DIR = "/tmp"
20
- DIST_URL = "http://www.takeyellow.com/apachemirror//cassandra/0.6.8/apache-cassandra-0.6.8-bin.tar.gz"
18
+ DIST_URL = "http://www.fightrice.com/mirrors/apache/cassandra/0.6.12/apache-cassandra-0.6.12-bin.tar.gz"
21
19
  DIST_FILE = DIST_URL.split('/').last
22
20
 
23
21
  directory CASSANDRA_HOME
data/cassandra.gemspec CHANGED
@@ -2,18 +2,18 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{cassandra}
5
- s.version = "0.9.0"
5
+ s.version = "0.9.1"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0.8") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Evan Weaver, Ryan King"]
9
- s.date = %q{2010-12-08}
9
+ s.date = %q{2011-04-06}
10
10
  s.default_executable = %q{cassandra_helper}
11
11
  s.description = %q{A Ruby client for the Cassandra distributed database.}
12
12
  s.email = %q{}
13
13
  s.executables = ["cassandra_helper"]
14
14
  s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README.rdoc", "bin/cassandra_helper", "lib/cassandra.rb", "lib/cassandra/0.6.rb", "lib/cassandra/0.6/cassandra.rb", "lib/cassandra/0.6/columns.rb", "lib/cassandra/0.6/protocol.rb", "lib/cassandra/0.7.rb", "lib/cassandra/0.7/cassandra.rb", "lib/cassandra/0.7/column_family.rb", "lib/cassandra/0.7/columns.rb", "lib/cassandra/0.7/keyspace.rb", "lib/cassandra/0.7/protocol.rb", "lib/cassandra/array.rb", "lib/cassandra/cassandra.rb", "lib/cassandra/columns.rb", "lib/cassandra/comparable.rb", "lib/cassandra/constants.rb", "lib/cassandra/debug.rb", "lib/cassandra/helpers.rb", "lib/cassandra/long.rb", "lib/cassandra/mock.rb", "lib/cassandra/ordered_hash.rb", "lib/cassandra/time.rb"]
15
15
  s.files = ["CHANGELOG", "LICENSE", "Manifest", "README.rdoc", "Rakefile", "bin/cassandra_helper", "conf/cassandra.in.sh", "conf/cassandra.yaml", "conf/log4j.properties", "conf/storage-conf.xml", "lib/cassandra.rb", "lib/cassandra/0.6.rb", "lib/cassandra/0.6/cassandra.rb", "lib/cassandra/0.6/columns.rb", "lib/cassandra/0.6/protocol.rb", "lib/cassandra/0.7.rb", "lib/cassandra/0.7/cassandra.rb", "lib/cassandra/0.7/column_family.rb", "lib/cassandra/0.7/columns.rb", "lib/cassandra/0.7/keyspace.rb", "lib/cassandra/0.7/protocol.rb", "lib/cassandra/array.rb", "lib/cassandra/cassandra.rb", "lib/cassandra/columns.rb", "lib/cassandra/comparable.rb", "lib/cassandra/constants.rb", "lib/cassandra/debug.rb", "lib/cassandra/helpers.rb", "lib/cassandra/long.rb", "lib/cassandra/mock.rb", "lib/cassandra/ordered_hash.rb", "lib/cassandra/time.rb", "test/cassandra_client_test.rb", "test/cassandra_mock_test.rb", "test/cassandra_test.rb", "test/comparable_types_test.rb", "test/eventmachine_test.rb", "test/ordered_hash_test.rb", "test/test_helper.rb", "vendor/0.6/gen-rb/cassandra.rb", "vendor/0.6/gen-rb/cassandra_constants.rb", "vendor/0.6/gen-rb/cassandra_types.rb", "vendor/0.7/gen-rb/cassandra.rb", "vendor/0.7/gen-rb/cassandra_constants.rb", "vendor/0.7/gen-rb/cassandra_types.rb", "cassandra.gemspec"]
16
- s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/cassandra/}
16
+ s.homepage = %q{}
17
17
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Cassandra", "--main", "README.rdoc"]
18
18
  s.require_paths = ["lib"]
19
19
  s.rubyforge_project = %q{fauna}
@@ -162,6 +162,7 @@
162
162
  </DataFileDirectories>
163
163
  <CalloutLocation>data/cassandra/callouts</CalloutLocation>
164
164
  <StagingFileDirectory>data/cassandra/staging</StagingFileDirectory>
165
+ <SavedCachesDirectory>data/cassandra/saved_caches</SavedCachesDirectory>
165
166
 
166
167
 
167
168
  <!--
@@ -41,23 +41,21 @@ class Cassandra
41
41
  def reconnect!
42
42
  @servers = all_nodes
43
43
  @client = new_client
44
- check_keyspace
45
- end
46
-
47
- def check_keyspace
48
- unless (keyspaces = client.get_string_list_property("keyspaces")).include?(@keyspace)
49
- raise AccessError, "Keyspace #{@keyspace.inspect} not found. Available: #{keyspaces.inspect}"
50
- end
51
44
  end
52
45
 
53
46
  def all_nodes
54
47
  if @auto_discover_nodes
55
- ips = ::JSON.parse(new_client.get_string_property('token map')).values
56
- port = @servers.first.split(':').last
57
- ips.map{|ip| "#{ip}:#{port}" }
48
+ temp_client = new_client
49
+ begin
50
+ ips = ::JSON.parse(temp_client.get_string_property('token map')).values
51
+ port = @servers.first.split(':').last
52
+ ips.map{|ip| "#{ip}:#{port}" }
53
+ ensure
54
+ temp_client.disconnect!
55
+ end
58
56
  else
59
57
  @servers
60
58
  end
61
59
  end
62
60
 
63
- end
61
+ end
@@ -31,5 +31,31 @@ class Cassandra
31
31
  )
32
32
  )
33
33
  end
34
+
35
+ # General info about a deletion object within a mutation
36
+ # timestamp - required. If this is the only param, it will cause deletion of the whole key at that TS
37
+ # supercolumn - opt. If passed, the deletes will only occur within that supercolumn (only subcolumns
38
+ # will be deleted). Otherwise the normal columns will be deleted.
39
+ # predicate - opt. Defines how to match the columns to delete. if supercolumn passed, the slice will
40
+ # be scoped to subcolumns of that supercolumn.
41
+
42
+ # Deletes a single column from the containing key/CF (and possibly supercolumn), at a given timestamp.
43
+ # Although mutations (as opposed to 'remove' calls) support deleting slices and lists of columns in one shot, this is not implemented here.
44
+ # The main reason being that the batch function takes removes, but removes don't have that capability...so we'd need to change the remove
45
+ # methods to use delete mutation calls...although that might have performance implications. We'll leave that refactoring for later.
46
+ def _delete_mutation(cf, column, subcolumn, timestamp, options={})
47
+
48
+ deletion_hash = {:timestamp => timestamp}
49
+ if is_super(cf)
50
+ deletion_hash[:super_column] = column if column
51
+ deletion_hash[:predicate] = CassandraThrift::SlicePredicate.new(:column_names => [subcolumn]) if subcolumn
52
+ else
53
+ deletion_hash[:predicate] = CassandraThrift::SlicePredicate.new(:column_names => [column]) if column
54
+ end
55
+ CassandraThrift::Mutation.new(
56
+ :deletion => CassandraThrift::Deletion.new(deletion_hash)
57
+ )
58
+ end
59
+
34
60
  end
35
61
  end
@@ -1,4 +1,5 @@
1
1
  class Cassandra
2
+
2
3
  def self.DEFAULT_TRANSPORT_WRAPPER
3
4
  Thrift::FramedTransport
4
5
  end
@@ -16,7 +17,7 @@ class Cassandra
16
17
  end
17
18
 
18
19
  def keyspace=(ks)
19
- client.set_keyspace(ks) if check_keyspace(ks)
20
+ client.set_keyspace(ks)
20
21
  @schema = nil; @keyspace = ks
21
22
  end
22
23
 
@@ -99,6 +100,16 @@ class Cassandra
99
100
  res
100
101
  end
101
102
 
103
+ def update_column_family(cf_def)
104
+ begin
105
+ res = client.system_update_column_family(cf_def)
106
+ rescue CassandraThrift::TimedOutException => te
107
+ puts "Timed out: #{te.inspect}"
108
+ end
109
+ @schema = nil
110
+ res
111
+ end
112
+
102
113
  def add_keyspace(ks_def)
103
114
  begin
104
115
  res = client.system_add_keyspace(ks_def)
@@ -137,6 +148,18 @@ class Cassandra
137
148
  res
138
149
  end
139
150
 
151
+ def update_keyspace(ks_def)
152
+ begin
153
+ res = client.system_update_keyspace(ks_def)
154
+ rescue CassandraThrift::TimedOutException => toe
155
+ puts "Timed out: #{toe.inspect}"
156
+ rescue Thrift::TransportException => te
157
+ puts "Timed out: #{te.inspect}"
158
+ end
159
+ @keyspaces = nil
160
+ res
161
+ end
162
+
140
163
  # Open a batch operation and yield self. Inserts and deletes will be queued
141
164
  # until the block closes, and then sent atomically to the server. Supports
142
165
  # the <tt>:consistency</tt> option, which overrides the consistency set in
@@ -145,28 +168,83 @@ class Cassandra
145
168
  _, _, _, options =
146
169
  extract_and_validate_params(schema.cf_defs.first.name, "", [options], WRITE_DEFAULTS)
147
170
 
148
- @batch = []
149
- yield(self)
150
- compact_mutations!
171
+ @batch = []
172
+ yield(self)
173
+ compacted_map,seen_clevels = compact_mutations!
174
+ clevel = if options[:consistency] != nil # Override any clevel from individual mutations if
175
+ options[:consistency]
176
+ elsif seen_clevels.length > 1 # Cannot choose which CLevel to use if there are several ones
177
+ raise "Multiple consistency levels used in the batch, and no override...cannot pick one"
178
+ else # if no consistency override has been provided but all the clevels in the batch are the same: use that one
179
+ seen_clevels.first
180
+ end
151
181
 
152
- @batch.each do |mutation|
153
- case mutation.first
154
- when :remove
155
- _remove(*mutation[1])
156
- else
157
- _mutate(*mutation)
158
- end
159
- end
182
+ _mutate(compacted_map,clevel)
160
183
  ensure
161
184
  @batch = nil
162
185
  end
163
186
 
187
+ ### 2ary Indexing
188
+
189
+ def create_index(ks_name, cf_name, c_name, v_class)
190
+ cf_def = client.describe_keyspace(ks_name).cf_defs.find{|x| x.name == cf_name}
191
+ if !cf_def.nil? and !cf_def.column_metadata.find{|x| x.name == c_name}
192
+ c_def = CassandraThrift::ColumnDef.new do |cd|
193
+ cd.name = c_name
194
+ cd.validation_class = "org.apache.cassandra.db.marshal."+v_class
195
+ cd.index_type = CassandraThrift::IndexType::KEYS
196
+ end
197
+ cf_def.column_metadata.push(c_def)
198
+ update_column_family(cf_def)
199
+ end
200
+ end
201
+
202
+ def drop_index(ks_name, cf_name, c_name)
203
+ cf_def = client.describe_keyspace(ks_name).cf_defs.find{|x| x.name == cf_name}
204
+ if !cf_def.nil? and cf_def.column_metadata.find{|x| x.name == c_name}
205
+ cf_def.column_metadata.delete_if{|x| x.name == c_name}
206
+ update_column_family(cf_def)
207
+ end
208
+ end
209
+
210
+ def create_idx_expr(c_name, value, op)
211
+ CassandraThrift::IndexExpression.new(
212
+ :column_name => c_name,
213
+ :value => value,
214
+ :op => (case op
215
+ when nil, "EQ", "eq", "=="
216
+ CassandraThrift::IndexOperator::EQ
217
+ when "GTE", "gte", ">="
218
+ CassandraThrift::IndexOperator::GTE
219
+ when "GT", "gt", ">"
220
+ CassandraThrift::IndexOperator::GT
221
+ when "LTE", "lte", "<="
222
+ CassandraThrift::IndexOperator::LTE
223
+ when "LT", "lt", "<"
224
+ CassandraThrift::IndexOperator::LT
225
+ end ))
226
+ end
227
+
228
+ def create_idx_clause(idx_expressions, start = "")
229
+ CassandraThrift::IndexClause.new(
230
+ :start_key => start,
231
+ :expressions => idx_expressions)
232
+ end
233
+
234
+ # TODO: Supercolumn support.
235
+ def get_indexed_slices(column_family, idx_clause, *columns_and_options)
236
+ column_family, columns, _, options =
237
+ extract_and_validate_params(column_family, [], columns_and_options, READ_DEFAULTS)
238
+ _get_indexed_slices(column_family, idx_clause, columns, options[:count], options[:start],
239
+ options[:finish], options[:reversed], options[:consistency])
240
+ end
241
+
164
242
  protected
165
243
 
166
244
  def client
167
245
  if @client.nil? || @client.current_server.nil?
168
246
  reconnect!
169
- @client.set_keyspace(@keyspace) if check_keyspace
247
+ @client.set_keyspace(@keyspace)
170
248
  end
171
249
  @client
172
250
  end
@@ -176,17 +254,16 @@ class Cassandra
176
254
  @client = new_client
177
255
  end
178
256
 
179
- def check_keyspace(ks = @keyspace)
180
- !(unless (_keyspaces = keyspaces()).include?(ks)
181
- raise AccessError, "Keyspace #{ks.inspect} not found. Available: #{_keyspaces.inspect}"
182
- end)
183
- end
184
-
185
257
  def all_nodes
186
258
  if @auto_discover_nodes && !@keyspace.eql?("system")
187
- ips = (new_client.describe_ring(@keyspace).map {|range| range.endpoints}).flatten.uniq
188
- port = @servers.first.split(':').last
189
- ips.map{|ip| "#{ip}:#{port}" }
259
+ temp_client = new_client
260
+ begin
261
+ ips = (temp_client.describe_ring(@keyspace).map {|range| range.endpoints}).flatten.uniq
262
+ port = @servers.first.split(':').last
263
+ ips.map{|ip| "#{ip}:#{port}" }
264
+ ensure
265
+ temp_client.disconnect!
266
+ end
190
267
  else
191
268
  @servers
192
269
  end
@@ -55,5 +55,31 @@ class Cassandra
55
55
  )
56
56
  )
57
57
  end
58
+
59
+ # General info about a deletion object within a mutation
60
+ # timestamp - required. If this is the only param, it will cause deletion of the whole key at that TS
61
+ # supercolumn - opt. If passed, the deletes will only occur within that supercolumn (only subcolumns
62
+ # will be deleted). Otherwise the normal columns will be deleted.
63
+ # predicate - opt. Defines how to match the columns to delete. if supercolumn passed, the slice will
64
+ # be scoped to subcolumns of that supercolumn.
65
+
66
+ # Deletes a single column from the containing key/CF (and possibly supercolumn), at a given timestamp.
67
+ # Although mutations (as opposed to 'remove' calls) support deleting slices and lists of columns in one shot, this is not implemented here.
68
+ # The main reason being that the batch function takes removes, but removes don't have that capability...so we'd need to change the remove
69
+ # methods to use delete mutation calls...although that might have performance implications. We'll leave that refactoring for later.
70
+ def _delete_mutation(cf, column, subcolumn, timestamp, options={})
71
+
72
+ deletion_hash = {:timestamp => timestamp}
73
+ if is_super(cf)
74
+ deletion_hash[:super_column] = column if column
75
+ deletion_hash[:predicate] = CassandraThrift::SlicePredicate.new(:column_names => [subcolumn]) if subcolumn
76
+ else
77
+ deletion_hash[:predicate] = CassandraThrift::SlicePredicate.new(:column_names => [column]) if column
78
+ end
79
+ CassandraThrift::Mutation.new(
80
+ :deletion => CassandraThrift::Deletion.new(deletion_hash)
81
+ )
82
+ end
83
+
58
84
  end
59
85
  end
@@ -85,6 +85,22 @@ class Cassandra
85
85
  _get_range(column_family, start, finish, count, consistency).collect{|i| i.key }
86
86
  end
87
87
 
88
+ # TODO: Supercolumn support
89
+ def _get_indexed_slices(column_family, idx_clause, column, count, start, finish, reversed, consistency)
90
+ column_parent = CassandraThrift::ColumnParent.new(:column_family => column_family)
91
+ if column
92
+ predicate = CassandraThrift::SlicePredicate.new(:column_names => [column])
93
+ else
94
+ predicate = CassandraThrift::SlicePredicate.new(:slice_range =>
95
+ CassandraThrift::SliceRange.new(
96
+ :reversed => reversed,
97
+ :count => count,
98
+ :start => start,
99
+ :finish => finish))
100
+ end
101
+ client.get_indexed_slices(column_parent, idx_clause, predicate, consistency)
102
+ end
103
+
88
104
  def each_key(column_family)
89
105
  column_parent = CassandraThrift::ColumnParent.new(:column_family => column_family.to_s)
90
106
  predicate = CassandraThrift::SlicePredicate.new(:column_names => [])
@@ -84,8 +84,10 @@ class Cassandra
84
84
  end
85
85
 
86
86
  def disconnect!
87
- @client.disconnect!
88
- @client = nil
87
+ if @client
88
+ @client.disconnect!
89
+ @client = nil
90
+ end
89
91
  end
90
92
 
91
93
  def keyspaces
@@ -136,16 +138,26 @@ class Cassandra
136
138
  # _mutate the element at the column_family:key:[column]:[sub_column]
137
139
  # path you request. Supports the <tt>:consistency</tt> and <tt>:timestamp</tt>
138
140
  # options.
141
+ # TODO: we could change this function or add another that support multi-column removal (by list or predicate)
139
142
  def remove(column_family, key, *columns_and_options)
140
143
  column_family, column, sub_column, options = extract_and_validate_params(column_family, key, columns_and_options, WRITE_DEFAULTS)
141
144
 
142
- args = {:column_family => column_family}
143
- columns = is_super(column_family) ? {:super_column => column, :column => sub_column} : {:column => column}
144
- column_path = CassandraThrift::ColumnPath.new(args.merge(columns))
145
-
146
- mutation = [:remove, [key, column_path, options[:timestamp] || Time.stamp, options[:consistency]]]
147
-
148
- @batch ? @batch << mutation : _remove(*mutation[1])
145
+ if @batch
146
+ mutation_map =
147
+ {
148
+ key => {
149
+ column_family => [ _delete_mutation(column_family , is_super(column_family)? column : nil , sub_column , options[:timestamp]|| Time.stamp) ]
150
+ }
151
+ }
152
+ @batch << [mutation_map, options[:consistency]]
153
+ else
154
+ # Let's continue using the 'remove' thrift method...not sure about the implications/performance of using the mutate instead
155
+ # Otherwise we coul get use the mutation_map above, and do _mutate(mutation_map, options[:consistency])
156
+ args = {:column_family => column_family}
157
+ columns = is_super(column_family) ? {:super_column => column, :column => sub_column} : {:column => column}
158
+ column_path = CassandraThrift::ColumnPath.new(args.merge(columns))
159
+ _remove(key, column_path, options[:timestamp] || Time.stamp, options[:consistency])
160
+ end
149
161
  end
150
162
 
151
163
  ### Read
@@ -214,8 +226,8 @@ class Cassandra
214
226
  end
215
227
  end
216
228
 
217
- # Return a list of keys in the column_family you request. Requires the
218
- # table to be partitioned with OrderPreservingHash. Supports the
229
+ # Return a list of keys in the column_family you request. Only works well if
230
+ # the table is partitioned with OrderPreservingPartitioner. Supports the
219
231
  # <tt>:count</tt>, <tt>:start</tt>, <tt>:finish</tt>, and <tt>:consistency</tt>
220
232
  # options.
221
233
  def get_range(column_family, options = {})
@@ -241,16 +253,16 @@ class Cassandra
241
253
 
242
254
  @batch = []
243
255
  yield(self)
244
- compact_mutations!
245
-
246
- @batch.each do |mutation|
247
- case mutation.first
248
- when :remove
249
- _remove(*mutation[1])
250
- else
251
- _mutate(*mutation)
252
- end
253
- end
256
+ compacted_map,seen_clevels = compact_mutations!
257
+ clevel = if options[:consistency] != nil # Override any clevel from individual mutations if
258
+ options[:consistency]
259
+ elsif seen_clevels.length > 1 # Cannot choose which CLevel to use if there are several ones
260
+ raise "Multiple consistency levels used in the batch, and no override...cannot pick one"
261
+ else # if no consistency override has been provided but all the clevels in the batch are the same: use that one
262
+ seen_clevels.first
263
+ end
264
+
265
+ _mutate(compacted_map,clevel)
254
266
  ensure
255
267
  @batch = nil
256
268
  end
@@ -261,13 +273,45 @@ class Cassandra
261
273
  "#{self.class}##{caller[0].split('`').last[0..-3]}"
262
274
  end
263
275
 
264
- # Roll up queued mutations, to improve atomicity.
276
+ # Roll up queued mutations, to improve atomicity (and performance).
265
277
  def compact_mutations!
266
- #TODO re-do this rollup
278
+ used_clevels = {} # hash that lists the consistency levels seen in the batch array. key is the clevel, value is true
279
+ by_key = {}
280
+ # @batch is an array of mutation_ops.
281
+ # A mutation op is a 2-item array containing [mutationmap, consistency_number]
282
+ # a mutation map is a hash, by key (string) that has a hash by CF name, containing a list of column_mutations)
283
+ @batch.each do |mutation_op|
284
+ # A single mutation op looks like:
285
+ # For an insert/update
286
+ #[ { key1 =>
287
+ # { CF1 => [several of CassThrift:Mutation(colname,value,TS,ttl)]
288
+ # CF2 => [several mutations]
289
+ # },
290
+ # key2 => {...} # Not sure if they can come batched like this...so there might only be a single key (and CF)
291
+ # }, # [0]
292
+ # consistency # [1]
293
+ #]
294
+ # For a remove:
295
+ # [ :remove, # [0]
296
+ # [key, CassThrift:ColPath, timestamp, consistency ] # [1]
297
+ # ]
298
+ mmap = mutation_op[0] # :remove OR a hash like {"key"=> {"CF"=>[mutationclass1,...] } }
299
+ used_clevels[mutation_op[1]]=true #save the clevel required for this operation
300
+
301
+ mmap.keys.each do |k|
302
+ by_key[k] = {} unless by_key.has_key? k #make sure the key exists
303
+ mmap[k].keys.each do |cf| # For each CF in that key
304
+ by_key[k][cf] = [] unless by_key[k][cf] != nil
305
+ by_key[k][cf].concat mmap[k][cf] # Append the list of mutations for that key and CF
306
+ end
307
+ end
308
+ end
309
+ # Returns the batch mutations map, and an array with the consistency levels 'seen' in the batch
310
+ [by_key, used_clevels.keys]
267
311
  end
268
312
 
269
313
  def new_client
270
314
  thrift_client_class.new(CassandraThrift::Cassandra::Client, @servers, @thrift_client_options)
271
315
  end
272
316
 
273
- end
317
+ end
@@ -89,6 +89,7 @@ class Cassandra
89
89
  if column
90
90
  row[column]
91
91
  else
92
+ row = apply_range(row, column_family, options[:start], options[:finish])
92
93
  apply_count(row, options[:count], options[:reversed])
93
94
  end
94
95
  end
@@ -107,17 +108,7 @@ class Cassandra
107
108
  row = cf(column_family)[key] && cf(column_family)[key][column] ?
108
109
  cf(column_family)[key][column] :
109
110
  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
111
+ row = apply_range(row, column_family, options[:start], options[:finish], false)
121
112
  apply_count(row, options[:count], options[:reversed])
122
113
  end
123
114
  elsif cf(column_family)[key]
@@ -142,7 +133,7 @@ class Cassandra
142
133
  def remove(column_family, key, *columns_and_options)
143
134
  column_family, column, sub_column, options = extract_and_validate_params_for_real(column_family, key, columns_and_options, WRITE_DEFAULTS)
144
135
  if @batch
145
- @batch << [:remove, column_family, key, column]
136
+ @batch << [:remove, column_family, key, column, sub_column]
146
137
  else
147
138
  if column
148
139
  if sub_column
@@ -318,5 +309,18 @@ class Cassandra
318
309
  row
319
310
  end
320
311
  end
312
+
313
+ def apply_range(row, column_family, strt, fin, standard=true)
314
+ start = to_compare_with_type(strt, column_family, standard)
315
+ finish = to_compare_with_type(fin, column_family, standard)
316
+ ret = OrderedHash.new
317
+ row.keys.each do |key|
318
+ if (start.nil? || key >= start) && (finish.nil? || key <= finish)
319
+ ret[key] = row[key]
320
+ end
321
+ end
322
+ ret
323
+ end
324
+
321
325
  end
322
326
  end
@@ -156,13 +156,13 @@ class Cassandra
156
156
  super
157
157
  end
158
158
 
159
- def delete_if
160
- @timestamps.delete_if
159
+ def delete_if(&block)
160
+ @timestamps.delete_if(&block)
161
161
  super
162
162
  end
163
163
 
164
- def reject!
165
- @timestamps.reject!
164
+ def reject!(&block)
165
+ @timestamps.reject!(&block)
166
166
  super
167
167
  end
168
168
 
@@ -189,12 +189,5 @@ class Cassandra
189
189
  def inspect
190
190
  "#<OrderedHash #{super}\n#{@timestamps.inspect}>"
191
191
  end
192
-
193
- private
194
-
195
- def sync_keys!
196
- @timestamps.delete_if {|k,v| !has_key?(k)}
197
- super
198
- end
199
192
  end
200
193
  end
@@ -1,5 +1,9 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
2
 
3
+ def cassandra07?
4
+ CassandraThrift::VERSION != '2.1.0'
5
+ end
6
+
3
7
  class CassandraTest < Test::Unit::TestCase
4
8
  include Cassandra::Constants
5
9
 
@@ -7,10 +11,10 @@ class CassandraTest < Test::Unit::TestCase
7
11
  @twitter = Cassandra.new('Twitter', "127.0.0.1:9160", :retries => 2, :exception_classes => [])
8
12
  @twitter.clear_keyspace!
9
13
 
10
- @blogs = Cassandra.new('Multiblog')
14
+ @blogs = Cassandra.new('Multiblog', "127.0.0.1:9160",:retries => 2, :exception_classes => [])
11
15
  @blogs.clear_keyspace!
12
16
 
13
- @blogs_long = Cassandra.new('MultiblogLong')
17
+ @blogs_long = Cassandra.new('MultiblogLong', "127.0.0.1:9160",:retries => 2, :exception_classes => [])
14
18
  @blogs_long.clear_keyspace!
15
19
 
16
20
  @uuids = (0..6).map {|i| SimpleUUID::UUID.new(Time.at(2**(24+i))) }
@@ -102,6 +106,18 @@ class CassandraTest < Test::Unit::TestCase
102
106
  assert !@twitter.exists?(:Statuses, 'bogus', 'body')
103
107
  end
104
108
 
109
+ def test_get_value_with_range
110
+ 10.times do |i|
111
+ @twitter.insert(:Statuses, key, {"body-#{i}" => 'v'})
112
+ end
113
+
114
+ assert_equal 5, @twitter.get(:Statuses, key, :count => 5).length
115
+ assert_equal 5, @twitter.get(:Statuses, key, :start => "body-5").length
116
+ assert_equal 5, @twitter.get(:Statuses, key, :finish => "body-4").length
117
+ assert_equal 5, @twitter.get(:Statuses, key, :start => "body-1", :count => 5).length
118
+ assert_equal 5, @twitter.get(:Statuses, key, :start => "body-0", :finish => "body-4", :count => 7).length
119
+ end
120
+
105
121
  def test_exists_with_only_key
106
122
  @twitter.insert(:Statuses, key, {'body' => 'v'})
107
123
  assert @twitter.exists?(:Statuses, key)
@@ -240,11 +256,12 @@ class CassandraTest < Test::Unit::TestCase
240
256
  end
241
257
 
242
258
  def test_remove_super_value
243
- columns = {@uuids[1] => 'v1'}
259
+ columns = {@uuids[1] => 'v1', @uuids[2] => 'v2'}
260
+ column_name_to_remove = @uuids[2]
244
261
  @twitter.insert(:StatusRelationships, key, {'user_timelines' => columns})
245
- @twitter.remove(:StatusRelationships, key, 'user_timelines', columns.keys.first)
246
- assert_nil @twitter.get(:StatusRelationships, key, 'user_timelines', columns.keys.first)
247
- assert_nil @twitter.get(:StatusRelationships, key, 'user_timelines').timestamps[columns.keys.first]
262
+ @twitter.remove(:StatusRelationships, key, 'user_timelines', column_name_to_remove)
263
+ assert_equal( columns.reject{|k,v| k == column_name_to_remove}, @twitter.get(:StatusRelationships, key, 'user_timelines') )
264
+ assert_nil @twitter.get(:StatusRelationships, key, 'user_timelines').timestamps[column_name_to_remove]
248
265
  end
249
266
 
250
267
  def test_clear_column_family
@@ -344,6 +361,12 @@ class CassandraTest < Test::Unit::TestCase
344
361
  k = key
345
362
 
346
363
  @twitter.insert(:Users, k + '1', {'body' => 'v1', 'user' => 'v1'})
364
+ initial_subcolumns = {@uuids[1] => 'v1', @uuids[2] => 'v2'}
365
+ @twitter.insert(:StatusRelationships, k, {'user_timelines' => initial_subcolumns, 'dummy_supercolumn' => {@uuids[5] => 'value'}})
366
+ assert_equal(initial_subcolumns, @twitter.get(:StatusRelationships, k, 'user_timelines'))
367
+ assert_equal({@uuids[5] => 'value'}, @twitter.get(:StatusRelationships, k, 'dummy_supercolumn'))
368
+ new_subcolumns = {@uuids[3] => 'v3', @uuids[4] => 'v4'}
369
+ subcolumn_to_delete = initial_subcolumns.keys.first # the first column of the initial set
347
370
 
348
371
  @twitter.batch do
349
372
  @twitter.insert(:Users, k + '2', {'body' => 'v2', 'user' => 'v2'})
@@ -361,6 +384,15 @@ class CassandraTest < Test::Unit::TestCase
361
384
  @twitter.remove(:Users, k + '4')
362
385
  @twitter.insert(:Users, k + '4', {'body' => 'v4', 'user' => 'v4'})
363
386
  assert_equal({}, @twitter.get(:Users, k + '4')) # Not yet written
387
+
388
+ # SuperColumns
389
+ # Add and delete new sub columns to the user timeline supercolumn
390
+ @twitter.insert(:StatusRelationships, k, {'user_timelines' => new_subcolumns })
391
+ @twitter.remove(:StatusRelationships, k, 'user_timelines' , subcolumn_to_delete ) # Delete the first of the initial_subcolumns from the user_timeline supercolumn
392
+ assert_equal(initial_subcolumns, @twitter.get(:StatusRelationships, k, 'user_timelines')) # No additions or deletes reflected yet
393
+ # Delete a complete supercolumn
394
+ @twitter.remove(:StatusRelationships, k, 'dummy_supercolumn' ) # Delete the full dummy supercolumn
395
+ assert_equal({@uuids[5] => 'value'}, @twitter.get(:StatusRelationships, k, 'dummy_supercolumn')) # dummy supercolumn not yet deleted
364
396
  end
365
397
 
366
398
  assert_equal({'body' => 'v2', 'user' => 'v2'}, @twitter.get(:Users, k + '2')) # Written
@@ -373,6 +405,12 @@ class CassandraTest < Test::Unit::TestCase
373
405
  assert_equal({'body' => 'v3', 'user' => 'v3', 'location' => 'v3'}.keys.sort, @twitter.get(:Users, k + '3').timestamps.keys.sort) # Written and compacted
374
406
  assert_equal({'body' => 'v4', 'user' => 'v4'}.keys.sort, @twitter.get(:Users, k + '4').timestamps.keys.sort) # Written
375
407
  assert_equal({'body' => 'v'}.keys.sort, @twitter.get(:Statuses, k + '3').timestamps.keys.sort) # Written
408
+
409
+ # Final result: initial_subcolumns - initial_subcolumns.first + new_subcolumns
410
+ resulting_subcolumns = initial_subcolumns.merge(new_subcolumns).reject{|k,v| k == subcolumn_to_delete }
411
+ assert_equal(resulting_subcolumns, @twitter.get(:StatusRelationships, key, 'user_timelines'))
412
+ assert_equal({}, @twitter.get(:StatusRelationships, key, 'dummy_supercolumn')) # dummy supercolumn deleted
413
+
376
414
  end
377
415
 
378
416
  def test_complain_about_nil_key
@@ -381,13 +419,6 @@ class CassandraTest < Test::Unit::TestCase
381
419
  end
382
420
  end
383
421
 
384
- def test_raise_access_error_on_nonexistent_keyspace
385
- nonexistent = Cassandra.new('Nonexistent')
386
- assert_raises(Cassandra::AccessError) do
387
- nonexistent.get "foo", "bar"
388
- end
389
- end
390
-
391
422
  def test_nil_sub_column_value
392
423
  @twitter.insert(:Index, 'asdf', {"thing" => {'jkl' => ''} })
393
424
  end
@@ -397,6 +428,13 @@ class CassandraTest < Test::Unit::TestCase
397
428
  assert_nil @twitter.instance_variable_get(:@client)
398
429
  end
399
430
 
431
+ def test_disconnect_when_not_connected!
432
+ assert_nothing_raised do
433
+ @twitter = Cassandra.new('Twitter', "127.0.0.1:9160", :retries => 2, :exception_classes => [])
434
+ @twitter.disconnect!
435
+ end
436
+ end
437
+
400
438
  def test_super_allows_for_non_string_values_while_normal_does_not
401
439
  columns = {'user_timelines' => {@uuids[4] => '4', @uuids[5] => '5'}}
402
440
 
@@ -404,6 +442,34 @@ class CassandraTest < Test::Unit::TestCase
404
442
  @twitter.insert(:Statuses, key, { 'body' => '1' })
405
443
  end
406
444
 
445
+ if cassandra07?
446
+ def test_creating_and_dropping_new_index
447
+ @twitter.create_index('Twitter', 'Statuses', 'column_name', 'LongType')
448
+ assert_nil @twitter.create_index('Twitter', 'Statuses', 'column_name', 'LongType')
449
+
450
+ @twitter.drop_index('Twitter', 'Statuses', 'column_name')
451
+ assert_nil @twitter.drop_index('Twitter', 'Statuses', 'column_name')
452
+
453
+ # Recreating and redropping the same index should not error either.
454
+ @twitter.create_index('Twitter', 'Statuses', 'column_name', 'LongType')
455
+ @twitter.drop_index('Twitter', 'Statuses', 'column_name')
456
+ end
457
+
458
+ def test_get_indexed_slices
459
+ @twitter.create_index('Twitter', 'Statuses', 'x', 'LongType')
460
+
461
+ @twitter.insert(:Statuses, 'row1', { 'x' => [0,10].pack("NN") })
462
+ @twitter.insert(:Statuses, 'row2', { 'x' => [0,20].pack("NN") })
463
+
464
+ idx_expr = @twitter.create_idx_expr('x', [0,20].pack("NN"), "==")
465
+ idx_clause = @twitter.create_idx_clause([idx_expr])
466
+
467
+ indexed_row = @twitter.get_indexed_slices(:Statuses, idx_clause)
468
+ assert_equal(1, indexed_row.length)
469
+ assert_equal('row2', indexed_row.first.key)
470
+ end
471
+ end
472
+
407
473
  private
408
474
 
409
475
  def key
@@ -321,6 +321,7 @@ class OrderedHashTest < Test::Unit::TestCase
321
321
  @ordered_hash.reject! { |k, _| k == 'pink' }
322
322
  assert_equal copy, @ordered_hash
323
323
  assert !@ordered_hash.keys.include?('pink')
324
+ assert !@ordered_hash.timestamps.keys.include?('pink')
324
325
  end
325
326
 
326
327
  def test_reject
@@ -6,6 +6,7 @@
6
6
 
7
7
  require 'thrift'
8
8
  require 'cassandra_types'
9
+ require 'cassandra_constants'
9
10
 
10
11
  module CassandraThrift
11
12
  module Cassandra
@@ -6,6 +6,7 @@
6
6
 
7
7
  require 'thrift'
8
8
  require 'cassandra_types'
9
+ require 'cassandra_constants'
9
10
 
10
11
  module CassandraThrift
11
12
  module Cassandra
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cassandra
3
3
  version: !ruby/object:Gem::Version
4
- hash: 59
4
+ hash: 57
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 9
9
- - 0
10
- version: 0.9.0
9
+ - 1
10
+ version: 0.9.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Evan Weaver, Ryan King
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-08 00:00:00 -08:00
18
+ date: 2011-04-06 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -159,7 +159,7 @@ files:
159
159
  - vendor/0.7/gen-rb/cassandra_types.rb
160
160
  - cassandra.gemspec
161
161
  has_rdoc: true
162
- homepage: http://blog.evanweaver.com/files/doc/fauna/cassandra/
162
+ homepage: ""
163
163
  licenses: []
164
164
 
165
165
  post_install_message: