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 +14 -0
- data/README.md +81 -0
- data/cassandra-cql.gemspec +7 -2
- data/lib/cassandra-cql.rb +1 -1
- data/lib/cassandra-cql/1.1.rb +7 -0
- data/lib/cassandra-cql/1.1/result.rb +6 -0
- data/lib/cassandra-cql/1.1/statement.rb +6 -0
- data/lib/cassandra-cql/database.rb +2 -0
- data/lib/cassandra-cql/result.rb +7 -5
- data/lib/cassandra-cql/row.rb +21 -4
- data/lib/cassandra-cql/statement.rb +1 -1
- data/lib/cassandra-cql/types/abstract_type.rb +15 -0
- data/lib/cassandra-cql/types/ascii_type.rb +3 -1
- data/lib/cassandra-cql/types/boolean_type.rb +5 -0
- data/lib/cassandra-cql/types/date_type.rb +2 -0
- data/lib/cassandra-cql/types/decimal_type.rb +2 -0
- data/lib/cassandra-cql/types/double_type.rb +5 -0
- data/lib/cassandra-cql/types/float_type.rb +5 -0
- data/lib/cassandra-cql/types/utf8_type.rb +2 -0
- data/lib/cassandra-cql/types/uuid_type.rb +2 -0
- data/lib/cassandra-cql/version.rb +1 -1
- data/spec/column_family_spec.rb +2 -2
- data/spec/misc_spec.rb +68 -0
- data/spec/result_spec.rb +6 -1
- data/spec/spec_helper.rb +8 -2
- data/spec/uuid_spec.rb +3 -1
- data/spec/validation_spec.rb +71 -21
- data/vendor/1.1/gen-rb/cassandra.rb +2511 -0
- data/vendor/1.1/gen-rb/cassandra_constants.rb +13 -0
- data/vendor/1.1/gen-rb/cassandra_types.rb +928 -0
- metadata +67 -21
- data/README.rdoc +0 -71
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.
|
data/cassandra-cql.gemspec
CHANGED
@@ -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@
|
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
|
-
|
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
@@ -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
|
data/lib/cassandra-cql/result.rb
CHANGED
@@ -104,10 +104,12 @@ module CassandraCQL
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
else
|
107
|
-
if
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
data/lib/cassandra-cql/row.rb
CHANGED
@@ -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
|
-
|
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
|
-
@
|
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?(
|
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
|
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 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
|
data/spec/column_family_spec.rb
CHANGED
@@ -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
|