cassandra 0.4 → 0.5

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.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