cequel 0.0.0 → 0.4.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 (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