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 +4 -0
- data/README.rdoc +15 -0
- data/Rakefile +1 -3
- data/cassandra.gemspec +3 -3
- data/conf/storage-conf.xml +1 -0
- data/lib/cassandra/0.6/cassandra.rb +9 -11
- data/lib/cassandra/0.6/columns.rb +26 -0
- data/lib/cassandra/0.7/cassandra.rb +99 -22
- data/lib/cassandra/0.7/columns.rb +26 -0
- data/lib/cassandra/0.7/protocol.rb +16 -0
- data/lib/cassandra/cassandra.rb +68 -24
- data/lib/cassandra/mock.rb +16 -12
- data/lib/cassandra/ordered_hash.rb +4 -11
- data/test/cassandra_test.rb +79 -13
- data/test/ordered_hash_test.rb +1 -0
- data/vendor/0.6/gen-rb/cassandra.rb +1 -0
- data/vendor/0.7/gen-rb/cassandra.rb +1 -0
- metadata +5 -5
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.
|
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.
|
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{
|
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{
|
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}
|
data/conf/storage-conf.xml
CHANGED
@@ -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
|
-
|
56
|
-
|
57
|
-
|
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)
|
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
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
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)
|
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
|
-
|
188
|
-
|
189
|
-
|
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 => [])
|
data/lib/cassandra/cassandra.rb
CHANGED
@@ -84,8 +84,10 @@ class Cassandra
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def disconnect!
|
87
|
-
@client
|
88
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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.
|
218
|
-
# table
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
-
#
|
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
|
data/lib/cassandra/mock.rb
CHANGED
@@ -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
|
-
|
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
|
data/test/cassandra_test.rb
CHANGED
@@ -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',
|
246
|
-
|
247
|
-
assert_nil @twitter.get(:StatusRelationships, key, 'user_timelines').timestamps[
|
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
|
data/test/ordered_hash_test.rb
CHANGED
@@ -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
|
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:
|
4
|
+
hash: 57
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 9
|
9
|
-
-
|
10
|
-
version: 0.9.
|
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:
|
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:
|
162
|
+
homepage: ""
|
163
163
|
licenses: []
|
164
164
|
|
165
165
|
post_install_message:
|