cassandra-cql 1.0.4 → 1.1.0

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