perpetuity-mongodb 1.0.0.beta

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.
@@ -0,0 +1,16 @@
1
+ require 'perpetuity/mongodb/query_intersection'
2
+ require 'perpetuity/mongodb/query_expression'
3
+
4
+ module Perpetuity
5
+ class MongoDB
6
+ describe QueryIntersection do
7
+ let(:lhs) { QueryExpression.new :first, :equals, 'one' }
8
+ let(:rhs) { QueryExpression.new :second, :equals, 'two' }
9
+ let(:intersection) { QueryIntersection.new lhs, rhs }
10
+
11
+ it 'returns a Mongo representation of the union of 2 expressions' do
12
+ intersection.to_db.should be == { '$and' => [{first: 'one'}, {second: 'two'}] }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,79 @@
1
+ require 'perpetuity/mongodb/query'
2
+
3
+ module Perpetuity
4
+ describe MongoDB::Query do
5
+ let(:query) { MongoDB::Query }
6
+
7
+ it 'generates Mongo equality expressions' do
8
+ query.new{ |user| user.name == 'Jamie' }.to_db.should == {name: 'Jamie'}
9
+ end
10
+
11
+ it 'generates Mongo less-than expressions' do
12
+ query.new{ |v| v.quantity < 10 }.to_db.should == {quantity: { '$lt' => 10}}
13
+ end
14
+
15
+ it 'generates Mongo less-than-or-equal expressions' do
16
+ query.new{ |v| v.quantity <= 10 }.to_db.should == {quantity: { '$lte' => 10}}
17
+ end
18
+
19
+ it 'generates Mongo greater-than expressions' do
20
+ query.new{ |v| v.quantity > 10 }.to_db.should == {quantity: { '$gt' => 10}}
21
+ end
22
+
23
+ it 'generates Mongo greater-than-or-equal expressions' do
24
+ query.new{ |v| v.quantity >= 10 }.to_db.should == {quantity: { '$gte' => 10}}
25
+ end
26
+
27
+ it 'generates Mongo inequality expressions' do
28
+ query.new{ |user| user.name.not_equal? 'Jamie' }.to_db.should == {
29
+ name: {'$ne' => 'Jamie'}
30
+ }
31
+ end
32
+
33
+ it 'generates Mongo regexp expressions' do
34
+ query.new{ |user| user.name =~ /Jamie/ }.to_db.should == {name: /Jamie/}
35
+ end
36
+
37
+ describe 'negated queries' do
38
+ it 'negates an equality query' do
39
+ q = query.new { |user| user.name == 'Jamie' }
40
+ q.negate.to_db.should == { name: { '$ne' => 'Jamie' } }
41
+ end
42
+
43
+ it 'negates a not-equal query' do
44
+ q = query.new { |account| account.balance != 10 }
45
+ q.negate.to_db.should == { balance: { '$not' => { '$ne' => 10 } } }
46
+ end
47
+
48
+ it 'negates a less-than query' do
49
+ q = query.new { |account| account.balance < 10 }
50
+ q.negate.to_db.should == { balance: { '$not' => { '$lt' => 10 } } }
51
+ end
52
+
53
+ it 'negates a less-than-or-equal query' do
54
+ q = query.new { |account| account.balance <= 10 }
55
+ q.negate.to_db.should == { balance: { '$not' => { '$lte' => 10 } } }
56
+ end
57
+
58
+ it 'negates a greater-than query' do
59
+ q = query.new { |account| account.balance > 10 }
60
+ q.negate.to_db.should == { balance: { '$not' => { '$gt' => 10 } } }
61
+ end
62
+
63
+ it 'negates a greater-than-or-equal query' do
64
+ q = query.new { |account| account.balance >= 10 }
65
+ q.negate.to_db.should == { balance: { '$not' => { '$gte' => 10 } } }
66
+ end
67
+
68
+ it 'negates a regex query' do
69
+ q = query.new { |account| account.name =~ /Jamie/ }
70
+ q.negate.to_db.should == { name: { '$not' => /Jamie/ } }
71
+ end
72
+
73
+ it 'negates a inclusion query' do
74
+ q = query.new { |article| article.tags.in ['tag1', 'tag2'] }
75
+ q.negate.to_db.should == { tags: { '$not' => { '$in' => ['tag1', 'tag2'] } } }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ require 'perpetuity/mongodb/query_union'
2
+ require 'perpetuity/mongodb/query_expression'
3
+
4
+ module Perpetuity
5
+ class MongoDB
6
+ describe QueryUnion do
7
+ let(:lhs) { QueryExpression.new :first, :equals, 'one' }
8
+ let(:rhs) { QueryExpression.new :second, :equals, 'two' }
9
+ let(:union) { QueryUnion.new lhs, rhs }
10
+
11
+ it 'returns the proper union of two expressions' do
12
+ union.to_db.should be == { '$or' => [{first: 'one'}, {second: 'two'}] }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,212 @@
1
+ require 'perpetuity/mongodb/serializer'
2
+ require 'perpetuity/mapper'
3
+ require 'perpetuity/mapper_registry'
4
+ require 'support/test_classes/book'
5
+ require 'support/test_classes/user'
6
+ require 'support/test_classes/car'
7
+
8
+ module Perpetuity
9
+ class MongoDB
10
+ describe Serializer do
11
+ let(:dave) { User.new('Dave') }
12
+ let(:andy) { User.new('Andy') }
13
+ let(:authors) { [dave, andy] }
14
+ let(:book) { Book.new('The Pragmatic Programmer', authors) }
15
+ let(:mapper_registry) { MapperRegistry.new }
16
+ let(:book_mapper) do
17
+ registry = mapper_registry
18
+ Class.new(Perpetuity::Mapper) do
19
+ map Book, registry
20
+ attribute :title
21
+ attribute :authors
22
+ end.new(registry)
23
+ end
24
+ let(:user_mapper) do
25
+ registry = mapper_registry
26
+ Class.new(Perpetuity::Mapper) do
27
+ map User, registry
28
+ attribute :name
29
+ end.new(registry)
30
+ end
31
+ let(:data_source) { double('Data Source') }
32
+ let(:serializer) { Serializer.new(book_mapper) }
33
+
34
+ before do
35
+ serializer.give_id_to dave, 1
36
+ serializer.give_id_to andy, 2
37
+ end
38
+
39
+ it 'serializes an array of non-embedded attributes as references' do
40
+ user_mapper.stub(data_source: data_source)
41
+ book_mapper.stub(data_source: data_source)
42
+ data_source.should_receive(:can_serialize?).with(book.title).and_return true
43
+ data_source.should_receive(:can_serialize?).with(dave).and_return false
44
+ data_source.should_receive(:can_serialize?).with(andy).and_return false
45
+ serializer.serialize(book).should be == {
46
+ 'title' => book.title,
47
+ 'authors' => [
48
+ {
49
+ '__metadata__' => {
50
+ 'class' => 'User',
51
+ 'id' => user_mapper.id_for(dave)
52
+ }
53
+ },
54
+ {
55
+ '__metadata__' => {
56
+ 'class' => 'User',
57
+ 'id' => user_mapper.id_for(andy)
58
+ }
59
+ }
60
+ ]
61
+ }
62
+ end
63
+
64
+ it 'can serialize only changed attributes' do
65
+ book = Book.new('Original Title')
66
+ updated_book = book.dup
67
+ updated_book.title = 'New Title'
68
+ book_mapper.stub(data_source: data_source)
69
+ data_source.stub(:can_serialize?).with('New Title') { true }
70
+ data_source.stub(:can_serialize?).with('Original Title') { true }
71
+ serializer.serialize_changes(updated_book, book).should == {
72
+ 'title' => 'New Title'
73
+ }
74
+ end
75
+
76
+ context 'with objects that have hashes as attributes' do
77
+ let(:name_data) { {first_name: 'Jamie', last_name: 'Gaskins'} }
78
+ let(:serialized_data) { { 'name' => name_data } }
79
+ let(:user) { User.new(name_data) }
80
+ let(:user_serializer) { Serializer.new(user_mapper) }
81
+
82
+ before do
83
+ user_mapper.stub(data_source: data_source)
84
+ book_mapper.stub(data_source: data_source)
85
+ data_source.stub(:can_serialize?).with(name_data) { true }
86
+ end
87
+
88
+ it 'serializes' do
89
+ user_serializer.serialize(user).should be == serialized_data
90
+ end
91
+
92
+ it 'unserializes' do
93
+ user_serializer.unserialize(serialized_data).name.should be == user.name
94
+ end
95
+ end
96
+
97
+ describe 'with an array of references' do
98
+ let(:author) { Reference.new(User, 1) }
99
+ let(:title) { 'title' }
100
+ let(:book) { Book.new(title, [author]) }
101
+
102
+ before do
103
+ user_mapper.stub(data_source: data_source)
104
+ book_mapper.stub(data_source: data_source)
105
+ end
106
+
107
+ it 'passes the reference unserialized' do
108
+ data_source.should_receive(:can_serialize?).with('title') { true }
109
+ serializer.serialize(book).should == {
110
+ 'title' => title,
111
+ 'authors' => [{
112
+ '__metadata__' => {
113
+ 'class' => author.klass.to_s,
114
+ 'id' => author.id
115
+ }
116
+ }]
117
+ }
118
+ end
119
+ end
120
+
121
+ context 'with uninitialized attributes' do
122
+ let(:car_model) { 'Corvette' }
123
+ let(:car) { Car.new(model: car_model) }
124
+ let(:mapper) do
125
+ registry = mapper_registry
126
+ Class.new(Mapper) do
127
+ map Car, registry
128
+
129
+ attribute :make
130
+ attribute :model
131
+ end.new(registry)
132
+ end
133
+ let(:serializer) { Serializer.new(mapper) }
134
+
135
+
136
+ it 'does not persist uninitialized attributes' do
137
+ mapper.stub data_source: data_source
138
+ data_source.should_receive(:can_serialize?).with(car_model) { true }
139
+
140
+ serializer.serialize(car).should == { 'model' => car_model }
141
+ end
142
+ end
143
+
144
+ context 'with marshaled data' do
145
+ let(:unserializable_value) { 1..10 }
146
+
147
+ it 'stores metadata with marshal information' do
148
+ book = Book.new(unserializable_value)
149
+
150
+ book_mapper.stub(data_source: data_source)
151
+ data_source.stub(:can_serialize?).with(book.title) { false }
152
+
153
+ serializer.serialize(book).should == {
154
+ 'title' => {
155
+ '__marshaled__' => true,
156
+ 'value' => Marshal.dump(unserializable_value)
157
+ },
158
+ 'authors' => []
159
+ }
160
+ end
161
+
162
+ it 'stores marshaled attributes within arrays' do
163
+ book = Book.new([unserializable_value])
164
+ book_mapper.stub(data_source: data_source)
165
+ data_source.stub(:can_serialize?).with(book.title.first) { false }
166
+
167
+ serializer.serialize(book).should == {
168
+ 'title' => [{
169
+ '__marshaled__' => true,
170
+ 'value' => Marshal.dump(unserializable_value)
171
+ }],
172
+ 'authors' => []
173
+ }
174
+ end
175
+
176
+ it 'unmarshals data that has been marshaled by the serializer' do
177
+ data = {
178
+ 'title' => {
179
+ '__marshaled__' => true,
180
+ 'value' => Marshal.dump(unserializable_value),
181
+ }
182
+ }
183
+ serializer.unserialize(data).title.should be_a unserializable_value.class
184
+ end
185
+
186
+ it 'does not unmarshal data not marshaled by the serializer' do
187
+ data = { 'title' => Marshal.dump(unserializable_value) }
188
+
189
+ serializer.unserialize(data).title.should be_a String
190
+ end
191
+ end
192
+
193
+ it 'unserializes a hash of primitives' do
194
+ time = Time.now
195
+ serialized_data = {
196
+ 'number' => 1,
197
+ 'string' => 'hello',
198
+ 'boolean' => true,
199
+ 'float' => 7.5,
200
+ 'time' => time
201
+ }
202
+
203
+ object = serializer.unserialize(serialized_data)
204
+ object.instance_variable_get(:@number).should == 1
205
+ object.instance_variable_get(:@string).should == 'hello'
206
+ object.instance_variable_get(:@boolean).should == true
207
+ object.instance_variable_get(:@float).should == 7.5
208
+ object.instance_variable_get(:@time).should == time
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,218 @@
1
+ require 'perpetuity/mongodb'
2
+ require 'date'
3
+
4
+ module Perpetuity
5
+ describe MongoDB do
6
+ let(:mongo) { MongoDB.new db: 'perpetuity_gem_test' }
7
+ let(:klass) { String }
8
+ subject { mongo }
9
+
10
+ it 'is not connected when instantiated' do
11
+ mongo.should_not be_connected
12
+ end
13
+
14
+ it 'connects to its host' do
15
+ mongo.connect
16
+ mongo.should be_connected
17
+ end
18
+
19
+ it 'connects automatically when accessing the database' do
20
+ mongo.database
21
+ mongo.should be_connected
22
+ end
23
+
24
+ describe 'initialization params' do
25
+ let(:host) { double('host') }
26
+ let(:port) { double('port') }
27
+ let(:db) { double('db') }
28
+ let(:pool_size) { double('pool size') }
29
+ let(:username) { double('username') }
30
+ let(:password) { double('password') }
31
+ let(:mongo) do
32
+ MongoDB.new(
33
+ host: host,
34
+ port: port,
35
+ db: db,
36
+ pool_size: pool_size,
37
+ username: username,
38
+ password: password
39
+ )
40
+ end
41
+ subject { mongo }
42
+
43
+ its(:host) { should == host }
44
+ its(:port) { should == port }
45
+ its(:db) { should == db }
46
+ its(:pool_size) { should == pool_size }
47
+ its(:username) { should == username }
48
+ its(:password) { should == password }
49
+ end
50
+
51
+ it 'inserts documents into a collection' do
52
+ expect { mongo.insert klass, { name: 'foo' }, [] }.to change { mongo.count klass }.by 1
53
+ end
54
+
55
+ it 'inserts multiple documents into a collection' do
56
+ expect { mongo.insert klass, [{name: 'foo'}, {name: 'bar'}], [] }
57
+ .to change { mongo.count klass }.by 2
58
+ end
59
+
60
+ it 'removes all documents from a collection' do
61
+ mongo.insert klass, {}, []
62
+ mongo.delete_all klass
63
+ mongo.count(klass).should == 0
64
+ end
65
+
66
+ it 'counts the documents in a collection' do
67
+ mongo.delete_all klass
68
+ 3.times do
69
+ mongo.insert klass, {}, []
70
+ end
71
+ mongo.count(klass).should == 3
72
+ end
73
+
74
+ it 'counts the documents matching a query' do
75
+ mongo.delete_all klass
76
+ 1.times { mongo.insert klass, { name: 'bar' }, [] }
77
+ 3.times { mongo.insert klass, { name: 'foo' }, [] }
78
+ mongo.count(klass) { |o| o.name == 'foo' }.should == 3
79
+ end
80
+
81
+ it 'gets the first document in a collection' do
82
+ value = {value: 1}
83
+ mongo.insert klass, value, []
84
+ mongo.first(klass)[:hypothetical_value].should == value['value']
85
+ end
86
+
87
+ it 'gets all of the documents in a collection' do
88
+ values = [{value: 1}, {value: 2}]
89
+ mongo.should_receive(:retrieve).with(Object, mongo.nil_query, {})
90
+ .and_return(values)
91
+ mongo.all(Object).should == values
92
+ end
93
+
94
+ it 'retrieves by id if the id is a string' do
95
+ time = Time.now.utc
96
+ id = mongo.insert Object, {inserted: time}, []
97
+
98
+ object = mongo.retrieve(Object, mongo.query{|o| o.id == id.to_s }).first
99
+ retrieved_time = object["inserted"]
100
+ retrieved_time.to_f.should be_within(0.001).of time.to_f
101
+ end
102
+
103
+ describe 'serialization' do
104
+ let(:object) { Object.new }
105
+ let(:foo_attribute) { double('Attribute', name: :foo) }
106
+ let(:baz_attribute) { double('Attribute', name: :baz) }
107
+ let(:mapper) { double('Mapper',
108
+ mapped_class: Object,
109
+ mapper_registry: {},
110
+ attribute_set: Set[foo_attribute, baz_attribute],
111
+ data_source: mongo,
112
+ ) }
113
+
114
+ before do
115
+ object.instance_variable_set :@foo, 'bar'
116
+ object.instance_variable_set :@baz, 'quux'
117
+ end
118
+
119
+ it 'serializes objects' do
120
+ mongo.serialize(object, mapper).should == {
121
+ 'foo' => 'bar',
122
+ 'baz' => 'quux'
123
+ }
124
+ end
125
+
126
+ it 'can serialize only modified attributes of objects' do
127
+ updated = object.dup
128
+ updated.instance_variable_set :@foo, 'foo'
129
+
130
+ serialized = mongo.serialize_changed_attributes(updated, object, mapper)
131
+ serialized.should == { 'foo' => 'foo' }
132
+ end
133
+ end
134
+
135
+ describe 'serializable objects' do
136
+ let(:serializable_values) { [nil, true, false, 1, 1.2, '', [], {}, Time.now] }
137
+
138
+ it 'can insert serializable values' do
139
+ serializable_values.each do |value|
140
+ mongo.insert(Object, {value: value}, []).should be_a Moped::BSON::ObjectId
141
+ mongo.can_serialize?(value).should be_true
142
+ end
143
+ end
144
+ end
145
+
146
+ it 'generates a new query DSL object' do
147
+ mongo.query { |object| object.whatever == 1 }.should respond_to :to_db
148
+ end
149
+
150
+ describe 'indexing' do
151
+ let(:collection) { Object }
152
+ let(:key) { 'object_id' }
153
+
154
+ before { mongo.index collection, key }
155
+ after { mongo.drop_collection collection }
156
+
157
+ it 'adds indexes for the specified key on the specified collection' do
158
+ indexes = mongo.indexes(collection).select{ |index| index.attribute == 'object_id' }
159
+ indexes.should_not be_empty
160
+ indexes.first.order.should be :ascending
161
+ end
162
+
163
+ it 'adds descending-order indexes' do
164
+ index = mongo.index collection, 'hash', order: :descending
165
+ index.order.should be :descending
166
+ end
167
+
168
+ it 'creates indexes on the database collection' do
169
+ mongo.delete_all collection
170
+ index = mongo.index collection, 'real_index', order: :descending, unique: true
171
+ mongo.activate_index! index
172
+
173
+ mongo.active_indexes(collection).should include index
174
+ end
175
+
176
+ it 'removes indexes' do
177
+ mongo.drop_collection collection
178
+ index = mongo.index collection, 'real_index', order: :descending, unique: true
179
+ mongo.activate_index! index
180
+ mongo.remove_index index
181
+ mongo.active_indexes(collection).should_not include index
182
+ end
183
+ end
184
+
185
+ describe 'atomic operations' do
186
+ after(:all) { mongo.delete_all klass }
187
+
188
+ it 'increments the value of an attribute' do
189
+ id = mongo.insert klass, { count: 1 }, []
190
+ mongo.increment klass, id, :count
191
+ mongo.increment klass, id, :count, 10
192
+ query = mongo.query { |o| o.id == id }
193
+ mongo.retrieve(klass, query).first['count'].should be == 12
194
+ mongo.increment klass, id, :count, -1
195
+ mongo.retrieve(klass, query).first['count'].should be == 11
196
+ end
197
+ end
198
+
199
+ describe 'operation errors' do
200
+ let(:data) { { foo: 'bar' } }
201
+ let(:index) { mongo.index Object, :foo, unique: true }
202
+
203
+ before do
204
+ mongo.delete_all Object
205
+ mongo.activate_index! index
206
+ end
207
+
208
+ after { mongo.drop_collection Object }
209
+
210
+ it 'raises an exception when insertion fails' do
211
+ mongo.insert Object, data, []
212
+
213
+ expect { mongo.insert Object, data, [] }.to raise_error DuplicateKeyError,
214
+ 'Tried to insert Object with duplicate unique index: foo => "bar"'
215
+ end
216
+ end
217
+ end
218
+ end