cassandra-cql 1.0.4 → 1.1.0

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/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ services:
3
+ - cassandra
4
+ rvm:
5
+ - "1.8.7"
6
+ - "1.9.2"
7
+ - "1.9.3"
8
+ # - jruby-18mode # JRuby in 1.8 mode
9
+ # - jruby-19mode # JRuby in 1.9 mode
10
+ - ree
11
+ # - rbx-18mode
12
+ # - rbx-19mode
13
+ # uncomment this line if your project needs to run something other than `rake`:
14
+ # script: bundle exec rspec spec
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Cassandra [![Build Status](https://secure.travis-ci.org/kreynolds/cassandra-cql.png)](http://travis-ci.org/kreynolds/cassandra-cql)
2
+ The Apache Cassandra Project (http://cassandra.apache.org) develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model.
3
+
4
+ # CQL
5
+ Cassandra originally went with a Thrift RPC-based API as a way to provide a common denominator that more idiomatic clients could build upon independently.
6
+ However, this worked poorly in practice: raw Thrift is too low-level to use productively, and keeping pace with new API methods to support (for example) indexes in 0.7 or distributed counters in 0.8 is too much for many maintainers.
7
+
8
+ CQL, the Cassandra Query Language, addresses this by pushing all implementation details to the server; all the client has to know for any operation is how to interpret "resultset" objects.
9
+ So adding a feature like counters just requires teaching the CQL parser to understand "column + N" notation; no client-side changes are necessary.
10
+
11
+ (CQL Specification: http://cassandra.apache.org/doc/cql/CQL.html)
12
+
13
+ # Quick Start
14
+
15
+ ## Establishing a connection
16
+
17
+ # Defaults to the system keyspace
18
+ db = CassandraCQL::Database.new('127.0.0.1:9160')
19
+
20
+ # Specifying a keyspace
21
+ db = CassandraCQL::Database.new('127.0.0.1:9160', {:keyspace => 'keyspace1'})
22
+
23
+ # Specifying more than one seed node
24
+ db = CassandraCQL::Database.new(['127.0.0.1:9160','127.0.0.2:9160'])
25
+
26
+ ## Creating a Keyspace
27
+
28
+ # Creating a simple keyspace with replication factor 1
29
+ db.execute("CREATE KEYSPACE keyspace1 WITH strategy_class='org.apache.cassandra.locator.SimpleStrategy' AND strategy_options:replication_factor=1")
30
+ db.execute("USE keyspace1")
31
+
32
+ ## Creating a Column Family
33
+
34
+ # Creating a column family with a single validated column
35
+ db.execute("CREATE COLUMNFAMILY users (id varchar PRIMARY KEY, email varchar)")
36
+
37
+ # Create an index on the name
38
+ db.execute("CREATE INDEX users_email_idx ON users (email)")
39
+
40
+ ## Inserting into a Column Family
41
+
42
+ # Insert without bound variables
43
+ db.execute("INSERT INTO users (id, email) VALUES ('kreynolds', 'kelley@insidesystems.net')")
44
+
45
+ # Insert with bound variables
46
+ db.execute("INSERT INTO users (id, email) VALUES (?, ?)", 'kway', 'kevin@insidesystems.net')
47
+
48
+ ## Updating a Column Family
49
+
50
+ # Update
51
+ db.execute("UPDATE users SET email=? WHERE id=?", 'kreynolds@insidesystems.net', 'kreynolds')
52
+
53
+ ## Selecting from a Column Family
54
+
55
+ # Select all
56
+ db.execute("SELECT * FROM users").fetch { |row| puts row.to_hash.inspect }
57
+ {"id"=>"kway", "email"=>"kevin@insidesystems.net"}
58
+ {"id"=>"kreynolds", "email"=>"kreynolds@insidesystems.net"}
59
+
60
+ # Select just one user by id
61
+ db.execute("SELECT * FROM users WHERE id=?", 'kreynolds').fetch { |row| puts row.to_hash.inspect }
62
+ {"id"=>"kreynolds", "email"=>"kreynolds@insidesystems.net"}
63
+
64
+ # Select just one user by indexed column
65
+ db.execute("SELECT * FROM users WHERE email=?", 'kreynolds@insidesystems.net').fetch { |row| puts row.to_hash.inspect }
66
+ {"id"=>"kreynolds", "email"=>"kreynolds@insidesystems.net"}
67
+
68
+ ## Deleting from a Column Family
69
+
70
+ # Delete the swarthy bastard Kevin
71
+ db.execute("DELETE FROM users WHERE id=?", 'kway')
72
+
73
+ # Notes
74
+
75
+ ## Changing Validation on Columns with existing/unvalidatable data
76
+
77
+ If you have existing data and change the validation on a column in an incompatible
78
+ way (ie. blank strings with a column validated as Integer), a CastException will be raised.
79
+ The exception has a 'bytes' attribute that will give you access to the bytes that caused the problem.
80
+
81
+ Other columns in a row can still be accessible via index or column_name without raising that exception.
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.version = CassandraCQL::VERSION
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Kelley Reynolds"]
9
- s.email = ["kelley@insidesystems.net"]
9
+ s.email = ["kelley.reynolds@rubyscale.com"]
10
10
  s.homepage = "http://code.google.com/a/apache-extras.org/p/cassandra-ruby/"
11
11
  s.summary = "CQL Interface to Cassandra"
12
12
  s.description = "CQL Interface to Cassandra"
@@ -15,7 +15,12 @@ Gem::Specification.new do |s|
15
15
  s.rubyforge_project = "cassandra-cql"
16
16
 
17
17
  s.add_development_dependency "bundler", ">= 1.0.0"
18
- s.add_development_dependency "rcov", ">= 0.9.9"
18
+
19
+ if RUBY_VERSION >= "1.9"
20
+ s.add_development_dependency "simplecov"
21
+ else
22
+ s.add_development_dependency "rcov", ">= 0.9.9"
23
+ end
19
24
  s.add_development_dependency "rspec", ">= 2.6.0"
20
25
  s.add_development_dependency "rake", ">= 0.9.2"
21
26
  s.add_development_dependency "yard", ">= 0.7.2"
data/lib/cassandra-cql.rb CHANGED
@@ -16,7 +16,7 @@ limitations under the License.
16
16
 
17
17
  module CassandraCQL; end;
18
18
  unless CassandraCQL.respond_to?(:CASSANDRA_VERSION)
19
- require "cassandra-cql/1.0"
19
+ require "cassandra-cql/1.1"
20
20
  end
21
21
 
22
22
  here = File.expand_path(File.dirname(__FILE__))
@@ -0,0 +1,7 @@
1
+ module CassandraCQL
2
+ def self.CASSANDRA_VERSION
3
+ "1.1"
4
+ end
5
+ end
6
+
7
+ require "#{File.expand_path(File.dirname(__FILE__))}/../cassandra-cql"
@@ -0,0 +1,6 @@
1
+ module CassandraCQL
2
+ module V11
3
+ class Result < CassandraCQL::Result
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module CassandraCQL
2
+ module V11
3
+ class Statement < CassandraCQL::Statement
4
+ end
5
+ end
6
+ end
@@ -32,6 +32,7 @@ module CassandraCQL
32
32
  }.merge(thrift_client_options)
33
33
 
34
34
  @keyspace = @options[:keyspace]
35
+ @cql_version = @options[:cql_version]
35
36
  @servers = servers
36
37
  connect!
37
38
  execute("USE #{@keyspace}")
@@ -41,6 +42,7 @@ module CassandraCQL
41
42
  @connection = ThriftClient.new(CassandraCQL::Thrift::Client, @servers, @thrift_client_options)
42
43
  obj = self
43
44
  @connection.add_callback(:post_connect) do
45
+ @connection.set_cql_version(@cql_version) if @cql_version
44
46
  execute("USE #{@keyspace}")
45
47
  @connection.login(@auth_request) if @auth_request
46
48
  end
@@ -104,10 +104,12 @@ module CassandraCQL
104
104
  end
105
105
  end
106
106
  else
107
- if (row = fetch_row).kind_of?(Fixnum)
108
- {row => row}
109
- else
110
- row.to_hash
107
+ if row = fetch_row
108
+ if row.kind_of?(Fixnum)
109
+ {row => row}
110
+ else
111
+ row.to_hash
112
+ end
111
113
  end
112
114
  end
113
115
  end
@@ -130,4 +132,4 @@ module CassandraCQL
130
132
  end
131
133
  end
132
134
  end
133
- end
135
+ end
@@ -20,12 +20,27 @@ module CassandraCQL
20
20
 
21
21
  def initialize(row, schema)
22
22
  @row, @schema = row, schema
23
+ @value_cache = Hash.new { |h, key|
24
+ # If it's a number and not one of our columns, assume it's an index
25
+ if key.kind_of?(Fixnum) and !column_names.include?(key)
26
+ column_name = column_names[key]
27
+ column_index = key
28
+ else
29
+ column_name = key
30
+ column_index = column_names.index(key)
31
+ end
32
+
33
+ if column_index.nil?
34
+ # Cache negative hits
35
+ h[column_name] = nil
36
+ else
37
+ h[column_name] = ColumnFamily.cast(@row.columns[column_index].value, @schema.values[@row.columns[column_index].name])
38
+ end
39
+ }
23
40
  end
24
41
 
25
42
  def [](obj)
26
- column_index = obj.kind_of?(Fixnum) ? obj : column_names.index(obj)
27
- return nil if column_index.nil?
28
- column_values[column_index]
43
+ @value_cache[obj]
29
44
  end
30
45
 
31
46
  def column_names
@@ -35,7 +50,9 @@ module CassandraCQL
35
50
  end
36
51
 
37
52
  def column_values
38
- @values ||= @row.columns.map { |column| ColumnFamily.cast(column.value, @schema.values[column.name]) }
53
+ @row.columns.map { |column|
54
+ @value_cache[column.name]
55
+ }
39
56
  end
40
57
 
41
58
  def columns
@@ -74,7 +74,7 @@ module CassandraCQL
74
74
  obj.map { |member| quote(member) }.join(",")
75
75
  elsif obj.kind_of?(String)
76
76
  "'" + obj + "'"
77
- elsif obj.kind_of?(Fixnum) or obj.kind_of?(Float)
77
+ elsif obj.kind_of?(Numeric)
78
78
  obj
79
79
  else
80
80
  raise Error::UnescapableObject, "Unable to escape object of class #{obj.class}"
@@ -15,6 +15,17 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  module CassandraCQL
18
+ module Error
19
+ class CastException < Exception
20
+ attr_reader :bytes
21
+
22
+ def initialize(message = nil, bytes = nil)
23
+ super(message)
24
+ @bytes = bytes
25
+ end
26
+ end
27
+ end
28
+
18
29
  module Types
19
30
  class AbstractType
20
31
  def self.cast(value)
@@ -31,6 +42,8 @@ module CassandraCQL
31
42
  int = int - (1 << bytes.length * 8)
32
43
  end
33
44
  int
45
+ rescue
46
+ raise Error::CastException.new("Unable to convert bytes to int", bytes)
34
47
  end
35
48
 
36
49
  def self.bytes_to_long(bytes)
@@ -41,6 +54,8 @@ module CassandraCQL
41
54
  else
42
55
  val
43
56
  end
57
+ rescue
58
+ raise Error::CastException.new("Unable to convert bytes to long", bytes)
44
59
  end
45
60
  end
46
61
  end
@@ -18,7 +18,9 @@ module CassandraCQL
18
18
  module Types
19
19
  class AsciiType < AbstractType
20
20
  def self.cast(value)
21
- RUBY_VERSION >= "1.9" ? value.to_s.dup.force_encoding('ASCII-8BIT') : value.to_s.dup
21
+ RUBY_VERSION >= "1.9" ? value.to_s.dup.force_encoding('US-ASCII') : value.to_s.dup
22
+ rescue
23
+ raise Error::CastException.new("Unable to convert bytes to ascii", value)
22
24
  end
23
25
  end
24
26
  end
@@ -18,7 +18,12 @@ module CassandraCQL
18
18
  module Types
19
19
  class BooleanType < AbstractType
20
20
  def self.cast(value)
21
+ # Do not assume that an empty string is false
22
+ raise if value.empty?
23
+
21
24
  value.unpack('C') == [1]
25
+ rescue
26
+ raise Error::CastException.new("Unable to convert bytes to boolean", value)
22
27
  end
23
28
  end
24
29
  end
@@ -19,6 +19,8 @@ module CassandraCQL
19
19
  class DateType < AbstractType
20
20
  def self.cast(value)
21
21
  Time.at(bytes_to_long(value) / 1000.0)
22
+ rescue
23
+ raise Error::CastException.new("Unable to convert bytes to Date", value)
22
24
  end
23
25
  end
24
26
  end
@@ -19,6 +19,8 @@ module CassandraCQL
19
19
  class DecimalType < AbstractType
20
20
  def self.cast(value)
21
21
  BigDecimal.new(bytes_to_int(value[4..-1]).to_s) * BigDecimal.new('10')**(bytes_to_int(value[0..3])*-1)
22
+ rescue
23
+ raise Error::CastException.new("Unable to convert bytes to decimal", value)
22
24
  end
23
25
  end
24
26
  end
@@ -18,7 +18,12 @@ module CassandraCQL
18
18
  module Types
19
19
  class DoubleType < AbstractType
20
20
  def self.cast(value)
21
+ # Do not return nil, this should be an error
22
+ raise if value.empty?
23
+
21
24
  value.unpack('G')[0]
25
+ rescue
26
+ raise Error::CastException.new("Unable to convert bytes to double", value)
22
27
  end
23
28
  end
24
29
  end
@@ -18,7 +18,12 @@ module CassandraCQL
18
18
  module Types
19
19
  class FloatType < AbstractType
20
20
  def self.cast(value)
21
+ # Do not return nil, this should be an error
22
+ raise if value.empty?
23
+
21
24
  value.unpack('g')[0]
25
+ rescue
26
+ raise Error::CastException.new("Unable to convert bytes to float", value)
22
27
  end
23
28
  end
24
29
  end
@@ -19,6 +19,8 @@ module CassandraCQL
19
19
  class UTF8Type < AbstractType
20
20
  def self.cast(value)
21
21
  RUBY_VERSION >= "1.9" ? value.to_s.dup.force_encoding('UTF-8') : value.to_s.dup
22
+ rescue => e
23
+ raise CassandraCQL::Error::CastException, "Unable to convert bytes to UTF8: #{bytes.inspect}", caller
22
24
  end
23
25
  end
24
26
  end
@@ -19,6 +19,8 @@ module CassandraCQL
19
19
  class UUIDType < AbstractType
20
20
  def self.cast(value)
21
21
  UUID.new(value)
22
+ rescue => e
23
+ raise CassandraCQL::Error::CastException, "Unable to convert bytes to UUID: #{value.inspect}", caller
22
24
  end
23
25
  end
24
26
 
@@ -15,5 +15,5 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  module CassandraCQL
18
- VERSION = "1.0.4"
18
+ VERSION = "1.1.0"
19
19
  end
@@ -62,7 +62,7 @@ describe "ColumnFamily class" do
62
62
 
63
63
  it "should turn a packed long into a number" do
64
64
  number = 2**33
65
- packed = [number >> 32, number].pack("N*")
65
+ packed = [number >> 32, number % 2**32].pack("N*")
66
66
 
67
67
  ColumnFamily.cast(packed, "LongType").should eq(number)
68
68
  ColumnFamily.cast(packed, "CounterColumnType").should eq(number)
@@ -70,7 +70,7 @@ describe "ColumnFamily class" do
70
70
 
71
71
  it "should turn a packed negative long into a negative number" do
72
72
  number = -2**33
73
- packed = [number >> 32, number].pack("N*")
73
+ packed = [number >> 32, number % 2**32].pack("N*")
74
74
 
75
75
  ColumnFamily.cast(packed, "LongType").should eq(number)
76
76
  ColumnFamily.cast(packed, "CounterColumnType").should eq(number)
data/spec/misc_spec.rb ADDED
@@ -0,0 +1,68 @@
1
+ require File.expand_path('spec_helper.rb', File.dirname(__FILE__))
2
+ include CassandraCQL
3
+
4
+ describe "Miscellaneous tests that handle specific failures/regressions" do
5
+ before(:each) do
6
+ @connection = setup_cassandra_connection
7
+ @connection.execute("DROP COLUMNFAMILY misc_tests") if @connection.schema.column_family_names.include?('misc_tests')
8
+ @connection.execute("CREATE COLUMNFAMILY misc_tests (id text PRIMARY KEY)")
9
+ end
10
+
11
+ context "with ascii validation" do
12
+ before(:each) do
13
+ @connection.execute("ALTER COLUMNFAMILY misc_tests ADD test_column ascii")
14
+ end
15
+
16
+ it "should be consistent with ascii-encoded text" do
17
+ @connection.execute("INSERT INTO misc_tests (id, test_column) VALUES (?, ?)", 'test', 'test_column').should be_nil
18
+ row = @connection.execute("SELECT test_column FROM misc_tests WHERE id=?", 'test').fetch
19
+ row['test_column'].should eq 'test_column'
20
+ @connection.execute("INSERT INTO misc_tests (id, test_column) VALUES (?, ?)", 'test', row['test_column']).should be_nil
21
+ row = @connection.execute("SELECT test_column FROM misc_tests WHERE id=?", 'test').fetch
22
+ row['test_column'].should eq 'test_column'
23
+ end
24
+ end
25
+
26
+ context "with unvalidatable data" do
27
+ before(:each) do
28
+ @connection.execute("INSERT INTO misc_tests (id, good_column, bad_column) VALUES (?, ?, ?)", 'test', 'blah', '')
29
+ @connection.execute("ALTER COLUMNFAMILY misc_tests ADD bad_column int")
30
+ @row = @connection.execute("SELECT good_column, bad_column FROM misc_tests WHERE id=?", 'test').fetch
31
+ end
32
+
33
+ it "should have valid column_names" do
34
+ @row.column_names.should eq ['good_column', 'bad_column']
35
+ end
36
+
37
+ it "should have raise an exception with column_values" do
38
+ expect { @row.column_values }.to raise_error(Error::CastException)
39
+ end
40
+
41
+ it "should be able to fetch good columns even with bad columns in the row" do
42
+ @row['good_column'].should eq 'blah'
43
+ @row[0].should eq 'blah'
44
+ end
45
+
46
+ it "should only cast a column once regardless of it's access method" do
47
+ expect {
48
+ @row['good_column'].should eq 'blah'
49
+ @row[0].should eq 'blah'
50
+ }.to change {
51
+ @row.instance_variable_get(:@value_cache).size
52
+ }.by 1
53
+ end
54
+
55
+ it "should throw an error trying to fetch the bad column by name" do
56
+ expect { @row['bad_column'] }.to raise_error(Error::CastException)
57
+ end
58
+
59
+ it "should throw an error trying to fetch the bad column by index" do
60
+ expect { @row[1] }.to raise_error(Error::CastException)
61
+ end
62
+
63
+ it "the raw bytes should be accessible for repair" do
64
+ @row.row.columns[1].name.should eq 'bad_column'
65
+ @row.row.columns[1].value.should eq ''
66
+ end
67
+ end
68
+ end