cassandra 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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: