virsandra 0.5.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.
Files changed (43) hide show
  1. data/LICENSE.txt +22 -0
  2. data/README.md +93 -0
  3. data/Rakefile +18 -0
  4. data/lib/virsandra.rb +89 -0
  5. data/lib/virsandra/configuration.rb +61 -0
  6. data/lib/virsandra/connection.rb +39 -0
  7. data/lib/virsandra/cql_value.rb +38 -0
  8. data/lib/virsandra/errors.rb +3 -0
  9. data/lib/virsandra/model.rb +86 -0
  10. data/lib/virsandra/model_query.rb +52 -0
  11. data/lib/virsandra/queries/add_query.rb +25 -0
  12. data/lib/virsandra/queries/alter_query.rb +34 -0
  13. data/lib/virsandra/queries/delete_query.rb +25 -0
  14. data/lib/virsandra/queries/insert_query.rb +34 -0
  15. data/lib/virsandra/queries/limit_query.rb +17 -0
  16. data/lib/virsandra/queries/order_query.rb +36 -0
  17. data/lib/virsandra/queries/select_query.rb +72 -0
  18. data/lib/virsandra/queries/table_query.rb +13 -0
  19. data/lib/virsandra/queries/values_query.rb +41 -0
  20. data/lib/virsandra/queries/where_query.rb +69 -0
  21. data/lib/virsandra/query.rb +87 -0
  22. data/lib/virsandra/version.rb +3 -0
  23. data/spec/feature_helper.rb +62 -0
  24. data/spec/integration/virsandra_spec.rb +13 -0
  25. data/spec/lib/virsandra/configuration_spec.rb +66 -0
  26. data/spec/lib/virsandra/connection_spec.rb +47 -0
  27. data/spec/lib/virsandra/cql_value_spec.rb +25 -0
  28. data/spec/lib/virsandra/model_query_spec.rb +58 -0
  29. data/spec/lib/virsandra/model_spec.rb +173 -0
  30. data/spec/lib/virsandra/queries/add_query_spec.rb +26 -0
  31. data/spec/lib/virsandra/queries/alter_query_spec.rb +35 -0
  32. data/spec/lib/virsandra/queries/delete_query_spec.rb +34 -0
  33. data/spec/lib/virsandra/queries/insert_query_spec.rb +36 -0
  34. data/spec/lib/virsandra/queries/limit_query_spec.rb +20 -0
  35. data/spec/lib/virsandra/queries/order_query_spec.rb +33 -0
  36. data/spec/lib/virsandra/queries/select_query_spec.rb +108 -0
  37. data/spec/lib/virsandra/queries/table_query_spec.rb +13 -0
  38. data/spec/lib/virsandra/queries/values_query_spec.rb +41 -0
  39. data/spec/lib/virsandra/queries/where_query_spec.rb +76 -0
  40. data/spec/lib/virsandra/query_spec.rb +117 -0
  41. data/spec/lib/virsandra_spec.rb +108 -0
  42. data/spec/spec_helper.rb +19 -0
  43. metadata +207 -0
@@ -0,0 +1,3 @@
1
+ module Virsandra
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ TEST_KEYSPACE = "virtest"
4
+
5
+ module IntegrationTestHelper
6
+ def create_keyspace
7
+ Virsandra.keyspace = 'system'
8
+ Virsandra.execute("CREATE KEYSPACE #{TEST_KEYSPACE} WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1}")
9
+ Virsandra.reset!
10
+ end
11
+
12
+ def create_companies_table
13
+ cql = <<-CQL
14
+ CREATE TABLE companies (
15
+ id uuid,
16
+ name text,
17
+ score int,
18
+ founder text,
19
+ founded int,
20
+ PRIMARY KEY (id, score))
21
+ CQL
22
+ Virsandra.keyspace = TEST_KEYSPACE
23
+ Virsandra.execute(cql)
24
+ end
25
+
26
+ def drop_keyspace
27
+ Virsandra.reset!
28
+ Virsandra.keyspace = 'system'
29
+ Virsandra.execute("DROP KEYSPACE #{TEST_KEYSPACE}")
30
+ end
31
+
32
+ def build_up
33
+ begin
34
+ create_keyspace
35
+ create_companies_table
36
+ rescue Cql::QueryError
37
+ drop_keyspace
38
+
39
+ if defined?(retried)
40
+ raise $!
41
+ else
42
+ retried = true
43
+ retry
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+
50
+
51
+ RSpec.configure do |config|
52
+
53
+ config.include(IntegrationTestHelper)
54
+
55
+ config.before do
56
+ if example.metadata[:integration]
57
+ build_up
58
+ Virsandra.reset!
59
+ Virsandra.keyspace = TEST_KEYSPACE
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ require 'feature_helper'
2
+
3
+ describe "Virsandra", integration: true do
4
+
5
+ it "return connection to server" do
6
+ Virsandra.connection.should_not be_nil
7
+ end
8
+
9
+ it "allows to disconnect" do
10
+ Virsandra.connection.should_receive(:disconnect!).and_call_original
11
+ Virsandra.disconnect!
12
+ end
13
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virsandra::Configuration do
4
+ subject(:config){ described_class.new(given_options) }
5
+ let(:given_options){ {} }
6
+
7
+ its(:servers){ should eq("127.0.0.1") }
8
+ its(:consistency){ should eq(:quorum) }
9
+ its(:keyspace){ should be_nil }
10
+
11
+ it "uses given values over default ones" do
12
+ described_class.new(consistency: :one).consistency.should eq(:one)
13
+ end
14
+
15
+ describe "validate!" do
16
+ context "no keyspace" do
17
+ it "should raise an error" do
18
+ expect{ config.validate! }.to raise_error(Virsandra::ConfigurationError)
19
+ end
20
+ end
21
+
22
+ context "no servers" do
23
+ let(:given_options){ {servers: nil} }
24
+
25
+ it "should raise an error" do
26
+ expect{ config.validate! }.to raise_error(Virsandra::ConfigurationError)
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#reset!" do
32
+ let(:given_options){ {keyspace: :my_keyspace} }
33
+
34
+ it "reset options to default ones" do
35
+ config.reset!
36
+ config.keyspace.should == :my_keyspace
37
+ end
38
+ end
39
+
40
+ describe "to_hash" do
41
+ it "returns all options as a hash" do
42
+ config.to_hash.should eq({
43
+ consistency: :quorum,
44
+ keyspace: nil,
45
+ servers: "127.0.0.1"
46
+ })
47
+ end
48
+ end
49
+
50
+ describe "changed?" do
51
+ it "should be changed when option attribute value changes" do
52
+ config.changed?.should be_false
53
+ config.keyspace = :other
54
+ config.changed?.should be_true
55
+ end
56
+
57
+ it "stops being changed when changes are accepted" do
58
+ config.changed?.should be_false
59
+ config.keyspace = :other
60
+ config.changed?.should be_true
61
+ config.accept_changes
62
+ config.changed?.should be_false
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virsandra::Connection do
4
+ let(:config){ Virsandra::Configuration.new(keyspace: :my_keyspace) }
5
+ let(:handle){ double("handle", use: nil) }
6
+ subject(:connection){ described_class.new(config) }
7
+
8
+ before do
9
+ Cql::Client.stub(:connect).and_return(handle)
10
+ end
11
+
12
+ its(:config){ should eq(config) }
13
+
14
+ context "invalid configuration" do
15
+ it "raises an exception if settings are invalid" do
16
+ config.keyspace = nil
17
+ expect { connection }.to raise_error(Virsandra::ConfigurationError)
18
+ end
19
+ end
20
+
21
+ it "obtains a db connection" do
22
+ connection.handle.should eq(handle)
23
+ end
24
+
25
+ it "delegates to the cassandra handle" do
26
+ connection.handle.should_receive(:keyspace)
27
+ connection.keyspace
28
+ end
29
+
30
+ it "checks if a cassandra connection will respond to a method" do
31
+ handle.stub(:respond_to? => true)
32
+ connection.should respond_to :execute
33
+ end
34
+
35
+ it "should disconnect" do
36
+ handle.should_receive(:close)
37
+ connection.disconnect!
38
+ end
39
+
40
+ describe "#execute" do
41
+ it "should use configuration consistency when none is given" do
42
+ config.consistency = :one
43
+ handle.should_receive(:execute).with("query", :one)
44
+ connection.execute("query")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virsandra::CQLValue do
4
+
5
+ subject { Virsandra::CQLValue }
6
+
7
+ it "returns plain numbers" do
8
+ subject.convert(10).should == "10"
9
+ subject.convert(10.0).should == "10.0"
10
+ subject.convert(BigDecimal.new("10.0")).should == "0.1E2"
11
+ end
12
+
13
+ it "quotes strings" do
14
+ subject.convert("Hello").should == "'Hello'"
15
+ end
16
+
17
+ it "escapes strings" do
18
+ subject.convert("It's all good.").should == "'It''s all good.'"
19
+ end
20
+
21
+ it "converts UUID to guid" do
22
+ uuid = SimpleUUID::UUID.new
23
+ subject.convert(uuid).should == uuid.to_guid
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virsandra::ModelQuery do
4
+
5
+ let(:model) {
6
+ stub(:model,
7
+ attributes: {id: 123, name: 'Funky Circle'},
8
+ table: :companies,
9
+ key: {id: 123})
10
+ }
11
+
12
+ let(:model_query) { Virsandra::ModelQuery.new(model) }
13
+
14
+ it "stores the model on initialization" do
15
+ model_query
16
+ model_query.instance_variable_get(:@model).should == model
17
+ end
18
+
19
+ it "saves the model" do
20
+ query = stub(into: '', values: '', fetch: '')
21
+
22
+ Virsandra::Query.should_receive(:insert).and_return(query)
23
+
24
+ query.should_receive(:into).with(:companies).and_return(query)
25
+ query.should_receive(:values).with(id: 123, name: "Funky Circle").and_return(query)
26
+ query.should_receive(:fetch)
27
+ model_query.save
28
+ end
29
+
30
+ it "can retrieve the hash row from cassandra using the model's key" do
31
+ query = stub(from: '', where: '', fetch: '')
32
+
33
+ Virsandra::Query.should_receive(:select).and_return(query)
34
+ model.should_receive(:valid?).and_return(true)
35
+ query.should_receive(:from).with(:companies).and_return(query)
36
+ query.should_receive(:where).with(id: 123).and_return(query)
37
+ query.should_receive(:fetch)
38
+
39
+ model_query.find_by_key
40
+ end
41
+
42
+ it "doesn't fetch a row if the model is invalid" do
43
+ model.should_receive(:valid?).and_return(false)
44
+ model_query.find_by_key.should == {}
45
+ end
46
+
47
+ it "deletes the model" do
48
+ query = stub(from: '', where: '', fetch: '')
49
+
50
+ Virsandra::Query.should_receive(:delete).and_return(query)
51
+ query.should_receive(:from).with(:companies).and_return(query)
52
+ query.should_receive(:where).with(id: 123).and_return(query)
53
+ query.should_receive(:fetch)
54
+
55
+ model_query.delete
56
+ end
57
+
58
+ end
@@ -0,0 +1,173 @@
1
+ require 'feature_helper'
2
+
3
+ class Company
4
+ include Virsandra::Model
5
+
6
+ attribute :id, SimpleUUID::UUID, :default => proc { SimpleUUID::UUID.new }
7
+ attribute :name, String
8
+ attribute :score, Fixnum
9
+ attribute :founded, Fixnum
10
+ attribute :founder, String
11
+
12
+ table :companies
13
+ key :id, :score
14
+ end
15
+
16
+
17
+ describe Virsandra::Model, integration: true do
18
+
19
+ let(:id) { SimpleUUID::UUID.new }
20
+
21
+ let(:company) { Company.new(id: id, name: "Testco", score: 78)}
22
+
23
+ it "has attributes" do
24
+ company.attributes.should be_a Hash
25
+ end
26
+
27
+ it "selects keys" do
28
+ company.key.should == {id: id, score: 78}
29
+ end
30
+
31
+ it "reads the configured table" do
32
+ company.table.should == :companies
33
+ end
34
+
35
+ it "is valid with a key" do
36
+ company.should be_valid
37
+ end
38
+
39
+ it "is invalid when a key element is nil" do
40
+ company.id = nil
41
+ company.should_not be_valid
42
+ end
43
+
44
+ describe "with no assigned id" do
45
+ it "creates unique uuid's for each instance" do
46
+ company_one = Company.new(name: 'x')
47
+ company_two = Company.new(name: 'x')
48
+ company_one.should_not == company_two
49
+ company_one[:id].should_not == company_two[:id]
50
+ end
51
+ end
52
+
53
+ it "is equivelent with the same model same attributes" do
54
+ Company.new(id: id, name: 'x').should == Company.new(id: id, name: 'x')
55
+ end
56
+
57
+ it "is not equivelent with different attributes" do
58
+ Company.new(id: id, name: 'x').should_not == Company.new(id: id, name: 'y')
59
+ end
60
+
61
+ it "is not equivelent to a different model" do
62
+ Company.new(id: id, name: 'x').should_not == stub(attributes: {id: id, name: 'x'})
63
+ end
64
+
65
+ describe "finding an existing record" do
66
+ before do
67
+ Virsandra.execute("INSERT INTO companies (id, score, name, founded) VALUES (#{id.to_guid}, 101, 'Funky', 1990)")
68
+ end
69
+
70
+ it "can find a record from a key" do
71
+ company = Company.find(id: id, score: 101)
72
+ company.attributes.should == {id: id, score: 101, name: "Funky", founded: 1990, founder: nil}
73
+ end
74
+
75
+ it "raises an error with an incomplete key" do
76
+ expect { Company.find(score: 11) }.to raise_error(ArgumentError)
77
+ end
78
+
79
+ it "raises an error with an over-specified key" do
80
+ expect { Company.find(score: 11, id: id, name: 'Whatever') }.to raise_error(ArgumentError)
81
+ end
82
+
83
+ it "populates missing columns, keeping specified values" do
84
+ attrs = {id: id, name: "Google", score: 101, founder: "Larry Brin"}
85
+
86
+ company = Company.load(attrs)
87
+ company.attributes.should == attrs.merge(founded: 1990)
88
+ end
89
+ end
90
+
91
+ describe "saving a model" do
92
+ before do
93
+ Virsandra.execute("USE virtest")
94
+ Virsandra.execute("TRUNCATE companies")
95
+ end
96
+
97
+ it "can be saved" do
98
+ company = Company.new(id: id, score: 101, name: "Job Place")
99
+ company.save
100
+ Company.find(company.key).should == company
101
+ end
102
+
103
+ it "doesn't save when invalid" do
104
+ company = Company.new(name: "Keyless Inc.")
105
+ Virsandra::ModelQuery.should_not_receive(:new)
106
+ company.save
107
+ end
108
+ end
109
+
110
+ describe "deleting a model" do
111
+ before do
112
+ Virsandra.execute("USE virtest")
113
+ Virsandra.execute("TRUNCATE companies")
114
+ end
115
+
116
+ it "can be deleted" do
117
+ Virsandra.execute("INSERT INTO companies (id, score) VALUES (#{id.to_guid}, 101)")
118
+ company = Company.find(id: id, score: 101)
119
+ company.delete
120
+ Company.where(id: id, score: 101).to_a.should be_empty
121
+ end
122
+
123
+ it "only deletes the current model" do
124
+ Virsandra.execute("INSERT INTO companies (id, score) VALUES (#{id.to_guid}, 101)")
125
+ Virsandra.execute("INSERT INTO companies (id, score) VALUES (#{id.to_guid}, 102)")
126
+ company = Company.find(id: id, score: 101)
127
+ company.delete
128
+ Company.where(id: id, score: 102).to_a.should_not be_empty
129
+
130
+ end
131
+
132
+
133
+
134
+ end
135
+
136
+ describe "working with existing records" do
137
+ before do
138
+ Virsandra.execute("USE virtest")
139
+ Virsandra.execute("TRUNCATE companies")
140
+
141
+ 5.times { |n| Company.new(id: id, score: n + 1, name: "Test").save }
142
+ 5.times { |n| Company.new(id: SimpleUUID::UUID.new, score: n + 1, name: "Test").save }
143
+ end
144
+
145
+ it "checks all search terms are attributes" do
146
+ expect { Company.where(id: 1, missing: 'key') }.to raise_error(ArgumentError)
147
+ expect { Company.where(id: id, score: 10) }.not_to raise_error(ArgumentError)
148
+ end
149
+
150
+ it "returns an enumerable" do
151
+ Company.where(id: id).should be_an Enumerator
152
+ end
153
+
154
+ it "creates an model instance for each entry " do
155
+ Company.where(id: id).map(&:score).should == [1,2,3,4,5]
156
+ end
157
+
158
+ it "can get all records" do
159
+ Company.all.to_a.length.should == 10
160
+ end
161
+
162
+ it "lazily instanciates models" do
163
+ Company.should_receive(:new).twice
164
+ Company.all.take(2)
165
+ end
166
+
167
+ it "can be empty" do
168
+ Company.where(id: id, score: 1009).to_a.should be_empty
169
+ end
170
+
171
+ end
172
+
173
+ end