cassandra_record 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -4
- data/Gemfile.lock +3 -5
- data/README.md +55 -3
- data/lib/cassandra_record.rb +3 -0
- data/lib/cassandra_record/base.rb +44 -9
- data/lib/cassandra_record/database/adapters/cassandra.rb +27 -2
- data/lib/cassandra_record/statement.rb +7 -7
- data/lib/cassandra_record/version.rb +1 -1
- data/spec/integration/cassandra_record/base_spec.rb +104 -2
- data/spec/spec_helper.rb +2 -1
- data/spec/unit/cassandra_record/database/adapters/cassandra_spec.rb +46 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f7f7196553b6334084a1b9cbdc32fcc741c7234
|
4
|
+
data.tar.gz: 0d387f3cbb15897d9f585c52d722a4fe31dd124b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1b89fe70793c22999b40a738ef9ba09b868ffcd6fc91ea1099384a90de9214d5288068d83499a37aa5df73eb056626ab1c8cf1d4df43110eb8e40fe53b152e8
|
7
|
+
data.tar.gz: 0a28d9c85a751abbafb5642495bf751fca95e8beeb2a154bfb9399bc53067eec0e946bbefa0edd6113467160cadc6ef57c23d9f08ad35d57457dfc4d5ca1a410
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cassandra_record (0.0
|
4
|
+
cassandra_record (0.1.0)
|
5
5
|
activesupport
|
6
6
|
cassandra-driver
|
7
7
|
|
@@ -17,8 +17,8 @@ GEM
|
|
17
17
|
byebug (2.7.0)
|
18
18
|
columnize (~> 0.3)
|
19
19
|
debugger-linecache (~> 1.2)
|
20
|
-
cassandra-driver (1.0
|
21
|
-
ione (~> 1.2
|
20
|
+
cassandra-driver (2.1.0)
|
21
|
+
ione (~> 1.2)
|
22
22
|
coderay (1.1.0)
|
23
23
|
columnize (0.8.9)
|
24
24
|
debugger-linecache (1.2.0)
|
@@ -61,9 +61,7 @@ PLATFORMS
|
|
61
61
|
ruby
|
62
62
|
|
63
63
|
DEPENDENCIES
|
64
|
-
activesupport
|
65
64
|
bundler (~> 1.7)
|
66
|
-
cassandra-driver
|
67
65
|
cassandra_record!
|
68
66
|
pry
|
69
67
|
pry-byebug
|
data/README.md
CHANGED
@@ -4,20 +4,70 @@
|
|
4
4
|
|
5
5
|
Add this line to your application's Gemfile:
|
6
6
|
|
7
|
-
```
|
7
|
+
```bash
|
8
8
|
gem 'cassandra_record'
|
9
9
|
```
|
10
10
|
|
11
11
|
And then execute:
|
12
12
|
|
13
|
-
|
13
|
+
```bash
|
14
|
+
$ bundle
|
15
|
+
```
|
14
16
|
|
15
17
|
Or install it yourself as:
|
16
18
|
|
17
|
-
|
19
|
+
```bash
|
20
|
+
$ gem install cassandra_record
|
21
|
+
```
|
18
22
|
|
19
23
|
## Usage
|
20
24
|
|
25
|
+
CassandraRecord models are based on the following Cassandra table:
|
26
|
+
|
27
|
+
```sql
|
28
|
+
CREATE TABLE thingies (
|
29
|
+
id int,
|
30
|
+
name text,
|
31
|
+
PRIMARY KEY (id)
|
32
|
+
);
|
33
|
+
```
|
34
|
+
|
35
|
+
__A simple Cassandra-backed model__
|
36
|
+
|
37
|
+
Define the model by inheriting from CassandraRecord::Base. ...and you're done. ...you're welcome
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class Thingy < CassandraRecord::Base
|
41
|
+
end
|
42
|
+
|
43
|
+
# record creation
|
44
|
+
Thingy.create(id: 123, name: 'pizza')
|
45
|
+
|
46
|
+
# record retrieval and attribute access
|
47
|
+
my_thingy = Thingy.where(id: 123)
|
48
|
+
my_thingy.name # => pizza
|
49
|
+
```
|
50
|
+
|
51
|
+
__A model with creation options__
|
52
|
+
|
53
|
+
Override the instance-level #create method.
|
54
|
+
Overriding the instance-level #create method will apply the configured options to all created records.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class Thingy < CassandraRecord::Base
|
58
|
+
TTL = 3600 # one hour
|
59
|
+
|
60
|
+
def create
|
61
|
+
options = { ttl: TTL }
|
62
|
+
super(options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# record creation
|
67
|
+
# this record will auto-expire in 1 hour.
|
68
|
+
Thingy.create(id: 123, name: 'spaghetti')
|
69
|
+
```
|
70
|
+
|
21
71
|
## Contributing
|
22
72
|
|
23
73
|
1. Fork it ( https://github.com/zephyr-dev/cassandra_record/fork )
|
@@ -25,3 +75,5 @@ Or install it yourself as:
|
|
25
75
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
26
76
|
4. Push to the branch (`git push origin my-new-feature`)
|
27
77
|
5. Create a new Pull Request
|
78
|
+
|
79
|
+
|
data/lib/cassandra_record.rb
CHANGED
@@ -9,9 +9,28 @@ module CassandraRecord
|
|
9
9
|
new(attributes).create
|
10
10
|
end
|
11
11
|
|
12
|
+
def batch_create(array_of_attributes, options={})
|
13
|
+
batch = configuration.database_adapter.session.batch do |batch|
|
14
|
+
array_of_attributes.map do |attr|
|
15
|
+
batch.add(new(attr).send(:insert_statement, attr, options), attr.values)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
configuration.database_adapter.session.execute(batch)
|
20
|
+
array_of_attributes.map { |attr| new(attr) }
|
21
|
+
end
|
22
|
+
|
12
23
|
def where(attributes={})
|
13
24
|
new.where(attributes)
|
14
25
|
end
|
26
|
+
|
27
|
+
def configure
|
28
|
+
yield configuration
|
29
|
+
end
|
30
|
+
|
31
|
+
def configuration
|
32
|
+
@@configuration ||= Configuration.new
|
33
|
+
end
|
15
34
|
end
|
16
35
|
|
17
36
|
attr_accessor :attributes
|
@@ -21,28 +40,36 @@ module CassandraRecord
|
|
21
40
|
end
|
22
41
|
|
23
42
|
def where(options={})
|
24
|
-
|
43
|
+
db.execute(where_statement(options)).map do |attributes|
|
25
44
|
self.class.new(attributes)
|
26
45
|
end
|
27
46
|
end
|
28
47
|
|
29
|
-
def create
|
30
|
-
|
48
|
+
def create(options={})
|
49
|
+
db.execute(insert_statement(attributes, options), *attributes.values)
|
31
50
|
self
|
32
51
|
end
|
33
52
|
|
34
53
|
private
|
35
54
|
|
36
|
-
def
|
37
|
-
|
55
|
+
def db
|
56
|
+
self.class.configuration.database_adapter
|
38
57
|
end
|
39
58
|
|
40
|
-
def
|
41
|
-
|
59
|
+
def where_statement(options={})
|
60
|
+
Statement.where(table_name, options)
|
42
61
|
end
|
43
62
|
|
44
|
-
def
|
45
|
-
attributes
|
63
|
+
def insert_statement(attributes, options={})
|
64
|
+
@insert_statement ||= db.prepare(insert_cql(attributes, options))
|
65
|
+
end
|
66
|
+
|
67
|
+
def insert_cql(attributes, options={})
|
68
|
+
Statement.create(table_name, attributes.keys, attributes.values, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_name
|
72
|
+
ActiveSupport::Inflector.tableize(self.class.name).gsub(/\//, '_')
|
46
73
|
end
|
47
74
|
|
48
75
|
def method_missing(method, *args, &block)
|
@@ -53,5 +80,13 @@ module CassandraRecord
|
|
53
80
|
end
|
54
81
|
end
|
55
82
|
|
83
|
+
class Configuration
|
84
|
+
attr_accessor :database_adapter
|
85
|
+
|
86
|
+
def initialize(adapter=Database::Adapters::Cassandra.instance)
|
87
|
+
adapter
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
56
91
|
end
|
57
92
|
end
|
@@ -16,11 +16,15 @@ module CassandraRecord
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def prepare(cql)
|
19
|
-
|
19
|
+
rescue_with_reset_and_retry do
|
20
|
+
session.prepare(cql)
|
21
|
+
end
|
20
22
|
end
|
21
23
|
|
22
24
|
def execute(cql, *args)
|
23
|
-
|
25
|
+
rescue_with_reset_and_retry do
|
26
|
+
session.execute(cql, arguments: args)
|
27
|
+
end
|
24
28
|
end
|
25
29
|
|
26
30
|
def cluster
|
@@ -38,6 +42,27 @@ module CassandraRecord
|
|
38
42
|
|
39
43
|
private
|
40
44
|
|
45
|
+
MAX_RETRIES = 4
|
46
|
+
|
47
|
+
def rescue_with_reset_and_retry
|
48
|
+
retry_count = 0
|
49
|
+
begin
|
50
|
+
yield
|
51
|
+
rescue ::Cassandra::Error
|
52
|
+
if (retry_count += 1) < MAX_RETRIES
|
53
|
+
reset_session
|
54
|
+
sleep(0.5)
|
55
|
+
retry
|
56
|
+
else
|
57
|
+
raise
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def reset_session
|
63
|
+
@session = nil
|
64
|
+
end
|
65
|
+
|
41
66
|
def cluster_connection
|
42
67
|
::Cassandra.cluster(connection_configuration.symbolize_keys)
|
43
68
|
end
|
@@ -5,28 +5,28 @@ module CassandraRecord
|
|
5
5
|
cql = base_where_query(table_name)
|
6
6
|
|
7
7
|
if options.present?
|
8
|
-
cql << 'WHERE'
|
8
|
+
cql << 'WHERE '
|
9
9
|
cql << parse_where_clause_options(options)
|
10
10
|
end
|
11
11
|
|
12
12
|
cql << ';'
|
13
|
-
db.execute(cql)
|
14
13
|
end
|
15
14
|
|
16
|
-
def create(table_name, columns, values)
|
15
|
+
def create(table_name, columns, values, options={})
|
17
16
|
cql = <<-CQL
|
18
17
|
INSERT INTO #{table_name} (#{columns.join(", ")})
|
19
18
|
VALUES (#{value_placeholders(values).join(", ")})
|
20
19
|
CQL
|
21
20
|
|
22
|
-
|
23
|
-
|
21
|
+
cql.tap do |statement|
|
22
|
+
statement << ttl(options[:ttl]) if options.has_key?(:ttl)
|
23
|
+
end
|
24
24
|
end
|
25
25
|
|
26
26
|
private
|
27
27
|
|
28
|
-
def
|
29
|
-
|
28
|
+
def ttl(secs)
|
29
|
+
"USING TTL #{secs}"
|
30
30
|
end
|
31
31
|
|
32
32
|
def value_placeholders(values)
|
@@ -4,6 +4,12 @@ describe CassandraRecord::Base do
|
|
4
4
|
let(:db) { RSpec.configuration.db }
|
5
5
|
let(:keyspace) { RSpec.configuration.keyspace }
|
6
6
|
|
7
|
+
before do
|
8
|
+
CassandraRecord::Base.configure do |configuration|
|
9
|
+
configuration.database_adapter = ::CassandraRecord::Database::Adapters::Cassandra.instance
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
class TestRecord < CassandraRecord::Base; end
|
8
14
|
|
9
15
|
describe ".create" do
|
@@ -18,8 +24,8 @@ describe CassandraRecord::Base do
|
|
18
24
|
record = TestRecord.create(id: 99, name: 'turkey')
|
19
25
|
|
20
26
|
select = <<-CQL
|
21
|
-
|
22
|
-
|
27
|
+
SELECT * from #{keyspace}.test_records
|
28
|
+
WHERE id = 99;
|
23
29
|
CQL
|
24
30
|
|
25
31
|
results = db.execute(select)
|
@@ -29,6 +35,72 @@ describe CassandraRecord::Base do
|
|
29
35
|
expect(result['id']).to eq(99)
|
30
36
|
expect(result['name']).to eq('turkey')
|
31
37
|
end
|
38
|
+
|
39
|
+
context "with TTL options" do
|
40
|
+
class TestRecord < CassandraRecord::Base
|
41
|
+
def create
|
42
|
+
options = { ttl: 1 }
|
43
|
+
super(options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "persists a record" do
|
48
|
+
TestRecord.create(id: 300, name: 'I\'m going away')
|
49
|
+
|
50
|
+
select = <<-CQL
|
51
|
+
SELECT * from #{keyspace}.test_records
|
52
|
+
WHERE id = 300;
|
53
|
+
CQL
|
54
|
+
|
55
|
+
results = db.execute(select)
|
56
|
+
expect(results.count).to eq(1)
|
57
|
+
|
58
|
+
# sucky, but we need to wait for Cassandra
|
59
|
+
# to remove the record to assert the TTL is working
|
60
|
+
sleep 1
|
61
|
+
|
62
|
+
results = db.execute(select)
|
63
|
+
expect(results.count).to eq(0)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe ".batch_create" do
|
69
|
+
context "with TTL options" do
|
70
|
+
it "persists a record" do
|
71
|
+
TestRecord.batch_create([{ id: 99, name: 'turkey' }, { id: 100, name: 'buffalo' }], ttl: 1)
|
72
|
+
|
73
|
+
select = <<-CQL
|
74
|
+
SELECT * from #{keyspace}.test_records
|
75
|
+
CQL
|
76
|
+
|
77
|
+
results = db.execute(select)
|
78
|
+
expect(results.count).to eq 2
|
79
|
+
|
80
|
+
sleep 2
|
81
|
+
|
82
|
+
results = db.execute(select)
|
83
|
+
expect(results.count).to eq 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns an array of the records it created" do
|
88
|
+
records = TestRecord.batch_create([{ id: 99, name: 'turkey' }, { id: 100, name: 'buffalo' }])
|
89
|
+
expect(records.first.name).to eq "turkey"
|
90
|
+
expect(records.last.name).to eq "buffalo"
|
91
|
+
end
|
92
|
+
|
93
|
+
it "persists the records" do
|
94
|
+
TestRecord.batch_create([{ id: 99, name: 'turkey' }, { id: 100, name: 'buffalo' }])
|
95
|
+
|
96
|
+
select = <<-CQL
|
97
|
+
SELECT * from #{keyspace}.test_records
|
98
|
+
CQL
|
99
|
+
|
100
|
+
results = db.execute(select)
|
101
|
+
expect(results.count).to eq 2
|
102
|
+
expect(results.first['id']).to eq 99
|
103
|
+
end
|
32
104
|
end
|
33
105
|
|
34
106
|
describe ".where" do
|
@@ -72,4 +144,34 @@ describe CassandraRecord::Base do
|
|
72
144
|
end
|
73
145
|
end
|
74
146
|
|
147
|
+
describe "configuring the database adapter" do
|
148
|
+
let(:some_adapter) { double(:some_adapter) }
|
149
|
+
|
150
|
+
before do
|
151
|
+
allow(some_adapter).to receive(:execute) { [] }
|
152
|
+
|
153
|
+
CassandraRecord::Base.configure do |configuration|
|
154
|
+
configuration.database_adapter = some_adapter
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context ".create" do
|
159
|
+
before do
|
160
|
+
allow(some_adapter).to receive(:prepare)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "uses the configured database adapter" do
|
164
|
+
TestRecord.create(name: 'things')
|
165
|
+
expect(some_adapter).to have_received(:prepare)
|
166
|
+
expect(some_adapter).to have_received(:execute)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context ".where" do
|
171
|
+
it "uses the configured database adapter" do
|
172
|
+
TestRecord.where(name: 'things')
|
173
|
+
expect(some_adapter).to have_received(:execute)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
75
177
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -16,8 +16,9 @@ RSpec.configure do |config|
|
|
16
16
|
db = CassandraRecord::Database::Adapters::Cassandra.instance
|
17
17
|
|
18
18
|
config.add_setting :db
|
19
|
-
config.db = db
|
20
19
|
config.add_setting :keyspace
|
20
|
+
|
21
|
+
config.db = db
|
21
22
|
config.keyspace = 'test_space' # CassandraRecord::Database::Adapters::Cassandra.instance.keyspace
|
22
23
|
|
23
24
|
create_keyspace = <<-CQL
|
@@ -3,6 +3,15 @@ require 'spec_helper'
|
|
3
3
|
describe CassandraRecord::Database::Adapters::Cassandra do
|
4
4
|
subject(:adapter) { CassandraRecord::Database::Adapters::Cassandra.instance }
|
5
5
|
|
6
|
+
before do
|
7
|
+
adapter.use(RSpec.configuration.keyspace)
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
# reset the singleton's @session
|
12
|
+
adapter.use(RSpec.configuration.keyspace)
|
13
|
+
end
|
14
|
+
|
6
15
|
describe "#configuration" do
|
7
16
|
before do
|
8
17
|
adapter.configuration do |config|
|
@@ -15,4 +24,41 @@ describe CassandraRecord::Database::Adapters::Cassandra do
|
|
15
24
|
specify { expect(adapter.configuration[:other_thing]).to eq('other_stuff') }
|
16
25
|
end
|
17
26
|
|
27
|
+
describe "connection retries" do
|
28
|
+
let(:cluster_connection) { double(:cluster_connection) }
|
29
|
+
let(:session) { double(:session) }
|
30
|
+
let(:retry_count) { CassandraRecord::Database::Adapters::Cassandra::MAX_RETRIES }
|
31
|
+
|
32
|
+
before do
|
33
|
+
allow(Cassandra).to receive(:cluster) { cluster_connection }
|
34
|
+
allow(cluster_connection).to receive(:connect) { session }
|
35
|
+
end
|
36
|
+
|
37
|
+
context "#prepare" do
|
38
|
+
before do
|
39
|
+
allow(session).to receive(:prepare).and_raise Cassandra::Errors::ClientError
|
40
|
+
end
|
41
|
+
|
42
|
+
it "retries the expected number of times" do
|
43
|
+
expect(session).to receive(:prepare).exactly(retry_count).times
|
44
|
+
expect {
|
45
|
+
adapter.prepare('some bogus commit statement')
|
46
|
+
}.to raise_error(Cassandra::Errors::ClientError)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "#execute" do
|
51
|
+
before do
|
52
|
+
allow(session).to receive(:execute).and_raise Cassandra::Errors::ClientError
|
53
|
+
end
|
54
|
+
|
55
|
+
it "retries the expected number of times" do
|
56
|
+
expect(session).to receive(:execute).exactly(retry_count).times
|
57
|
+
expect {
|
58
|
+
adapter.execute('some bogus commit statement')
|
59
|
+
}.to raise_error(Cassandra::Errors::ClientError)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
18
64
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cassandra_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gust
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-05-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|