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 +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 [](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
|