cequel 0.0.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/lib/cequel.rb +16 -0
  2. data/lib/cequel/batch.rb +58 -0
  3. data/lib/cequel/cql_row_specification.rb +22 -0
  4. data/lib/cequel/data_set.rb +346 -0
  5. data/lib/cequel/errors.rb +4 -0
  6. data/lib/cequel/keyspace.rb +106 -0
  7. data/lib/cequel/model.rb +95 -0
  8. data/lib/cequel/model/associations.rb +120 -0
  9. data/lib/cequel/model/callbacks.rb +32 -0
  10. data/lib/cequel/model/class_internals.rb +48 -0
  11. data/lib/cequel/model/column.rb +20 -0
  12. data/lib/cequel/model/dictionary.rb +202 -0
  13. data/lib/cequel/model/dirty.rb +53 -0
  14. data/lib/cequel/model/dynamic.rb +31 -0
  15. data/lib/cequel/model/errors.rb +13 -0
  16. data/lib/cequel/model/inheritable.rb +48 -0
  17. data/lib/cequel/model/instance_internals.rb +23 -0
  18. data/lib/cequel/model/local_association.rb +42 -0
  19. data/lib/cequel/model/magic.rb +79 -0
  20. data/lib/cequel/model/mass_assignment_security.rb +21 -0
  21. data/lib/cequel/model/naming.rb +17 -0
  22. data/lib/cequel/model/observer.rb +42 -0
  23. data/lib/cequel/model/persistence.rb +173 -0
  24. data/lib/cequel/model/properties.rb +143 -0
  25. data/lib/cequel/model/railtie.rb +33 -0
  26. data/lib/cequel/model/remote_association.rb +40 -0
  27. data/lib/cequel/model/scope.rb +362 -0
  28. data/lib/cequel/model/scoped.rb +50 -0
  29. data/lib/cequel/model/subclass_internals.rb +45 -0
  30. data/lib/cequel/model/timestamps.rb +52 -0
  31. data/lib/cequel/model/translation.rb +17 -0
  32. data/lib/cequel/model/validations.rb +50 -0
  33. data/lib/cequel/new_relic_instrumentation.rb +22 -0
  34. data/lib/cequel/row_specification.rb +63 -0
  35. data/lib/cequel/statement.rb +23 -0
  36. data/lib/cequel/version.rb +3 -0
  37. data/spec/environment.rb +3 -0
  38. data/spec/examples/data_set_spec.rb +382 -0
  39. data/spec/examples/keyspace_spec.rb +63 -0
  40. data/spec/examples/model/associations_spec.rb +109 -0
  41. data/spec/examples/model/callbacks_spec.rb +79 -0
  42. data/spec/examples/model/dictionary_spec.rb +413 -0
  43. data/spec/examples/model/dirty_spec.rb +39 -0
  44. data/spec/examples/model/dynamic_spec.rb +41 -0
  45. data/spec/examples/model/inheritable_spec.rb +45 -0
  46. data/spec/examples/model/magic_spec.rb +199 -0
  47. data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
  48. data/spec/examples/model/naming_spec.rb +9 -0
  49. data/spec/examples/model/observer_spec.rb +86 -0
  50. data/spec/examples/model/persistence_spec.rb +201 -0
  51. data/spec/examples/model/properties_spec.rb +81 -0
  52. data/spec/examples/model/scope_spec.rb +677 -0
  53. data/spec/examples/model/serialization_spec.rb +20 -0
  54. data/spec/examples/model/spec_helper.rb +12 -0
  55. data/spec/examples/model/timestamps_spec.rb +52 -0
  56. data/spec/examples/model/translation_spec.rb +23 -0
  57. data/spec/examples/model/validations_spec.rb +86 -0
  58. data/spec/examples/spec_helper.rb +9 -0
  59. data/spec/models/asset.rb +21 -0
  60. data/spec/models/asset_observer.rb +5 -0
  61. data/spec/models/blog.rb +14 -0
  62. data/spec/models/blog_posts.rb +6 -0
  63. data/spec/models/category.rb +9 -0
  64. data/spec/models/comment.rb +12 -0
  65. data/spec/models/photo.rb +5 -0
  66. data/spec/models/post.rb +88 -0
  67. data/spec/models/post_comments.rb +14 -0
  68. data/spec/models/post_observer.rb +43 -0
  69. data/spec/support/helpers.rb +26 -0
  70. data/spec/support/result_stub.rb +27 -0
  71. metadata +125 -23
@@ -0,0 +1,63 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'stringio'
3
+ require 'logger'
4
+
5
+ describe Cequel::Keyspace do
6
+ describe '::batch' do
7
+ it 'should send enclosed write statements in bulk' do
8
+ connection.should_receive(:execute).with(<<CQL, [:id, :title], [1, 'Hey'], :body, 'Body')
9
+ BEGIN BATCH
10
+ INSERT INTO posts (?) VALUES (?)
11
+ UPDATE posts SET ? = ?
12
+ DELETE FROM posts
13
+ APPLY BATCH
14
+ CQL
15
+ cequel.batch do
16
+ cequel[:posts].insert(:id => 1, :title => 'Hey')
17
+ cequel[:posts].update(:body => 'Body')
18
+ cequel[:posts].delete
19
+ end
20
+ end
21
+
22
+ it 'should auto-apply if option given' do
23
+ connection.should_receive(:execute).with(<<CQL, [:id, :title], [1, 'Hey'], :body, 'Body')
24
+ BEGIN BATCH
25
+ INSERT INTO posts (?) VALUES (?)
26
+ UPDATE posts SET ? = ?
27
+ APPLY BATCH
28
+ CQL
29
+ connection.should_receive(:execute).with(<<CQL)
30
+ BEGIN BATCH
31
+ DELETE FROM posts
32
+ APPLY BATCH
33
+ CQL
34
+
35
+
36
+ cequel.batch(:auto_apply => 2) do
37
+ cequel[:posts].insert(:id => 1, :title => 'Hey')
38
+ cequel[:posts].update(:body => 'Body')
39
+ cequel[:posts].delete
40
+ end
41
+ end
42
+
43
+ it 'should do nothing if no statements executed in batch' do
44
+ expect { cequel.batch {} }.to_not raise_error
45
+ end
46
+ end
47
+
48
+ describe '::logger=' do
49
+ let(:io) { StringIO.new }
50
+ let(:logger) { Logger.new(io) }
51
+
52
+ before do
53
+ logger.level = Logger::DEBUG
54
+ cequel.logger = logger
55
+ end
56
+
57
+ it 'should log queries with bind variables resolved' do
58
+ connection.stub(:execute).with("SELECT ? FROM posts", [:id, :title]).and_return result_stub
59
+ cequel[:posts].select(:id, :title).to_a
60
+ io.string.should =~ /CQL \(\d+ms\) SELECT 'id','title' FROM posts/
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,109 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Cequel::Model::Associations do
4
+
5
+ describe '::belongs_to' do
6
+ let(:post) do
7
+ Post.new(:id => 1).tap { |post| post.blog_id = 2 }
8
+ end
9
+
10
+ before do
11
+ connection.stub(:execute).
12
+ with('SELECT * FROM blogs WHERE ? = ? LIMIT 1', :id, 2).
13
+ and_return result_stub(:id => 2, :name => 'Big Data Blog')
14
+ end
15
+
16
+ it 'should query column family and return associated model' do
17
+ post.blog.name.should == 'Big Data Blog'
18
+ end
19
+
20
+ it 'should be nil if foreign key column is nil' do
21
+ post.blog_id = nil
22
+ post.blog.should be_nil
23
+ end
24
+
25
+ it 'should memoize instance' do
26
+ post.blog
27
+ connection.should_not_receive :execute
28
+ post.blog
29
+ end
30
+
31
+ it 'should unmemoize instance if foreign key is changed' do
32
+ post.blog
33
+ post.blog_id = 3
34
+ connection.stub(:execute).
35
+ with('SELECT * FROM blogs WHERE ? = ? LIMIT 1', :id, 3).
36
+ and_return result_stub(:id => 2, :name => 'Another Blog')
37
+
38
+ post.blog.name.should == 'Another Blog'
39
+ end
40
+
41
+ it 'should provide setter for association' do
42
+ post.blog = Blog.new(:id => 3, :name => 'This blog')
43
+ post.blog_id.should == 3
44
+ end
45
+
46
+ it 'should set foreign key to nil if association set to nil' do
47
+ post.blog = Blog.new(:id => 3, :name => 'This blog')
48
+ post.blog = nil
49
+ post.blog_id.should be_nil
50
+ end
51
+
52
+ it 'should save transient associated object' do
53
+ now = Time.now
54
+ Time.stub(:now).and_return now
55
+ post.blog = Blog.new(:id => 3, :name => 'This blog')
56
+ connection.should_receive(:execute).
57
+ with "INSERT INTO blogs (?) VALUES (?)",
58
+ ['id', 'published', 'name', 'updated_at', 'created_at'],
59
+ [3, true, 'This blog', now, now]
60
+ connection.stub(:execute).
61
+ with "INSERT INTO posts (?) VALUES (?)", ['id', 'blog_id'], [post.id, 3]
62
+ post.save
63
+ end
64
+
65
+ end
66
+
67
+ describe '::has_many' do
68
+ let(:blog) do
69
+ Blog.new(:id => 2)
70
+ end
71
+
72
+ before do
73
+ connection.stub(:execute).
74
+ with('SELECT * FROM posts WHERE ? = ?', :blog_id, 2).
75
+ and_return result_stub(
76
+ {:id => 1, :title => 'Cequel'},
77
+ {:id => 2, :title => 'Cequel revisited'}
78
+ )
79
+ end
80
+
81
+ it 'should provide scope for associated instances' do
82
+ blog.posts.map { |post| post.title }.should ==
83
+ ['Cequel', 'Cequel revisited']
84
+ end
85
+
86
+ it 'should destroy associated instances if :dependent => :destroy' do
87
+ connection.stub(:execute).with 'DELETE FROM blogs WHERE ? = ?', :id, 2
88
+ connection.should_receive(:execute).with 'DELETE FROM posts WHERE ? = ?', :id, 1
89
+ connection.should_receive(:execute).with 'DELETE FROM posts WHERE ? = ?', :id, 2
90
+ blog.destroy
91
+ end
92
+ end
93
+
94
+ describe '::has_one' do
95
+ let(:post) { Post.new(:id => 1) }
96
+
97
+ before do
98
+ connection.stub(:execute).
99
+ with("SELECT * FROM assets WHERE ? = ? AND ? = ? LIMIT 1", :class_name, 'Photo', :post_id, 1).
100
+ and_return result_stub(
101
+ {:id => 1, :type => 'Photo', :url => 'http://outofti.me/glamour.jpg'},
102
+ )
103
+ end
104
+
105
+ it 'should look up association by foreign key' do
106
+ post.thumbnail.url.should == 'http://outofti.me/glamour.jpg'
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,79 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Cequel::Model::Callbacks do
4
+ let(:post) do
5
+ connection.stub(:execute).
6
+ with("SELECT * FROM posts WHERE ? = ? LIMIT 1", :id, 1).
7
+ and_return result_stub(:id => 1, :title => 'Cequel')
8
+ Post.find(1)
9
+ end
10
+
11
+ context 'on create' do
12
+ let(:post) { Post.new(:id => 1) }
13
+
14
+ before do
15
+ post.save
16
+ end
17
+
18
+ it 'should invoke save callback' do
19
+ post.should have_callback(:save)
20
+ end
21
+
22
+ it 'should invoke create callback' do
23
+ post.should have_callback(:create)
24
+ end
25
+
26
+ it 'should not invoke update callback' do
27
+ post.should_not have_callback(:update)
28
+ end
29
+
30
+ it 'should not invoke destroy callback' do
31
+ post.should_not have_callback(:destroy)
32
+ end
33
+ end
34
+
35
+ context 'on update' do
36
+ before do
37
+ post.save
38
+ end
39
+
40
+ it 'should invoke save callback' do
41
+ post.should have_callback(:save)
42
+ end
43
+
44
+ it 'should not invoke create callback' do
45
+ post.should_not have_callback(:create)
46
+ end
47
+
48
+ it 'should invoke update callback' do
49
+ post.should have_callback(:update)
50
+ end
51
+
52
+ it 'should not invoke destroy callback' do
53
+ post.should_not have_callback(:destroy)
54
+ end
55
+ end
56
+
57
+ context 'on destroy' do
58
+ before do
59
+ connection.stub(:execute).with("DELETE FROM posts WHERE ? = ?", :id, 1)
60
+ post.destroy
61
+ end
62
+
63
+ it 'should not invoke save callback' do
64
+ post.should_not have_callback(:save)
65
+ end
66
+
67
+ it 'should not invoke create callback' do
68
+ post.should_not have_callback(:create)
69
+ end
70
+
71
+ it 'should not invoke update callback' do
72
+ post.should_not have_callback(:update)
73
+ end
74
+
75
+ it 'should invoke destroy callback' do
76
+ post.should have_callback(:destroy)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,413 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Cequel::Model::Dictionary do
4
+ let(:uuid1) { uuid }
5
+ let(:uuid2) { uuid }
6
+ let(:uuid3) { uuid }
7
+ let(:dictionary) { BlogPosts[1] }
8
+
9
+ describe '#save' do
10
+ before do
11
+ connection.stub(:execute).
12
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
13
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
14
+ connection.stub(:execute).
15
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
16
+ and_return result_stub('blog_id' => 1)
17
+ end
18
+
19
+ it 'should write to row' do
20
+ dictionary[uuid1] = 1
21
+ dictionary[uuid2] = 2
22
+ connection.should_receive(:execute).
23
+ with('UPDATE blog_posts SET ? = ?, ? = ? WHERE ? = ?', uuid1, 1, uuid2, 2, :blog_id, 1)
24
+ dictionary.save
25
+ end
26
+
27
+ it 'should update changed columns' do
28
+ dictionary.load
29
+ dictionary[uuid1] = 2
30
+ connection.should_receive(:execute).
31
+ with('UPDATE blog_posts SET ? = ? WHERE ? = ?', uuid1, 2, :blog_id, 1)
32
+ dictionary.save
33
+ end
34
+
35
+ it 'should write new columns' do
36
+ dictionary.load
37
+ dictionary[uuid3] = 3
38
+ connection.should_receive(:execute).
39
+ with('UPDATE blog_posts SET ? = ? WHERE ? = ?', uuid3, 3, :blog_id, 1)
40
+ dictionary.save
41
+ end
42
+
43
+ it 'should delete removed columns' do
44
+ dictionary.load
45
+ dictionary[uuid1] = nil
46
+ connection.should_receive(:execute).
47
+ with('DELETE ? FROM blog_posts WHERE ? = ?', [uuid1], :blog_id, 1)
48
+ dictionary.save
49
+ end
50
+
51
+ it 'should not update a row if it should be deleted' do
52
+ dictionary.load
53
+ dictionary[uuid1] = 1
54
+ dictionary[uuid1] = nil
55
+ connection.should_receive(:execute).once.
56
+ with('DELETE ? FROM blog_posts WHERE ? = ?', [uuid1], :blog_id, 1)
57
+ dictionary.save
58
+ end
59
+
60
+ it 'should not delete a row if it was subsequently updated' do
61
+ dictionary.load
62
+ dictionary[uuid1] = nil
63
+ dictionary[uuid1] = 1
64
+ connection.should_receive(:execute).once.
65
+ with('UPDATE blog_posts SET ? = ? WHERE ? = ?', uuid1, 1, :blog_id, 1)
66
+ dictionary.save
67
+ end
68
+
69
+ it 'should not save columns that have been saved previously' do
70
+ dictionary.load
71
+ dictionary[uuid3] = 3
72
+ connection.should_receive(:execute).once.
73
+ with('UPDATE blog_posts SET ? = ? WHERE ? = ?', uuid3, 3, :blog_id, 1)
74
+ 2.times { dictionary.save }
75
+ end
76
+
77
+ it 'should not delete columns that have been deleted previously' do
78
+ dictionary.load
79
+ dictionary[uuid1] = nil
80
+ connection.should_receive(:execute).once.
81
+ with('DELETE ? FROM blog_posts WHERE ? = ?', [uuid1], :blog_id, 1)
82
+ 2.times { dictionary.save }
83
+ end
84
+
85
+ end
86
+
87
+ describe '#destroy' do
88
+ before do
89
+ connection.stub(:execute).
90
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
91
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
92
+ connection.stub(:execute).
93
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
94
+ and_return result_stub({'blog_id' => 1})
95
+
96
+ dictionary.load
97
+ connection.should_receive(:execute).
98
+ with('DELETE FROM blog_posts WHERE ? = ?', :blog_id, 1)
99
+ end
100
+
101
+ it 'should delete row from cassandra' do
102
+ dictionary.destroy
103
+ end
104
+
105
+ it 'should remove all properties from memory' do
106
+ dictionary.destroy
107
+ connection.stub(:execute).
108
+ with('SELECT ? FROM blog_posts WHERE ? = ? LIMIT 1', [uuid1], :blog_id, 1).
109
+ and_return result_stub({})
110
+ dictionary[uuid1].should be_nil
111
+ end
112
+
113
+ it 'should not save previously updated properties when saved' do
114
+ dictionary[uuid1] = 5
115
+ dictionary.destroy
116
+ connection.should_not_receive(:execute)
117
+ dictionary.save
118
+ end
119
+
120
+ it 'should not delete previously deleted properties when saved' do
121
+ dictionary[uuid1] = nil
122
+ dictionary.destroy
123
+ connection.should_not_receive(:execute)
124
+ dictionary.save
125
+ end
126
+ end
127
+
128
+ context 'without row in memory' do
129
+
130
+ describe '#each_pair' do
131
+
132
+ it 'should load columns in batches and yield them' do
133
+ connection.should_receive(:execute).
134
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
135
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
136
+ connection.should_receive(:execute).
137
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
138
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
139
+ connection.should_receive(:execute).
140
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
141
+ and_return result_stub({'blog_id' => 1})
142
+ hash = {}
143
+ dictionary.each_pair do |key, value|
144
+ hash[key] = value
145
+ end
146
+ hash.should == {uuid1 => 1, uuid2 => 2, uuid3 => 3}
147
+ end
148
+
149
+ end
150
+
151
+ describe '#[]' do
152
+
153
+ it 'should load column from cassandra' do
154
+ connection.stub(:execute).
155
+ with('SELECT ? FROM blog_posts WHERE ? = ? LIMIT 1', [uuid1], :blog_id, 1).
156
+ and_return result_stub(uuid1 => 1)
157
+ dictionary[uuid1].should == 1
158
+ end
159
+
160
+ end
161
+
162
+ describe '#slice' do
163
+ it 'should load columns from data store' do
164
+ connection.stub(:execute).
165
+ with('SELECT ? FROM blog_posts WHERE ? = ? LIMIT 1', [uuid1,uuid2], :blog_id, 1).
166
+ and_return result_stub(uuid1 => 1, uuid2 => 2)
167
+ dictionary.slice(uuid1, uuid2).should == {uuid1 => 1, uuid2 => 2}
168
+ end
169
+ end
170
+
171
+ describe '#keys' do
172
+ it 'should load keys from data store' do
173
+ connection.should_receive(:execute).
174
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
175
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
176
+ connection.should_receive(:execute).
177
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
178
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
179
+ connection.should_receive(:execute).
180
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
181
+ and_return result_stub({'blog_id' => 1})
182
+ dictionary.keys.should == [uuid1, uuid2, uuid3]
183
+ end
184
+ end
185
+
186
+ describe '#values' do
187
+ it 'should load values from data store' do
188
+ connection.should_receive(:execute).
189
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
190
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
191
+ connection.should_receive(:execute).
192
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
193
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
194
+ connection.should_receive(:execute).
195
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
196
+ and_return result_stub({'blog_id' => 1})
197
+ dictionary.values.should == [1, 2, 3]
198
+ end
199
+ end
200
+
201
+ end
202
+
203
+ context 'with data loaded in memory' do
204
+ before do
205
+ connection.stub(:execute).
206
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
207
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
208
+ connection.stub(:execute).
209
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
210
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
211
+ connection.stub(:execute).
212
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
213
+ and_return result_stub({'blog_id' => 1})
214
+ dictionary.load
215
+ connection.should_not_receive(:execute)
216
+ end
217
+
218
+ describe '#each_pair' do
219
+ it 'should yield data from memory' do
220
+ hash = {}
221
+ dictionary.each_pair do |key, value|
222
+ hash[key] = value
223
+ end
224
+ hash.should == {uuid1 => 1, uuid2 => 2, uuid3 => 3}
225
+ end
226
+ end
227
+
228
+ describe '#[]' do
229
+ it 'should return value from memory' do
230
+ dictionary[uuid1].should == 1
231
+ end
232
+ end
233
+
234
+ describe '#slice' do
235
+ it 'should return slice of data in memory' do
236
+ dictionary.slice(uuid1, uuid2).should == {uuid1 => 1, uuid2 => 2}
237
+ end
238
+ end
239
+
240
+ describe '#keys' do
241
+ it 'should return keys from memory' do
242
+ dictionary.keys.should == [uuid1, uuid2, uuid3]
243
+ end
244
+ end
245
+
246
+ describe '#values' do
247
+ it 'should return values from memory' do
248
+ dictionary.values.should == [1, 2, 3]
249
+ end
250
+ end
251
+ end
252
+
253
+ context 'with data modified but not loaded in memory' do
254
+ let(:uuid4) { uuid }
255
+
256
+ before do
257
+ dictionary[uuid1] = -1
258
+ dictionary[uuid3] = nil
259
+ dictionary[uuid4] = 4
260
+ end
261
+
262
+ describe '#each_pair' do
263
+ it 'should override persisted data with unsaved changes' do
264
+ connection.stub(:execute).
265
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
266
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
267
+ connection.stub(:execute).
268
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
269
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
270
+ connection.stub(:execute).
271
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
272
+ and_return result_stub({'blog_id' => 1})
273
+ hash = {}
274
+ dictionary.each_pair do |key, value|
275
+ hash[key] = value
276
+ end
277
+ hash.should == {uuid1 => -1, uuid2 => 2, uuid4 => 4}
278
+ end
279
+ end
280
+
281
+ describe '#[]' do
282
+ it 'should return unsaved changed value' do
283
+ dictionary[uuid1].should == -1
284
+ end
285
+
286
+ it 'should return nil if value removed' do
287
+ dictionary[uuid3].should be_nil
288
+ end
289
+
290
+ it 'should return unsaved new value' do
291
+ dictionary[uuid4].should == 4
292
+ end
293
+ end
294
+
295
+ describe '#slice' do
296
+ it 'should override loaded slice with unsaved data in memory' do
297
+ connection.stub(:execute).
298
+ with('SELECT ? FROM blog_posts WHERE ? = ? LIMIT 1', [uuid1,uuid2,uuid3,uuid4], :blog_id, 1).
299
+ and_return result_stub(uuid1 => 1, uuid2 => 2, uuid3 => 3)
300
+ dictionary.slice(uuid1, uuid2, uuid3, uuid4).should ==
301
+ {uuid1 => -1, uuid2 => 2, uuid4 => 4}
302
+ end
303
+ end
304
+
305
+ describe '#keys' do
306
+ it 'should override keys that have been added or removed' do
307
+ connection.should_receive(:execute).
308
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
309
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
310
+ connection.should_receive(:execute).
311
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
312
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
313
+ connection.should_receive(:execute).
314
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
315
+ and_return result_stub({'blog_id' => 1})
316
+ dictionary.keys.should == [uuid1, uuid2, uuid4]
317
+ end
318
+ end
319
+
320
+ describe '#values' do
321
+ it 'should override values that have been added or removed' do
322
+ connection.should_receive(:execute).
323
+ with('SELECT FIRST 2 * FROM blog_posts WHERE ? = ? LIMIT 1', :blog_id, 1).
324
+ and_return result_stub('blog_id' => 1, uuid1 => 1, uuid2 => 2)
325
+ connection.should_receive(:execute).
326
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid2, '', :blog_id, 1).
327
+ and_return result_stub('blog_id' => 1, uuid2 => 2, uuid3 => 3)
328
+ connection.should_receive(:execute).
329
+ with('SELECT FIRST 2 ?..? FROM blog_posts WHERE ? = ? LIMIT 1', uuid3, '', :blog_id, 1).
330
+ and_return result_stub({'blog_id' => 1})
331
+ dictionary.values.should == [-1, 2, 4]
332
+ end
333
+ end
334
+ end
335
+
336
+ context 'with serializer/deserializer defined' do
337
+ let(:dictionary) { PostComments[1] }
338
+ let(:comment) { {'user' => 'Mat Brown', 'comment' => 'I like big data.'} }
339
+
340
+ describe '#save' do
341
+ it 'should serialize data' do
342
+ dictionary[4] = comment
343
+ connection.should_receive(:execute).
344
+ with(
345
+ 'UPDATE post_comments SET ? = ? WHERE ? = ?',
346
+ 4, comment.to_json, :post_id, 1
347
+ )
348
+ dictionary.save
349
+ end
350
+ end
351
+
352
+ describe '#[]' do
353
+ it 'should return deserialized data' do
354
+ connection.stub(:execute).with(
355
+ 'SELECT ? FROM post_comments WHERE ? = ? LIMIT 1',
356
+ [4], :post_id, 1
357
+ ).and_return result_stub(4 => comment.to_json)
358
+ dictionary[4].should == comment
359
+ end
360
+ end
361
+
362
+ describe '#slice' do
363
+ it 'should return deserialized values' do
364
+ connection.stub(:execute).with(
365
+ 'SELECT ? FROM post_comments WHERE ? = ? LIMIT 1',
366
+ [4, 5], :post_id, 1
367
+ ).and_return result_stub(4 => comment.to_json)
368
+ dictionary.slice(4, 5).should == {4 => comment}
369
+ end
370
+ end
371
+
372
+ describe '#load' do
373
+ it 'should retain deserialized values in memory' do
374
+ connection.stub(:execute).with(
375
+ 'SELECT FIRST 1000 * FROM post_comments WHERE ? = ? LIMIT 1',
376
+ :post_id, 1
377
+ ).and_return result_stub(4 => comment.to_json)
378
+ dictionary.load
379
+ connection.should_not_receive(:execute)
380
+ dictionary[4].should == comment
381
+ end
382
+ end
383
+
384
+ describe '#each_pair' do
385
+ it 'should yield deserialized values' do
386
+ connection.stub(:execute).
387
+ with(
388
+ 'SELECT FIRST 1000 * FROM post_comments WHERE ? = ? LIMIT 1',
389
+ :post_id, 1
390
+ ).and_return result_stub('post_id' => 1, 4 => comment.to_json)
391
+ dictionary.each_pair.map { |column, comment| comment }.first.
392
+ should == comment
393
+ end
394
+ end
395
+
396
+ describe '#values' do
397
+ it 'should return deserialized values' do
398
+ connection.stub(:execute).
399
+ with(
400
+ 'SELECT FIRST 1000 * FROM post_comments WHERE ? = ? LIMIT 1',
401
+ :post_id, 1
402
+ ).and_return result_stub('post_id' => 1, 4 => comment.to_json)
403
+ dictionary.values.should == [comment]
404
+ end
405
+ end
406
+ end
407
+
408
+ private
409
+
410
+ def uuid
411
+ SimpleUUID::UUID.new.to_guid
412
+ end
413
+ end