cassandra 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ v0.5. More API changes. Working temporal comparators.
3
+
2
4
  v0.4. Use new comparator API. Namespace Thrift bindings; rename gem and class to Cassandra. Make tokens and limits actually work. Retry UnavailableExceptions.
3
5
 
4
6
  v0.3. Use new Thrift API.
data/Manifest CHANGED
@@ -4,23 +4,23 @@ conf/log4j.properties
4
4
  conf/storage-conf.xml
5
5
  lib/cassandra/array.rb
6
6
  lib/cassandra/cassandra.rb
7
+ lib/cassandra/columns.rb
7
8
  lib/cassandra/comparable.rb
8
9
  lib/cassandra/constants.rb
9
- lib/cassandra/helper.rb
10
10
  lib/cassandra/long.rb
11
11
  lib/cassandra/ordered_hash.rb
12
+ lib/cassandra/protocol.rb
12
13
  lib/cassandra/safe_client.rb
13
- lib/cassandra/serialization.rb
14
14
  lib/cassandra/time.rb
15
15
  lib/cassandra/uuid.rb
16
16
  lib/cassandra.rb
17
17
  LICENSE
18
18
  Manifest
19
- quickstart.sh
20
19
  Rakefile
21
20
  README
22
- test/cassandra_client_test.rb
21
+ test/cassandra_test.rb
23
22
  test/comparable_types_test.rb
23
+ test/test_helper.rb
24
24
  vendor/gen-rb/cassandra.rb
25
25
  vendor/gen-rb/cassandra_constants.rb
26
26
  vendor/gen-rb/cassandra_types.rb
data/README CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  cassandra
3
3
 
4
- A client for the Cassandra distributed database.
4
+ A Ruby client for the Cassandra distributed database.
5
5
 
6
6
  == License
7
7
 
@@ -12,26 +12,26 @@ The public certificate for this gem is here[http://rubyforge.org/frs/download.ph
12
12
  == Features
13
13
 
14
14
  * clean encapsulation of the Thrift API
15
- * pluggable serialization and compression
15
+ * compatible UUID and Long classes, for GUID generation
16
16
  * Ruby 1.9 compatibility
17
17
 
18
- This is an alpha release and does not yet support the full Thrift API.
18
+ This is an alpha release and may not yet support the full set of Cassandra features.
19
19
 
20
- Cassandra is a rapidly moving target, but this library should always run against the bundle available {here}[http://github.com/fauna/cassandra/raw/6546d1155b77ca7bc5ebf58bdcce79ddda36f611/vendor/cassandra.tar.bz2]. Don't expect it to work against any other version right now.
21
-
22
- The Github source repository is {here}[http://github.com/fauna/cassandra/]; patches and contributions are very welcome.
20
+ The Github source repository is {here}[http://github.com/fauna/cassandra/]. Patches and contributions are very welcome.
23
21
 
24
22
  == Installation
25
23
 
26
- You need Ruby 1.8 or 1.9, and Thrift.
27
-
28
- If you don't want to install Thrift from {source}[http://incubator.apache.org/thrift/download/], you can use {this pre-packaged gem}[http://blog.evanweaver.com/files/cassandra/thrift-0.1.0.gem]. Once you have Thrift, just run:
24
+ You need Ruby 1.8 or 1.9. If you have those, just run:
29
25
 
30
26
  sudo gem install cassandra
31
27
 
32
28
  == Usage
33
29
 
34
- Require the library:
30
+ Cassandra is a rapidly moving target. In order to get a working server, change to the checkout or gem directory, and run:
31
+
32
+ rake cassandra
33
+
34
+ Now, start IRb and require the library:
35
35
 
36
36
  require 'cassandra'
37
37
 
@@ -45,7 +45,7 @@ Insert into a column family. You can insert a Cassandra::OrderedHash, or a regul
45
45
 
46
46
  Insert into a super column family:
47
47
 
48
- client.insert(:UserRelationships, "5", {"user_timeline" => {"1" => ""}})
48
+ client.insert(:UserRelationships, "5", {"user_timeline" => {UUID.new => "1"}})
49
49
 
50
50
  Query a super column:
51
51
 
data/Rakefile CHANGED
@@ -3,11 +3,66 @@ require 'echoe'
3
3
  Echoe.new("cassandra") do |p|
4
4
  p.author = "Evan Weaver"
5
5
  p.project = "fauna"
6
- p.summary = "A Ruby client for CassandraDB."
6
+ p.summary = "A Ruby client for the Cassandra distributed database."
7
7
  p.rubygems_version = ">= 0.8"
8
- p.dependencies = ['json']
8
+ p.dependencies = ['json', 'thrift']
9
9
  p.ignore_pattern = /^(data|vendor\/cassandra|cassandra|vendor\/thrift)/
10
- p.rdoc_pattern = /^(lib|bin|tasks|ext)|_types.rb|_constants.rb|^README|^CHANGELOG|^TODO|^LICENSE|^COPYING$/
10
+ p.rdoc_pattern = /^(lib|bin|tasks|ext)|^README|^CHANGELOG|^TODO|^LICENSE|^COPYING$/
11
11
  p.url = "http://blog.evanweaver.com/files/doc/fauna/cassandra/"
12
12
  p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
13
13
  end
14
+
15
+ desc "Start Cassandra"
16
+ task :cassandra => [:checkout, :patch, :build] do
17
+ env = "CASSANDRA_INCLUDE=#{Dir.pwd}/conf/cassandra.in.sh" unless ENV["CASSANDRA_INCLUDE"]
18
+ exec("env #{env} cassandra/bin/cassandra -f")
19
+ end
20
+
21
+ REVISION = "15354b4906fd654d58fe50fd01ebf95b69434ba9"
22
+ PATCHES = [
23
+ "http://issues.apache.org/jira/secure/attachment/12416014/0001-CASSANDRA-356-rename-clean-up-collectColumns-methods.txt",
24
+ "http://issues.apache.org/jira/secure/attachment/12416073/0002-v3.patch",
25
+ "http://issues.apache.org/jira/secure/attachment/12416074/357-v2.patch",
26
+ "http://issues.apache.org/jira/secure/attachment/12416086/357-3.patch"]
27
+
28
+ task :checkout do
29
+ # Like a git submodule, but all in one obvious place
30
+ unless File.exist?("cassandra")
31
+ system("git clone git://git.apache.org/cassandra.git")
32
+ ENV["RESET"] = "true"
33
+ end
34
+ end
35
+
36
+ task :patch do
37
+ if ENV["RESET"]
38
+ Dir.chdir("cassandra") do
39
+ system("ant clean && git fetch && git reset #{REVISION} --hard")
40
+ # Delete untracked files, so that the patchs can apply again
41
+ Array(`git status`[/Untracked files:(.*)$/m, 1].to_s.split("\n")[3..-1]).each do |file|
42
+ File.unlink(file.sub(/^.\s+/, "")) rescue nil
43
+ end
44
+ # Patch, with a handy commit for each one
45
+ PATCHES.each do |url|
46
+ raise "#{url} failed" unless system("curl #{url} | patch -p1")
47
+ system("git commit -a -m 'Applied patch: #{url.inspect}'")
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ task :build do
54
+ Dir.chdir("cassandra") { system("ant") } unless File.exist?("cassandra/build")
55
+ end
56
+
57
+ task :clean do
58
+ Dir.chdir("cassandra") { system("ant clean") }
59
+ end
60
+
61
+ desc "Regenerate thrift bindings for Cassandra"
62
+ task :thrift do
63
+ system(
64
+ "cd vendor &&
65
+ rm -rf gen-rb &&
66
+ thrift -gen rb ../cassandra/interface/cassandra.thrift")
67
+ end
68
+
@@ -2,24 +2,24 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{cassandra}
5
- s.version = "0.4"
5
+ s.version = "0.5"
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"]
9
9
  s.cert_chain = ["/Users/eweaver/p/configuration/gem_certificates/evan_weaver-original-public_cert.pem"]
10
- s.date = %q{2009-07-30}
11
- s.description = %q{A Ruby client for CassandraDB.}
10
+ s.date = %q{2009-08-18}
11
+ s.description = %q{A Ruby client for the Cassandra distributed database.}
12
12
  s.email = %q{}
13
- s.extra_rdoc_files = ["CHANGELOG", "lib/cassandra/array.rb", "lib/cassandra/cassandra.rb", "lib/cassandra/comparable.rb", "lib/cassandra/constants.rb", "lib/cassandra/helper.rb", "lib/cassandra/long.rb", "lib/cassandra/ordered_hash.rb", "lib/cassandra/safe_client.rb", "lib/cassandra/serialization.rb", "lib/cassandra/time.rb", "lib/cassandra/uuid.rb", "lib/cassandra.rb", "LICENSE", "README", "vendor/gen-rb/cassandra_constants.rb", "vendor/gen-rb/cassandra_types.rb"]
14
- s.files = ["CHANGELOG", "conf/cassandra.in.sh", "conf/log4j.properties", "conf/storage-conf.xml", "lib/cassandra/array.rb", "lib/cassandra/cassandra.rb", "lib/cassandra/comparable.rb", "lib/cassandra/constants.rb", "lib/cassandra/helper.rb", "lib/cassandra/long.rb", "lib/cassandra/ordered_hash.rb", "lib/cassandra/safe_client.rb", "lib/cassandra/serialization.rb", "lib/cassandra/time.rb", "lib/cassandra/uuid.rb", "lib/cassandra.rb", "LICENSE", "Manifest", "quickstart.sh", "Rakefile", "README", "test/cassandra_client_test.rb", "test/comparable_types_test.rb", "vendor/gen-rb/cassandra.rb", "vendor/gen-rb/cassandra_constants.rb", "vendor/gen-rb/cassandra_types.rb", "cassandra.gemspec"]
13
+ s.extra_rdoc_files = ["CHANGELOG", "lib/cassandra/array.rb", "lib/cassandra/cassandra.rb", "lib/cassandra/columns.rb", "lib/cassandra/comparable.rb", "lib/cassandra/constants.rb", "lib/cassandra/long.rb", "lib/cassandra/ordered_hash.rb", "lib/cassandra/protocol.rb", "lib/cassandra/safe_client.rb", "lib/cassandra/time.rb", "lib/cassandra/uuid.rb", "lib/cassandra.rb", "LICENSE", "README"]
14
+ s.files = ["CHANGELOG", "conf/cassandra.in.sh", "conf/log4j.properties", "conf/storage-conf.xml", "lib/cassandra/array.rb", "lib/cassandra/cassandra.rb", "lib/cassandra/columns.rb", "lib/cassandra/comparable.rb", "lib/cassandra/constants.rb", "lib/cassandra/long.rb", "lib/cassandra/ordered_hash.rb", "lib/cassandra/protocol.rb", "lib/cassandra/safe_client.rb", "lib/cassandra/time.rb", "lib/cassandra/uuid.rb", "lib/cassandra.rb", "LICENSE", "Manifest", "Rakefile", "README", "test/cassandra_test.rb", "test/comparable_types_test.rb", "test/test_helper.rb", "vendor/gen-rb/cassandra.rb", "vendor/gen-rb/cassandra_constants.rb", "vendor/gen-rb/cassandra_types.rb", "cassandra.gemspec"]
15
15
  s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/cassandra/}
16
16
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Cassandra", "--main", "README"]
17
17
  s.require_paths = ["lib"]
18
18
  s.rubyforge_project = %q{fauna}
19
19
  s.rubygems_version = %q{1.3.4}
20
20
  s.signing_key = %q{/Users/eweaver/p/configuration/gem_certificates/evan_weaver-original-private_key.pem}
21
- s.summary = %q{A Ruby client for CassandraDB.}
22
- s.test_files = ["test/cassandra_client_test.rb", "test/comparable_types_test.rb"]
21
+ s.summary = %q{A Ruby client for the Cassandra distributed database.}
22
+ s.test_files = ["test/cassandra_test.rb", "test/comparable_types_test.rb", "test/test_helper.rb"]
23
23
 
24
24
  if s.respond_to? :specification_version then
25
25
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -27,10 +27,13 @@ Gem::Specification.new do |s|
27
27
 
28
28
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
29
  s.add_runtime_dependency(%q<json>, [">= 0"])
30
+ s.add_runtime_dependency(%q<thrift>, [">= 0"])
30
31
  else
31
32
  s.add_dependency(%q<json>, [">= 0"])
33
+ s.add_dependency(%q<thrift>, [">= 0"])
32
34
  end
33
35
  else
34
36
  s.add_dependency(%q<json>, [">= 0"])
37
+ s.add_dependency(%q<thrift>, [">= 0"])
35
38
  end
36
39
  end
@@ -28,24 +28,24 @@
28
28
 
29
29
  There is an implicit table named 'system' for Cassandra internals.
30
30
  -->
31
- <Tables>
32
- <Table Name="Twitter">
31
+ <Keyspaces>
32
+ <Keyspace Name="Twitter">
33
33
  <KeysCachedFraction>0.01</KeysCachedFraction>
34
34
  <ColumnFamily CompareWith="UTF8Type" Name="Users" />
35
35
  <ColumnFamily CompareWith="UTF8Type" Name="UserAudits" />
36
- <ColumnFamily CompareWith="UTF8Type" CompareSubcolumnsWith="UUIDType" ColumnType="Super" Name="UserRelationships" />
36
+ <ColumnFamily CompareWith="UTF8Type" CompareSubcolumnsWith="TimeUUIDType" ColumnType="Super" Name="UserRelationships" />
37
37
  <ColumnFamily CompareWith="UTF8Type" Name="Usernames" />
38
38
  <ColumnFamily CompareWith="UTF8Type" Name="Statuses" />
39
39
  <ColumnFamily CompareWith="UTF8Type" Name="StatusAudits" />
40
- <ColumnFamily CompareWith="UTF8Type" CompareSubcolumnsWith="LongType" ColumnType="Super" Name="StatusRelationships" />
41
- </Table>
40
+ <ColumnFamily CompareWith="UTF8Type" CompareSubcolumnsWith="TimeUUIDType" ColumnType="Super" Name="StatusRelationships" />
41
+ </Keyspace>
42
42
 
43
- <Table Name="Multiblog">
43
+ <Keyspace Name="Multiblog">
44
44
  <KeysCachedFraction>0.01</KeysCachedFraction>
45
45
  <ColumnFamily CompareWith="UTF8Type" Name="Blogs"/>
46
46
  <ColumnFamily CompareWith="UTF8Type" Name="Comments"/>
47
- </Table>
48
- </Tables>
47
+ </Keyspace>
48
+ </Keyspaces>
49
49
 
50
50
  <!-- Partitioner: any IPartitioner may be used, including your own
51
51
  as long as it is on the classpath. Out of the box,
@@ -71,11 +71,24 @@
71
71
  clusters with a small number of nodes. -->
72
72
  <InitialToken></InitialToken>
73
73
 
74
- <!-- RackAware: Setting this to true instructs Cassandra to try and place the replicas in
75
- a different rack in the same datacenter and one in a different datacenter
74
+
75
+ <!-- EndPointSnitch: Setting this to the class that implements IEndPointSnitch
76
+ which will see if two endpoints are in the same data center or on the same rack.
77
+ Out of the box, Cassandra provides
78
+ org.apache.cassandra.locator.EndPointSnitch
76
79
  -->
77
- <RackAware>false</RackAware>
78
-
80
+ <EndPointSnitch>org.apache.cassandra.locator.EndPointSnitch</EndPointSnitch>
81
+
82
+ <!-- Strategy: Setting this to the class that implements IReplicaPlacementStrategy
83
+ will change the way the node picker works.
84
+ Out of the box, Cassandra provides
85
+ org.apache.cassandra.locator.RackUnawareStrategy
86
+ org.apache.cassandra.locator.RackAwareStrategy
87
+ (place one replica in a different datacenter, and the
88
+ others on different racks in the same one.)
89
+ -->
90
+ <ReplicaPlacementStrategy>org.apache.cassandra.locator.RackUnawareStrategy</ReplicaPlacementStrategy>
91
+
79
92
  <!-- Number of replicas of the data-->
80
93
  <ReplicationFactor>1</ReplicationFactor>
81
94
 
@@ -90,14 +103,15 @@
90
103
  <BootstrapFileDirectory>data/bootstrap</BootstrapFileDirectory>
91
104
  <StagingFileDirectory>data/staging</StagingFileDirectory>
92
105
 
93
-
94
- <!-- Add names of hosts that are deemed contact points. Cassandra nodes use
106
+ <!-- Addresses of hosts that are deemed contact points. Cassandra nodes use
95
107
  this list of hosts to find each other and learn the topology of the ring.
108
+ You must change this if you are running multiple nodes!
96
109
  -->
97
110
  <Seeds>
98
111
  <Seed>127.0.0.1</Seed>
99
112
  </Seeds>
100
113
 
114
+
101
115
  <!-- Miscellaneous -->
102
116
 
103
117
  <!-- time to wait for a reply from other nodes before failing the command -->
@@ -193,5 +207,4 @@
193
207
  face of hardware failures. The default value is ten days.
194
208
  -->
195
209
  <GCGraceSeconds>864000</GCGraceSeconds>
196
-
197
210
  </Storage>
@@ -15,8 +15,8 @@ require 'cassandra/comparable'
15
15
  require 'cassandra/uuid'
16
16
  require 'cassandra/long'
17
17
  require 'cassandra/safe_client'
18
- require 'cassandra/serialization'
19
18
  require 'cassandra/ordered_hash'
20
- require 'cassandra/constants'
21
- require 'cassandra/helper'
19
+ require 'cassandra/columns'
20
+ require 'cassandra/protocol'
22
21
  require 'cassandra/cassandra'
22
+ require 'cassandra/constants'
@@ -4,5 +4,5 @@ class Array
4
4
  result = []
5
5
  each { |el| result.concat(Array(el)) }
6
6
  result
7
- end
7
+ end
8
8
  end
@@ -1,22 +1,62 @@
1
1
 
2
- class Cassandra
3
- include Helper
4
- class AccessError < StandardError; end
2
+ =begin rdoc
3
+ Create a new Cassandra client instance. Accepts a keyspace name, and optional host and port.
4
+
5
+ client = Cassandra.new('twitter', '127.0.0.1', 9160)
5
6
 
6
- MAX_INT = 2**31 - 1
7
+ You can then make calls to the server via the <tt>client</tt> instance.
8
+
9
+ client.insert(:UserRelationships, "5", {"user_timeline" => {UUID.new => "1"}})
10
+ client.get(:UserRelationships, "5", "user_timeline")
7
11
 
12
+ For read methods, valid option parameters are:
13
+
14
+ <tt>:count</tt>:: How many results to return. Defaults to 100.
15
+ <tt>:start</tt>:: Column name token at which to start iterating, inclusive. Defaults to nil, which means the first column in the collation order.
16
+ <tt>:finish</tt>:: Column name token at which to stop iterating, inclusive. Defaults to nil, which means no boundary.
17
+ <tt>:reversed</tt>:: Swap the direction of the collation order.
18
+ <tt>:consistency</tt>:: The consistency level of the request. Defaults to <tt>Cassandra::Consistency::ONE</tt> (one node must respond). Other valid options are <tt>Cassandra::Consistency::ZERO</tt>, <tt>Cassandra::Consistency::QUORUM</tt>, and <tt>Cassandra::Consistency::ALL</tt>.
19
+
20
+ Note that some read options have no relevance in some contexts.
21
+
22
+ For write methods, valid option parameters are:
23
+
24
+ <tt>:timestamp </tt>:: The transaction timestamp. Defaults to the current time in milliseconds. This is used for conflict resolution by the server; you normally never need to change it.
25
+ <tt>:consistency</tt>:: See above.
26
+
27
+ =end rdoc
28
+
29
+ class Cassandra
30
+ include Columns
31
+ include Protocol
32
+
33
+ class AccessError < StandardError #:nodoc:
34
+ end
35
+
8
36
  module Consistency
9
37
  include CassandraThrift::ConsistencyLevel
10
- NONE = ZERO
11
- WEAK = ONE
12
- STRONG = QUORUM
13
- PERFECT = ALL
14
38
  end
15
-
39
+
40
+ MAX_INT = 2**31 - 1
41
+
42
+ WRITE_DEFAULTS = {
43
+ :count => MAX_INT,
44
+ :timestamp => nil,
45
+ :consistency => Consistency::ONE
46
+ }.freeze
47
+
48
+ READ_DEFAULTS = {
49
+ :count => 100,
50
+ :start => nil,
51
+ :finish => nil,
52
+ :reversed => false,
53
+ :consistency => Consistency::ONE
54
+ }.freeze
55
+
16
56
  attr_reader :keyspace, :host, :port, :serializer, :transport, :client, :schema
17
57
 
18
58
  # Instantiate a new Cassandra and open the connection.
19
- def initialize(keyspace, host = '127.0.0.1', port = 9160, serializer = Cassandra::Serialization::JSON)
59
+ def initialize(keyspace, host = '127.0.0.1', port = 9160)
20
60
  @is_super = {}
21
61
  @column_name_class = {}
22
62
  @sub_column_name_class = {}
@@ -24,242 +64,194 @@ class Cassandra
24
64
  @keyspace = keyspace
25
65
  @host = host
26
66
  @port = port
27
- @serializer = serializer
28
67
 
29
68
  @transport = Thrift::BufferedTransport.new(Thrift::Socket.new(@host, @port))
30
- @transport.open
69
+ @transport.open
31
70
  @client = CassandraThrift::Cassandra::SafeClient.new(
32
- CassandraThrift::Cassandra::Client.new(Thrift::BinaryProtocol.new(@transport)),
71
+ CassandraThrift::Cassandra::Client.new(Thrift::BinaryProtocol.new(@transport)),
33
72
  @transport)
34
73
 
35
- keyspaces = @client.get_string_list_property("tables")
74
+ keyspaces = @client.get_string_list_property("keyspaces")
36
75
  unless keyspaces.include?(@keyspace)
37
76
  raise AccessError, "Keyspace #{@keyspace.inspect} not found. Available: #{keyspaces.inspect}"
38
77
  end
39
-
40
- @schema = @client.describe_table(@keyspace)
78
+
79
+ @schema = @client.describe_keyspace(@keyspace)
41
80
  end
42
-
81
+
43
82
  def inspect
44
83
  "#<Cassandra:#{object_id}, @keyspace=#{keyspace.inspect}, @schema={#{
45
84
  schema.map {|name, hash| ":#{name} => #{hash['type'].inspect}"}.join(', ')
46
- }}, @host=#{host.inspect}, @port=#{port}, @serializer=#{serializer.name}>"
85
+ }}, @host=#{host.inspect}, @port=#{port}>"
47
86
  end
48
87
 
49
- ## Write
50
-
51
- # Insert a row for a key. Pass a flat hash for a regular column family, and
52
- # a nested hash for a super column family.
53
- def insert(column_family, key, hash, consistency = Consistency::WEAK, timestamp = Time.stamp)
54
- column_family = column_family.to_s
55
- mutation = if is_super(column_family)
56
- CassandraThrift::BatchMutationSuper.new(:key => key, :cfmap => {column_family.to_s => hash_to_super_columns(column_family, hash, timestamp)})
88
+ ### Write
89
+
90
+ # Insert a row for a key. Pass a flat hash for a regular column family, and
91
+ # a nested hash for a super column family. Supports the <tt>:consistency</tt>
92
+ # and <tt>:timestamp</tt> options.
93
+ def insert(column_family, key, hash, options = {})
94
+ column_family, _, _, options = params(column_family, [options], WRITE_DEFAULTS)
95
+
96
+ mutation = if is_super(column_family)
97
+ CassandraThrift::BatchMutationSuper.new(
98
+ :key => key,
99
+ :cfmap => {column_family =>
100
+ hash_to_super_columns(column_family, hash, options[:timestamp] || Time.stamp)})
57
101
  else
58
- CassandraThrift::BatchMutation.new(:key => key, :cfmap => {column_family.to_s => hash_to_columns(column_family, hash, timestamp)})
102
+ CassandraThrift::BatchMutation.new(
103
+ :key => key,
104
+ :cfmap => {column_family =>
105
+ hash_to_columns(column_family, hash, options[:timestamp] || Time.stamp)})
59
106
  end
60
- # FIXME Batched operations discard the consistency argument
61
- @batch ? @batch << mutation : _insert(mutation, consistency)
107
+
108
+ args = [mutation, options[:consistency]]
109
+ @batch ? @batch << args : _insert(*args)
62
110
  end
63
-
64
- private
65
-
66
- def _insert(mutation, consistency = Consistency::WEAK)
67
- case mutation
68
- when CassandraThrift::BatchMutationSuper then @client.batch_insert_super_column(@keyspace, mutation, consistency)
69
- when CassandraThrift::BatchMutation then @client.batch_insert(@keyspace, mutation, consistency)
70
- end
71
- end
72
-
73
- public
74
-
111
+
75
112
  ## Delete
76
-
113
+
77
114
  # Remove the element at the column_family:key:[column]:[sub_column]
78
- # path you request.
79
- def remove(column_family, key, column = nil, sub_column = nil, consistency = Consistency::WEAK, timestamp = Time.stamp)
80
- column_family = column_family.to_s
81
- assert_column_name_classes(column_family, column, sub_column)
82
-
83
- column = column.to_s if column
84
- sub_column = sub_column.to_s if sub_column
85
- args = [column_family, key, column, sub_column, consistency, timestamp]
115
+ # path you request. Supports the <tt>:consistency</tt> and <tt>:timestamp</tt>
116
+ # options.
117
+ def remove(column_family, key, *columns_and_options)
118
+ column_family, column, sub_column, options = params(column_family, columns_and_options, WRITE_DEFAULTS)
119
+ args = [column_family, key, column, sub_column, options[:consistency], options[:timestamp] || Time.stamp]
86
120
  @batch ? @batch << args : _remove(*args)
87
121
  end
88
-
89
- private
90
-
91
- def _remove(column_family, key, column, sub_column, consistency, timestamp)
92
- column_path_or_parent = if is_super(column_family)
93
- CassandraThrift::ColumnPathOrParent.new(:column_family => column_family, :super_column => column, :column => sub_column)
94
- else
95
- CassandraThrift::ColumnPathOrParent.new(:column_family => column_family, :column => column)
96
- end
97
- @client.remove(@keyspace, key, column_path_or_parent, timestamp, consistency)
122
+
123
+ # Remove all rows in the column family you request. Supports options
124
+ # <tt>:consistency</tt> and <tt>:timestamp</tt>.
125
+ # FIXME May not currently delete all records without multiple calls. Waiting
126
+ # for ranged remove support in Cassandra.
127
+ def clear_column_family!(column_family, options = {})
128
+ get_range(column_family).each { |key| remove(column_family, key, options) }
98
129
  end
99
-
100
- public
101
-
102
- # Remove all rows in the column family you request.
103
- def clear_column_family!(column_family)
104
- # Does not support consistency argument
105
- get_key_range(column_family).each do |key|
106
- remove(column_family, key)
107
- end
130
+
131
+ # Remove all rows in the keyspace. Supports options <tt>:consistency</tt> and
132
+ # <tt>:timestamp</tt>.
133
+ # FIXME May not currently delete all records without multiple calls. Waiting
134
+ # for ranged remove support in Cassandra.
135
+ def clear_keyspace!(options = {})
136
+ @schema.keys.each { |column_family| clear_column_family!(column_family, options) }
108
137
  end
109
138
 
110
- # Remove all rows in the keyspace
111
- def clear_keyspace!
112
- # Does not support consistency argument
113
- @schema.keys.each do |column_family|
114
- clear_column_family!(column_family)
115
- end
139
+ ### Read
140
+
141
+ # Count the elements at the column_family:key:[super_column] path you
142
+ # request. Supports options <tt>:count</tt>, <tt>:start</tt>, <tt>:finish</tt>,
143
+ # <tt>:reversed</tt>, and <tt>:consistency</tt>.
144
+ def count_columns(column_family, key, *columns_and_options)
145
+ column_family, super_column, _, options = params(column_family, columns_and_options, READ_DEFAULTS)
146
+ _count_columns(column_family, key, super_column, options[:consistency])
116
147
  end
117
-
118
- ## Read
119
-
120
- # Count the elements at the column_family:key:[super_column] path you
121
- # request.
122
- def count_columns(column_family, key, super_column = nil, consistency = Consistency::WEAK)
123
- column_family = column_family.to_s
124
- assert_column_name_classes(column_family, super_column)
125
-
126
- super_column = super_column.to_s if super_column
127
- @client.get_column_count(@keyspace, key,
128
- CassandraThrift::ColumnParent.new(:column_family => column_family, :super_column => super_column),
129
- consistency
130
- )
148
+
149
+ # Multi-key version of Cassandra#count_columns. Supports options <tt>:count</tt>,
150
+ # <tt>:start</tt>, <tt>:finish</tt>, <tt>:reversed</tt>, and <tt>:consistency</tt>.
151
+ def multi_count_columns(column_family, keys, *options)
152
+ OrderedHash[*keys.map { |key| [key, count_columns(column_family, key, *options)] }._flatten_once]
131
153
  end
132
-
133
- # Multi-key version of Cassandra#count_columns.
134
- def multi_count_columns(column_family, keys, super_column = nil, consistency = Consistency::WEAK)
135
- OrderedHash[*keys.map do |key|
136
- [key, count_columns(column_family, key, super_column)]
137
- end._flatten_once]
138
- end
139
-
154
+
140
155
  # Return a list of single values for the elements at the
141
- # column_family:key:column[s]:[sub_columns] path you request.
142
- def get_columns(column_family, key, columns, sub_columns = nil, consistency = Consistency::WEAK)
143
- column_family = column_family.to_s
144
- assert_column_name_classes(column_family, columns, sub_columns)
145
-
146
- result = if is_super(column_family)
147
- if sub_columns
148
- columns_to_hash(column_family, @client.get_slice_by_names(@keyspace, key,
149
- CassandraThrift::ColumnParent.new(:column_family => column_family, :super_column => columns),
150
- sub_columns, consistency))
151
- else
152
- columns_to_hash(column_family, @client.get_slice_super_by_names(@keyspace, key, column_family, columns, consistency))
153
- end
154
- else
155
- columns_to_hash(column_family, @client.get_slice_by_names(@keyspace, key,
156
- CassandraThrift::ColumnParent.new(:column_family => column_family), columns, consistency))
157
- end
158
- sub_columns || columns.map { |name| result[name] }
156
+ # column_family:key:column[s]:[sub_columns] path you request. Supports the
157
+ # <tt>:consistency</tt> option.
158
+ def get_columns(column_family, key, *columns_and_options)
159
+ column_family, columns, sub_columns, options = params(column_family, columns_and_options, READ_DEFAULTS)
160
+ _get_columns(column_family, key, columns, sub_columns, options[:consistency])
159
161
  end
160
162
 
161
- # Multi-key version of Cassandra#get_columns.
162
- def multi_get_columns(column_family, keys, columns, sub_columns = nil, consistency = Consistency::WEAK)
163
- OrderedHash[*keys.map do |key|
164
- [key, get_columns(column_family, key, columns, sub_columns, consistency)]
165
- end._flatten_once]
163
+ # Multi-key version of Cassandra#get_columns. Supports the <tt>:consistency</tt>
164
+ # option.
165
+ def multi_get_columns(column_family, keys, *options)
166
+ OrderedHash[*keys.map { |key| [key, get_columns(column_family, key, *options)] }._flatten_once]
166
167
  end
167
-
168
- # Return a hash (actually, a Cassandra::OrderedHash) or a single value
168
+
169
+ # Return a hash (actually, a Cassandra::OrderedHash) or a single value
169
170
  # representing the element at the column_family:key:[column]:[sub_column]
170
- # path you request.
171
- def get(column_family, key, column = nil, sub_column = nil, limit = 100, column_range = ''..'', reversed = false, consistency = Consistency::WEAK)
172
- column_family = column_family.to_s
173
- assert_column_name_classes(column_family, column, sub_column)
174
- _get(column_family, key, column, sub_column, limit = 100, column_range, reversed, consistency)
171
+ # path you request. Supports options <tt>:count</tt>, <tt>:start</tt>,
172
+ # <tt>:finish</tt>, <tt>:reversed</tt>, and <tt>:consistency</tt>.
173
+ def get(column_family, key, *columns_and_options)
174
+ column_family, column, sub_column, options = params(column_family, columns_and_options, READ_DEFAULTS)
175
+ _get(column_family, key, column, sub_column, options[:count], options[:start], options[:finish], options[:reversed], options[:consistency])
175
176
  rescue CassandraThrift::NotFoundException
176
177
  is_super(column_family) && !sub_column ? OrderedHash.new : nil
177
178
  end
178
179
 
179
- # Multi-key version of Cassandra#get.
180
- def multi_get(column_family, keys, column = nil, sub_column = nil, limit = 100, column_range = ''..'', reversed = false, consistency = Consistency::WEAK)
181
- OrderedHash[*keys.map do |key|
182
- [key, get(column_family, key, column, sub_column, limit, column_range, reversed, consistency)]
183
- end._flatten_once]
180
+ # Multi-key version of Cassandra#get. Supports options <tt>:count</tt>,
181
+ # <tt>:start</tt>, <tt>:finish</tt>, <tt>:reversed</tt>, and <tt>:consistency</tt>.
182
+ def multi_get(column_family, keys, *options)
183
+ OrderedHash[*keys.map { |key| [key, get(column_family, key, *options)] }._flatten_once]
184
184
  end
185
-
186
- # Return true if the column_family:key:[column]:[sub_column] path you
187
- # request exists.
188
- def exists?(column_family, key, column = nil, sub_column = nil, consistency = Consistency::WEAK)
189
- column_family = column_family.to_s
190
- assert_column_name_classes(column_family, column, sub_column)
191
- _get(column_family, key, column, sub_column, 1, ''..'', false, consistency)
185
+
186
+ # Return true if the column_family:key:[column]:[sub_column] path you
187
+ # request exists. Supports the <tt>:consistency</tt> option.
188
+ def exists?(column_family, key, *columns_and_options)
189
+ column_family, column, sub_column, options = params(column_family, columns_and_options, READ_DEFAULTS)
190
+ _get(column_family, key, column, sub_column, 1, nil, nil, nil, options[:consistency])
192
191
  true
193
192
  rescue CassandraThrift::NotFoundException
194
193
  end
195
-
196
- private
197
-
198
- def _get(column_family, key, column = nil, sub_column = nil, limit = 100, column_range = ''..'', reversed = false, consistency = Consistency::WEAK)
199
- column = column.to_s if column
200
- sub_column = sub_column.to_s if sub_column
201
- # FIXME Comparable types in a are not checked
202
- column_range = (column_range.begin.to_s)..(column_range.end.to_s)
203
-
204
- # You have got to be kidding
205
- if is_super(column_family)
206
- if sub_column
207
- # Limit and column_range parameters have no effect
208
- load(@client.get_column(@keyspace, key,
209
- CassandraThrift::ColumnPath.new(:column_family => column_family, :super_column => column, :column => sub_column),
210
- consistency).value)
211
- elsif column
212
- sub_columns_to_hash(column_family,
213
- @client.get_slice(@keyspace, key,
214
- CassandraThrift::ColumnParent.new(:column_family => column_family, :super_column => column),
215
- column_range.begin, column_range.end, !reversed, limit, consistency))
216
- else
217
- columns_to_hash(column_family,
218
- @client.get_slice_super(@keyspace, key, column_family, column_range.begin, column_range.end, !reversed, limit, consistency))
219
- end
220
- else
221
- if column
222
- # Limit and column_range parameters have no effect
223
- load(@client.get_column(@keyspace, key,
224
- CassandraThrift::ColumnPath.new(:column_family => column_family, :column => column),
225
- consistency).value)
226
- else
227
- columns_to_hash(column_family,
228
- @client.get_slice(@keyspace, key,
229
- CassandraThrift::ColumnParent.new(:column_family => column_family),
230
- column_range.begin, column_range.end, !reversed, limit, consistency))
231
- end
232
- end
233
- end
234
-
235
- public
236
-
194
+
237
195
  # Return a list of keys in the column_family you request. Requires the
238
- # table to be partitioned with OrderPreservingHash.
239
- def get_key_range(column_family, key_range = ''..'', limit = 100, consistency = Consistency::WEAK)
240
- column_family = column_family.to_s
241
- @client.get_key_range(@keyspace, column_family, key_range.begin, key_range.end, limit)
196
+ # table to be partitioned with OrderPreservingHash. Supports the
197
+ # <tt>:count</tt>, <tt>:start</tt>, <tt>:finish</tt>, and <tt>:consistency</tt>
198
+ # options.
199
+ def get_range(column_family, options = {})
200
+ column_family, _, _, options = params(column_family, [options], READ_DEFAULTS)
201
+ _get_range(column_family, options[:start], options[:finish], options[:count], options[:consistency])
242
202
  end
243
-
244
- # Count all rows in the column_family you request. Requires the table
245
- # to be partitioned with OrderPreservingHash.
246
- def count(column_family, key_range = ''..'', limit = MAX_INT, consistency = Consistency::WEAK)
247
- get_key_range(column_family, key_range, limit, consistency).size
203
+
204
+ # Count all rows in the column_family you request. Requires the table
205
+ # to be partitioned with OrderPreservingHash. Supports the <tt>:start</tt>,
206
+ # <tt>:finish</tt>, and <tt>:consistency</tt> options.
207
+ # FIXME will count only MAX_INT records
208
+ def count_range(column_family, options = {})
209
+ get_range(column_family, options.merge(:count => MAX_INT)).size
248
210
  end
249
-
211
+
212
+ # Open a batch operation and yield. Inserts and deletes will be queued until
213
+ # the block closes, and then sent atomically to the server.
214
+ # FIXME Make deletes truly atomic.
250
215
  def batch
251
216
  @batch = []
252
- yield
253
- compact_mutations!
254
- dispatch_mutations!
217
+ yield
218
+ compact_mutations
219
+ dispatch_mutations
255
220
  @batch = nil
256
221
  end
257
222
 
258
223
  private
224
+
225
+ # Extract and validate options.
226
+ def params(column_family, args, options)
227
+ if args.last.is_a?(Hash)
228
+ if (extras = args.last.keys - options.keys).any?
229
+ this = "#{self.class}##{caller[0].split('`').last[0..-2]}"
230
+ raise ArgumentError, "Invalid options #{extras.inspect[1..-2]} for #{this}"
231
+ end
232
+ options = options.merge(args.pop)
233
+ end
259
234
 
260
- def compact_mutations!
235
+ column_family, column, sub_column = column_family.to_s, args[0], args[1]
236
+ assert_column_name_classes(column_family, column, sub_column)
237
+ [column_family, map_to_s(column), map_to_s(sub_column), options]
238
+ end
239
+
240
+ # Convert stuff to strings.
241
+ def map_to_s(el)
242
+ case el
243
+ when NilClass # nil
244
+ when Array then el.map { |i| map_to_s(i) }
245
+ when Comparable, String, Symbol then el.to_s
246
+ else
247
+ raise Comparable::TypeError, "Can't map #{el.inspect}"
248
+ end
249
+ end
250
+
251
+ # Roll up queued mutations as much as possible, to improve atomicity.
252
+ def compact_mutations
261
253
  compact_batch = []
262
- mutations = {}
254
+ mutations = {}
263
255
 
264
256
  @batch << nil # Close it
265
257
  @batch.each do |m|
@@ -269,7 +261,7 @@ class Cassandra
269
261
  compact_batch.concat(mutations.values.map {|x| x.values}.flatten)
270
262
  mutations = {}
271
263
  # Insert delete operation
272
- compact_batch << m
264
+ compact_batch << m
273
265
  else # BatchMutation, BatchMutationSuper
274
266
  # Do a nested hash merge
275
267
  if mutation_class = mutations[m.class]
@@ -287,29 +279,20 @@ class Cassandra
287
279
  end
288
280
  end
289
281
  end
290
-
282
+
291
283
  @batch = compact_batch
292
284
  end
293
-
294
- def dispatch_mutations!
295
- @batch.each do |args|
296
- case args
297
- when Array
298
- _remove(*args)
299
- when CassandraThrift::BatchMutationSuper, CassandraThrift::BatchMutation
285
+
286
+ # Send all the queued mutations to the server.
287
+ def dispatch_mutations
288
+ @batch.compact!
289
+ @batch.each do |args|
290
+ case args.first
291
+ when CassandraThrift::BatchMutationSuper, CassandraThrift::BatchMutation
300
292
  _insert(*args)
293
+ else
294
+ _remove(*args)
301
295
  end
302
296
  end
303
297
  end
304
-
305
- def dump(object)
306
- # Special-case nil as the empty byte array
307
- return "" if object == nil
308
- @serializer.dump(object)
309
- end
310
-
311
- def load(object)
312
- return nil if object == ""
313
- @serializer.load(object)
314
- end
315
298
  end