cassandra_record 0.0.5 → 0.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.
- 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
|