low_card_tables 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +59 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE +21 -0
  6. data/README.md +75 -0
  7. data/Rakefile +6 -0
  8. data/lib/low_card_tables.rb +72 -0
  9. data/lib/low_card_tables/active_record/base.rb +55 -0
  10. data/lib/low_card_tables/active_record/migrations.rb +223 -0
  11. data/lib/low_card_tables/active_record/relation.rb +35 -0
  12. data/lib/low_card_tables/active_record/scoping.rb +87 -0
  13. data/lib/low_card_tables/errors.rb +74 -0
  14. data/lib/low_card_tables/has_low_card_table/base.rb +114 -0
  15. data/lib/low_card_tables/has_low_card_table/low_card_association.rb +273 -0
  16. data/lib/low_card_tables/has_low_card_table/low_card_associations_manager.rb +143 -0
  17. data/lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb +224 -0
  18. data/lib/low_card_tables/has_low_card_table/low_card_objects_manager.rb +80 -0
  19. data/lib/low_card_tables/low_card_table/base.rb +184 -0
  20. data/lib/low_card_tables/low_card_table/cache.rb +214 -0
  21. data/lib/low_card_tables/low_card_table/cache_expiration/exponential_cache_expiration_policy.rb +151 -0
  22. data/lib/low_card_tables/low_card_table/cache_expiration/fixed_cache_expiration_policy.rb +23 -0
  23. data/lib/low_card_tables/low_card_table/cache_expiration/has_cache_expiration.rb +100 -0
  24. data/lib/low_card_tables/low_card_table/cache_expiration/no_caching_expiration_policy.rb +13 -0
  25. data/lib/low_card_tables/low_card_table/cache_expiration/unlimited_cache_expiration_policy.rb +13 -0
  26. data/lib/low_card_tables/low_card_table/row_collapser.rb +175 -0
  27. data/lib/low_card_tables/low_card_table/row_manager.rb +681 -0
  28. data/lib/low_card_tables/low_card_table/table_unique_index.rb +134 -0
  29. data/lib/low_card_tables/version.rb +4 -0
  30. data/lib/low_card_tables/version_support.rb +52 -0
  31. data/low_card_tables.gemspec +69 -0
  32. data/spec/low_card_tables/helpers/database_helper.rb +148 -0
  33. data/spec/low_card_tables/helpers/query_spy_helper.rb +47 -0
  34. data/spec/low_card_tables/helpers/system_helpers.rb +63 -0
  35. data/spec/low_card_tables/system/basic_system_spec.rb +254 -0
  36. data/spec/low_card_tables/system/bulk_system_spec.rb +334 -0
  37. data/spec/low_card_tables/system/caching_system_spec.rb +531 -0
  38. data/spec/low_card_tables/system/migrations_system_spec.rb +747 -0
  39. data/spec/low_card_tables/system/options_system_spec.rb +581 -0
  40. data/spec/low_card_tables/system/queries_system_spec.rb +142 -0
  41. data/spec/low_card_tables/system/validations_system_spec.rb +88 -0
  42. data/spec/low_card_tables/unit/active_record/base_spec.rb +53 -0
  43. data/spec/low_card_tables/unit/active_record/migrations_spec.rb +207 -0
  44. data/spec/low_card_tables/unit/active_record/relation_spec.rb +47 -0
  45. data/spec/low_card_tables/unit/active_record/scoping_spec.rb +101 -0
  46. data/spec/low_card_tables/unit/has_low_card_table/base_spec.rb +79 -0
  47. data/spec/low_card_tables/unit/has_low_card_table/low_card_association_spec.rb +287 -0
  48. data/spec/low_card_tables/unit/has_low_card_table/low_card_associations_manager_spec.rb +190 -0
  49. data/spec/low_card_tables/unit/has_low_card_table/low_card_dynamic_method_manager_spec.rb +234 -0
  50. data/spec/low_card_tables/unit/has_low_card_table/low_card_objects_manager_spec.rb +70 -0
  51. data/spec/low_card_tables/unit/low_card_table/base_spec.rb +207 -0
  52. data/spec/low_card_tables/unit/low_card_table/cache_expiration/exponential_cache_expiration_policy_spec.rb +128 -0
  53. data/spec/low_card_tables/unit/low_card_table/cache_expiration/fixed_cache_expiration_policy_spec.rb +25 -0
  54. data/spec/low_card_tables/unit/low_card_table/cache_expiration/has_cache_expiration_policy_spec.rb +100 -0
  55. data/spec/low_card_tables/unit/low_card_table/cache_expiration/no_caching_expiration_policy_spec.rb +14 -0
  56. data/spec/low_card_tables/unit/low_card_table/cache_expiration/unlimited_cache_expiration_policy_spec.rb +14 -0
  57. data/spec/low_card_tables/unit/low_card_table/cache_spec.rb +282 -0
  58. data/spec/low_card_tables/unit/low_card_table/row_collapser_spec.rb +109 -0
  59. data/spec/low_card_tables/unit/low_card_table/row_manager_spec.rb +918 -0
  60. data/spec/low_card_tables/unit/low_card_table/table_unique_index_spec.rb +117 -0
  61. metadata +206 -0
@@ -0,0 +1,14 @@
1
+ require 'low_card_tables'
2
+ require 'active_support/time'
3
+
4
+ describe LowCardTables::LowCardTable::CacheExpiration::NoCachingExpirationPolicy do
5
+ it "should always be stale" do
6
+ instance = LowCardTables::LowCardTable::CacheExpiration::NoCachingExpirationPolicy.new
7
+
8
+ start_time = 10.minutes.ago
9
+ instance.stale?(start_time, start_time).should be
10
+ instance.stale?(start_time, start_time + 1.minute).should be
11
+ instance.stale?(start_time, start_time + 10.minutes).should be
12
+ instance.stale?(start_time, start_time + 100.minutes).should be
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require 'low_card_tables'
2
+ require 'active_support/time'
3
+
4
+ describe LowCardTables::LowCardTable::CacheExpiration::UnlimitedCacheExpirationPolicy do
5
+ it "should never be stale" do
6
+ instance = LowCardTables::LowCardTable::CacheExpiration::UnlimitedCacheExpirationPolicy.new
7
+
8
+ start_time = 10.minutes.ago
9
+ instance.stale?(start_time, start_time).should_not be
10
+ instance.stale?(start_time, start_time + 1.minute).should_not be
11
+ instance.stale?(start_time, start_time + 10.minutes).should_not be
12
+ instance.stale?(start_time, start_time + 100.minutes).should_not be
13
+ end
14
+ end
@@ -0,0 +1,282 @@
1
+ require 'low_card_tables'
2
+
3
+ describe LowCardTables::LowCardTable::Cache do
4
+ def klass
5
+ LowCardTables::LowCardTable::Cache
6
+ end
7
+
8
+ it "should require a low-card table class" do
9
+ mc = double("model_class")
10
+
11
+ lambda { klass.new(mc) }.should raise_error(ArgumentError)
12
+
13
+ allow(mc).to receive(:is_low_card_table?).and_return(false)
14
+
15
+ lambda { klass.new(mc) }.should raise_error(ArgumentError)
16
+ end
17
+
18
+ context "with an instance" do
19
+ before :each do
20
+ klass.class_eval do
21
+ class << self
22
+ def override_time=(x)
23
+ @override_time = x
24
+ end
25
+
26
+ def override_time
27
+ out = @override_time
28
+ @override_time = nil
29
+ out
30
+ end
31
+ end
32
+
33
+ def current_time
34
+ self.class.override_time || Time.now
35
+ end
36
+ end
37
+
38
+ @mc = double("model_class")
39
+ allow(@mc).to receive(:is_low_card_table?).and_return(true)
40
+ allow(@mc).to receive(:low_card_ensure_has_unique_index!)
41
+ allow(@mc).to receive(:primary_key).and_return("foobar")
42
+ allow(@mc).to receive(:table_name).and_return("barbaz")
43
+
44
+ im1 = double("im1")
45
+ expect(@mc).to receive(:order).once.with("foobar ASC").and_return(im1)
46
+ im2 = double("im2")
47
+ expect(im1).to receive(:limit).once.with(5001).and_return(im2)
48
+
49
+ @row1 = double("row1")
50
+ @row2 = double("row2")
51
+ @row3 = double("row3")
52
+
53
+ allow(@row1).to receive(:id).and_return(1234)
54
+ allow(@row2).to receive(:id).and_return(1235)
55
+ allow(@row3).to receive(:id).and_return(1238)
56
+
57
+ expect(im2).to receive(:to_a).once.and_return([ @row1, @row2, @row3 ])
58
+
59
+ @rows_read_at_time = double("rows_read_at_time")
60
+ klass.override_time = @rows_read_at_time
61
+
62
+ @cache = klass.new(@mc)
63
+ end
64
+
65
+ it "should correctly expose the time rows were read" do
66
+ @cache.loaded_at.should be(@rows_read_at_time)
67
+ end
68
+
69
+ it "should return all the rows" do
70
+ @cache.all_rows.sort_by(&:object_id).should == [ @row1, @row2, @row3 ].sort_by(&:object_id)
71
+ end
72
+
73
+ describe "#rows_matching" do
74
+ it "should fail if given no hashes" do
75
+ lambda { @cache.rows_matching([ ]) }.should raise_error(ArgumentError)
76
+ end
77
+
78
+ it "should fail if given something that isn't a Hash" do
79
+ lambda { @cache.rows_matching([ 12345 ]) }.should raise_error(ArgumentError)
80
+ end
81
+
82
+ it "should fail if neither a hash nor a block" do
83
+ lambda { @cache.rows_matching }.should raise_error(ArgumentError)
84
+ end
85
+
86
+ it "should fail if given both a hash and a block" do
87
+ lambda { @cache.rows_matching({ :foo => :bar }) { |x| true } }.should raise_error(ArgumentError)
88
+ end
89
+
90
+ it "should return a Hash if given an array of Hashes" do
91
+ h1_orig = { :a => :b, :c => :d }
92
+ h2_orig = { :foo => :bar }
93
+ h3_orig = { :bar => :baz }
94
+
95
+ h1 = h1_orig.with_indifferent_access
96
+ h2 = h2_orig.with_indifferent_access
97
+ h3 = h3_orig.with_indifferent_access
98
+
99
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
100
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h2 ]).and_return(true)
101
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h3 ]).and_return(false)
102
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
103
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h2 ]).and_return(true)
104
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h3 ]).and_return(false)
105
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
106
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h2 ]).and_return(false)
107
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h3 ]).and_return(false)
108
+
109
+ result = @cache.rows_matching([ h1_orig, h2_orig, h3_orig ])
110
+ result.class.should == Hash
111
+ result.size.should == 3
112
+
113
+ result[h1_orig].length.should == 1
114
+ result[h1_orig][0].should be(@row1)
115
+
116
+ result[h2_orig].length.should == 2
117
+ result[h2_orig].sort_by(&:object_id).should == [ @row1, @row2 ].sort_by(&:object_id)
118
+
119
+ result[h3_orig].class.should == Array
120
+ result[h3_orig].length.should == 0
121
+ end
122
+
123
+ it "should return a Hash if given an array of just one Hash" do
124
+ h1_orig = { :a => :b, :c => :d }
125
+ h1 = h1_orig.with_indifferent_access
126
+
127
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
128
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
129
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
130
+
131
+ result = @cache.rows_matching([ h1_orig ])
132
+ result.class.should == Hash
133
+ result.size.should == 1
134
+
135
+ result[h1_orig].length.should == 1
136
+ result[h1_orig][0].should be(@row1)
137
+ end
138
+
139
+ it "should return an Array if given a Hash" do
140
+ h1_orig = { :a => :b, :c => :d }
141
+ h1 = h1_orig.with_indifferent_access
142
+
143
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
144
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
145
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
146
+
147
+ result = @cache.rows_matching(h1_orig)
148
+ result.class.should == Array
149
+ result.sort_by(&:object_id).should == [ @row1, @row3 ].sort_by(&:object_id)
150
+ end
151
+
152
+ it "should return an Array if given a Block" do
153
+ block = lambda { }
154
+
155
+ expect(@row1).to receive(:_low_card_row_matches_block?).once.with(block).and_return(true)
156
+ expect(@row2).to receive(:_low_card_row_matches_block?).once.with(block).and_return(true)
157
+ expect(@row3).to receive(:_low_card_row_matches_block?).once.with(block).and_return(false)
158
+
159
+ result = @cache.rows_matching(&block)
160
+ result.class.should == Array
161
+ result.sort_by(&:object_id).should == [ @row1, @row2 ].sort_by(&:object_id)
162
+ end
163
+ end
164
+
165
+ describe "#ids_matching" do
166
+ it "should fail if given no hashes" do
167
+ lambda { @cache.ids_matching([ ]) }.should raise_error(ArgumentError)
168
+ end
169
+
170
+ it "should fail if given something that isn't a Hash" do
171
+ lambda { @cache.ids_matching([ 12345 ]) }.should raise_error(ArgumentError)
172
+ end
173
+
174
+ it "should fail if neither a hash nor a block" do
175
+ lambda { @cache.ids_matching }.should raise_error(ArgumentError)
176
+ end
177
+
178
+ it "should fail if given both a hash and a block" do
179
+ lambda { @cache.ids_matching({ :foo => :bar }) { |x| true } }.should raise_error(ArgumentError)
180
+ end
181
+
182
+ it "should return a Hash if given an array of Hashes" do
183
+ h1_orig = { :a => :b, :c => :d }
184
+ h2_orig = { :foo => :bar }
185
+ h3_orig = { :bar => :baz }
186
+
187
+ h1 = h1_orig.with_indifferent_access
188
+ h2 = h2_orig.with_indifferent_access
189
+ h3 = h3_orig.with_indifferent_access
190
+
191
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
192
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h2 ]).and_return(true)
193
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h3 ]).and_return(false)
194
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
195
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h2 ]).and_return(true)
196
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h3 ]).and_return(false)
197
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
198
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h2 ]).and_return(false)
199
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h3 ]).and_return(false)
200
+
201
+ result = @cache.ids_matching([ h1_orig, h2_orig, h3_orig ])
202
+ result.class.should == Hash
203
+ result.size.should == 3
204
+
205
+ result[h1_orig].length.should == 1
206
+ result[h1_orig][0].should == @row1.id
207
+
208
+ result[h2_orig].length.should == 2
209
+ result[h2_orig].sort.should == [ @row1.id, @row2.id ].sort
210
+
211
+ result[h3_orig].class.should == Array
212
+ result[h3_orig].length.should == 0
213
+ end
214
+
215
+ it "should return a Hash if given an array of just one Hash" do
216
+ h1_orig = { :a => :b, :c => :d }
217
+ h1 = h1_orig.with_indifferent_access
218
+
219
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
220
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
221
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
222
+
223
+ result = @cache.ids_matching([ h1_orig ])
224
+ result.class.should == Hash
225
+ result.size.should == 1
226
+
227
+ result[h1_orig].length.should == 1
228
+ result[h1_orig][0].should == @row1.id
229
+ end
230
+
231
+ it "should return an Array if given a Hash" do
232
+ h1_orig = { :a => :b, :c => :d }
233
+ h1 = h1_orig.with_indifferent_access
234
+
235
+ expect(@row1).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
236
+ expect(@row2).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(false)
237
+ expect(@row3).to receive(:_low_card_row_matches_any_hash?).once.with([ h1 ]).and_return(true)
238
+
239
+ result = @cache.ids_matching(h1_orig)
240
+ result.class.should == Array
241
+ result.sort.should == [ @row1.id, @row3.id ].sort
242
+ end
243
+
244
+ it "should return an Array if given a Block" do
245
+ block = lambda { }
246
+
247
+ expect(@row1).to receive(:_low_card_row_matches_block?).once.with(block).and_return(true)
248
+ expect(@row2).to receive(:_low_card_row_matches_block?).once.with(block).and_return(true)
249
+ expect(@row3).to receive(:_low_card_row_matches_block?).once.with(block).and_return(false)
250
+
251
+ result = @cache.ids_matching(&block)
252
+ result.class.should == Array
253
+ result.sort.should == [ @row1.id, @row2.id ].sort
254
+ end
255
+ end
256
+
257
+ describe "#rows_for_ids" do
258
+ it "should return a single row for a single ID" do
259
+ @cache.rows_for_ids(@row2.id).should be(@row2)
260
+ end
261
+
262
+ it "should return a map for multiple IDs" do
263
+ result = @cache.rows_for_ids([ @row1.id, @row2.id ])
264
+ result.class.should == Hash
265
+ result.size.should == 2
266
+ result[@row1.id].should be(@row1)
267
+ result[@row2.id].should be(@row2)
268
+ end
269
+
270
+ it "should return a map for a single ID, passed in an array" do
271
+ result = @cache.rows_for_ids([ @row3.id ])
272
+ result.class.should == Hash
273
+ result.size.should == 1
274
+ result[@row3.id].should be(@row3)
275
+ end
276
+
277
+ it "should blow up if passed a missing ID" do
278
+ lambda { @cache.rows_for_ids([ @row1.id, 98765, @row3.id ]) }.should raise_error(LowCardTables::Errors::LowCardIdNotFoundError, /98765/)
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,109 @@
1
+ require 'low_card_tables'
2
+
3
+ describe LowCardTables::LowCardTable::RowCollapser do
4
+ def klass
5
+ LowCardTables::LowCardTable::RowCollapser
6
+ end
7
+
8
+ it "should require a low-card table to be created" do
9
+ low_card_model = double("low_card_model")
10
+
11
+ lambda { klass.new(low_card_model, { }) }.should raise_error(ArgumentError)
12
+
13
+ allow(low_card_model).to receive(:is_low_card_table?).and_return(false)
14
+ lambda { klass.new(low_card_model, { }) }.should raise_error(ArgumentError)
15
+ end
16
+
17
+ context "with an instance" do
18
+ before :each do
19
+ @low_card_model = double("low_card_model")
20
+ allow(@low_card_model).to receive(:is_low_card_table?).and_return(true)
21
+
22
+ allow(@low_card_model).to receive(:low_card_value_column_names).and_return(%w{foo bar})
23
+
24
+ @referring_model_1 = double("referring_model_1")
25
+ @referring_model_2 = double("referring_model_2")
26
+
27
+ allow(@low_card_model).to receive(:low_card_referring_models).and_return([ @referring_model_1, @referring_model_2 ])
28
+
29
+ @row1 = double("row_1")
30
+ @row2 = double("row_2")
31
+ @row3 = double("row_3")
32
+ @row4 = double("row_4")
33
+ @row5 = double("row_5")
34
+ @row6 = double("row_6")
35
+
36
+ allow(@row1).to receive(:attributes).and_return({ 'foo' => "a", 'bar' => 1, 'irrelevant' => 'yo1' })
37
+ allow(@row2).to receive(:attributes).and_return({ 'foo' => "a", 'bar' => 1, 'irrelevant' => 'yo2' })
38
+ allow(@row3).to receive(:attributes).and_return({ 'foo' => "a", 'bar' => 1, 'irrelevant' => 'yo3' })
39
+ allow(@row4).to receive(:attributes).and_return({ 'foo' => "b", 'bar' => 1, 'irrelevant' => 'yo4' })
40
+ allow(@row5).to receive(:attributes).and_return({ 'foo' => "b", 'bar' => 1, 'irrelevant' => 'yo5' })
41
+ allow(@row6).to receive(:attributes).and_return({ 'foo' => "b", 'bar' => 1, 'irrelevant' => 'yo6' })
42
+
43
+ allow(@row1).to receive(:id).and_return(1)
44
+ allow(@row2).to receive(:id).and_return(2)
45
+ allow(@row3).to receive(:id).and_return(3)
46
+ allow(@row4).to receive(:id).and_return(4)
47
+ allow(@row5).to receive(:id).and_return(5)
48
+ allow(@row6).to receive(:id).and_return(6)
49
+ end
50
+
51
+ def use(options)
52
+ @instance = klass.new(@low_card_model, options)
53
+ end
54
+
55
+ describe "#collapse!" do
56
+ it "should do nothing if :low_card_collapse_rows => false" do
57
+ use(:low_card_collapse_rows => false)
58
+
59
+ @instance.collapse!
60
+ end
61
+
62
+ it "should do nothing if there are no duplicate rows" do
63
+ use({ })
64
+ expect(@low_card_model).to receive(:all).and_return([ @row1, @row4 ])
65
+
66
+ @instance.collapse!
67
+ end
68
+
69
+ context "actual collapsing" do
70
+ before :each do
71
+ expect(@low_card_model).to receive(:all).and_return([ @row1, @row2, @row3, @row4, @row5, @row6 ])
72
+ expect(@low_card_model).to receive(:delete_all).once.with([ "id IN (:ids)", { :ids => [ 2, 3, 5, 6 ]} ])
73
+
74
+ @expected_collapse_map = { @row1 => [ @row2, @row3 ], @row4 => [ @row5, @row6 ]}
75
+ end
76
+
77
+ it "should skip updating referring models if asked to" do
78
+ use({ :low_card_update_referring_models => false })
79
+ @instance.collapse!.should == @expected_collapse_map
80
+ end
81
+
82
+ context "with referring models updated" do
83
+ before :each do
84
+ expect(@low_card_model).to receive(:transaction).once { |*args, &block| block.call }
85
+ expect(@referring_model_1).to receive(:transaction).once { |*args, &block| block.call }
86
+ expect(@referring_model_2).to receive(:transaction).once { |*args, &block| block.call }
87
+
88
+ expect(@referring_model_1).to receive(:_low_card_update_collapsed_rows).once.with(@low_card_model, @expected_collapse_map)
89
+ expect(@referring_model_2).to receive(:_low_card_update_collapsed_rows).once.with(@low_card_model, @expected_collapse_map)
90
+ end
91
+
92
+ it "should collapse duplicate rows properly" do
93
+ use({ })
94
+ @instance.collapse!.should == @expected_collapse_map
95
+ end
96
+
97
+ it "should add additional referring models if asked to" do
98
+ additional_referring_model = double("additional_referring_model")
99
+ expect(additional_referring_model).to receive(:transaction).once { |*args, &block| block.call }
100
+ expect(additional_referring_model).to receive(:_low_card_update_collapsed_rows).once.with(@low_card_model, @expected_collapse_map)
101
+
102
+ use({ :low_card_referrers => [ @referring_model_1, additional_referring_model ]})
103
+ @instance.collapse!.should == @expected_collapse_map
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,918 @@
1
+ require 'low_card_tables'
2
+
3
+ # Note: later on in this spec, there's a bunch of unexpected madness -- it looks like we're going well out of our way
4
+ # to validate arguments to methods in a roundabout way, using Array#detect and so on, instead of just setting
5
+ # expectations. But we're doing this for a very good reason: we can't guarantee order of arguments to a number of
6
+ # methods since Ruby < 1.9 doesn't guarantee hash order.
7
+ describe LowCardTables::LowCardTable::RowManager do
8
+ def klass
9
+ LowCardTables::LowCardTable::RowManager
10
+ end
11
+
12
+ def capture_exception(&block)
13
+ out = begin
14
+ block.call
15
+ nil
16
+ rescue Exception => e
17
+ e
18
+ end
19
+
20
+ raise "exception expected, but not raised" unless e
21
+ e
22
+ end
23
+
24
+ before :each do
25
+ klass.class_eval do
26
+ class << self
27
+ def next_time
28
+ if @next_time
29
+ @next_time += 1
30
+ @next_time
31
+ end
32
+ end
33
+
34
+ def next_time=(x)
35
+ @next_time = x
36
+ end
37
+ end
38
+
39
+ def current_time
40
+ self.class.next_time || Time.now
41
+ end
42
+ end
43
+
44
+ LowCardTables::LowCardTable::Cache.class_eval do
45
+ def current_time
46
+ LowCardTables::LowCardTable::RowManager.next_time || Time.now
47
+ end
48
+ end
49
+
50
+ klass.next_time = 0
51
+
52
+ @cache_loads = [ ]
53
+ cl = @cache_loads
54
+ ::ActiveSupport::Notifications.subscribe("low_card_tables.cache_load") do |name, start, finish, id, payload|
55
+ cl << payload
56
+ end
57
+
58
+ @cache_flushes = [ ]
59
+ cf = @cache_flushes
60
+ ::ActiveSupport::Notifications.subscribe("low_card_tables.cache_flush") do |name, start, finish, id, payload|
61
+ cf << payload
62
+ end
63
+
64
+ @rows_created = [ ]
65
+ rc = @rows_created
66
+ ::ActiveSupport::Notifications.subscribe("low_card_tables.rows_created") do |name, start, finish, id, payload|
67
+ rc << payload
68
+ end
69
+ end
70
+
71
+ after :each do
72
+ klass.next_time = nil
73
+ end
74
+
75
+ it "should require a low-card model for instantiation" do
76
+ low_card_model = double("low_card_model")
77
+ lambda { klass.new(low_card_model) }.should raise_error(ArgumentError)
78
+
79
+ allow(low_card_model).to receive(:is_low_card_table?).and_return(false)
80
+ lambda { klass.new(low_card_model) }.should raise_error(ArgumentError)
81
+ end
82
+
83
+ context "with an instance" do
84
+ before :each do
85
+ @low_card_model = double("low_card_model")
86
+ allow(@low_card_model).to receive(:is_low_card_table?).and_return(true)
87
+ allow(@low_card_model).to receive(:low_card_options).and_return({ :foo => :bar })
88
+ allow(@low_card_model).to receive(:reset_column_information)
89
+ allow(@low_card_model).to receive(:table_exists?).and_return(true)
90
+ allow(@low_card_model).to receive(:table_name).and_return("thetablename")
91
+
92
+ @table_unique_index = double("table_unique_index")
93
+ allow(LowCardTables::LowCardTable::TableUniqueIndex).to receive(:new).with(@low_card_model).and_return(@table_unique_index)
94
+
95
+ @column_id = double("column_id")
96
+ allow(@column_id).to receive(:name).and_return("id")
97
+ allow(@column_id).to receive(:primary).and_return(true)
98
+ @column_foo = double("column_foo")
99
+ allow(@column_foo).to receive(:name).and_return("foo")
100
+ allow(@column_foo).to receive(:primary).and_return(false)
101
+ allow(@column_foo).to receive(:default).and_return(nil)
102
+ @column_bar = double("column_bar")
103
+ allow(@column_bar).to receive(:name).and_return("bar")
104
+ allow(@column_bar).to receive(:primary).and_return(false)
105
+ allow(@column_bar).to receive(:default).and_return('yohoho')
106
+ allow(@low_card_model).to receive(:columns).and_return([ @column_id, @column_foo, @column_bar ])
107
+
108
+ @cache_expiration_policy = double("cache_expiration_policy")
109
+ allow(@low_card_model).to receive(:low_card_cache_expiration_policy_object).and_return(@cache_expiration_policy)
110
+
111
+ @instance = klass.new(@low_card_model)
112
+
113
+ @created_cache_count = 0
114
+ @expected_caches = [ ]
115
+ ec = @expected_caches
116
+
117
+ allow(LowCardTables::LowCardTable::Cache).to receive(:new) do
118
+ if ec.length > 0
119
+ @expected_caches.shift
120
+ else
121
+ raise "created a cache that we didn't expect to create"
122
+ end
123
+ end
124
+
125
+ @expected_stale_calls = [ ]
126
+ esc = @expected_stale_calls
127
+
128
+ allow(@cache_expiration_policy).to receive(:stale?) do |cache_time, current_time|
129
+ if esc.length > 0
130
+ (expected_cache_time, expected_current_time, stale) = esc.shift
131
+ if (cache_time != expected_cache_time) || (current_time != expected_current_time)
132
+ raise "incorrect call to stale?; cache time: expected #{expected_cache_time}, got #{cache_time}; current time: expected #{expected_current_time}, got #{current_time}"
133
+ end
134
+
135
+ stale
136
+ else
137
+ raise "unexpected call to stale? (#{cache_time}, #{current_time})"
138
+ end
139
+ end
140
+ end
141
+
142
+ after :each do
143
+ if @expected_caches.length > 0
144
+ raise "didn't create as many caches as expected; still have: #{@expected_caches.inspect}"
145
+ end
146
+
147
+ if @expected_stale_calls.length > 0
148
+ raise "didn't call stale? as many times as expected; still have: #{@expected_stale_calls.inspect}"
149
+ end
150
+ end
151
+
152
+ def expect_cache_creation
153
+ @created_cache_count += 1
154
+ cache = double("cache-#{@created_cache_count}")
155
+ @expected_caches << cache
156
+ cache
157
+ end
158
+
159
+ def expect_cache_validation(cache, expected_cache_time, expected_current_time, stale)
160
+ expect(cache).to receive(:loaded_at).once.and_return(expected_cache_time)
161
+ @expected_stale_calls << [ expected_cache_time, expected_current_time, stale ]
162
+ end
163
+
164
+ describe "referring models" do
165
+ before :each do
166
+ @referring_class_1 = double("referring_class_1")
167
+ @referring_class_2 = double("referring_class_2")
168
+ end
169
+
170
+ it "should have no referring models, by default" do
171
+ @instance.referring_models.should == [ ]
172
+ end
173
+
174
+ it "should unify and return referring models" do
175
+ @instance.referred_to_by(@referring_class_1)
176
+ @instance.referred_to_by(@referring_class_2)
177
+ @instance.referred_to_by(@referring_class_1)
178
+
179
+ @instance.referring_models.sort_by(&:object_id).should == [ @referring_class_1, @referring_class_2 ].sort_by(&:object_id)
180
+ end
181
+
182
+ it "should tell all referring models when column information is reset" do
183
+ @instance.referred_to_by(@referring_class_1)
184
+ @instance.referred_to_by(@referring_class_2)
185
+
186
+ lcam_1 = double("lcam_1")
187
+ lcam_2 = double("lcam_2")
188
+
189
+ expect(@referring_class_1).to receive(:_low_card_associations_manager).once.and_return(lcam_1)
190
+ expect(@referring_class_2).to receive(:_low_card_associations_manager).once.and_return(lcam_2)
191
+
192
+ expect(lcam_1).to receive(:low_card_column_information_reset!).once.with(@low_card_model)
193
+ expect(lcam_2).to receive(:low_card_column_information_reset!).once.with(@low_card_model)
194
+
195
+ @instance.column_information_reset!
196
+ end
197
+ end
198
+
199
+ describe "cache management" do
200
+ it "should return #all_rows directly from cache, and notify that it loaded a cache" do
201
+ cache = expect_cache_creation
202
+ expect(cache).to receive(:all_rows).once.and_return(:allrows)
203
+
204
+ @instance.all_rows.should == :allrows
205
+
206
+ @cache_loads.length.should == 1
207
+ @cache_loads[0].should == { :low_card_model => @low_card_model }
208
+ @cache_flushes.length.should == 0
209
+ end
210
+
211
+ it "should check the cache on the second call, and use that cache" do
212
+ cache = expect_cache_creation
213
+ expect(cache).to receive(:all_rows).twice.and_return(:allrows)
214
+ expect_cache_validation(cache, 1, 2, false)
215
+
216
+ @instance.all_rows.should == :allrows
217
+ @instance.all_rows.should == :allrows
218
+
219
+ @cache_loads.length.should == 1
220
+ @cache_loads[0].should == { :low_card_model => @low_card_model }
221
+ @cache_flushes.length.should == 0
222
+ end
223
+
224
+ it "should refresh the cache on the second call, if needed; also, reset column information when doing so, and fire both load and flush notifications" do
225
+ cache1 = expect_cache_creation
226
+ expect(cache1).to receive(:all_rows).once.and_return(:allrows1)
227
+ expect_cache_validation(cache1, 1, 2, true)
228
+
229
+ cache2 = expect_cache_creation
230
+ expect(cache2).to receive(:all_rows).once.and_return(:allrows2)
231
+
232
+ expect(@low_card_model).to receive(:reset_column_information).once
233
+
234
+ @instance.all_rows.should == :allrows1
235
+ @instance.all_rows.should == :allrows2
236
+
237
+ @cache_loads.length.should == 2
238
+ @cache_loads[0].should == { :low_card_model => @low_card_model }
239
+ @cache_loads[1].should == { :low_card_model => @low_card_model }
240
+ @cache_flushes.length.should == 1
241
+ @cache_flushes[0].should == { :reason => :stale, :low_card_model => @low_card_model, :now => 2, :loaded => 1 }
242
+ end
243
+
244
+ it "should only reset column information if asked to flush the cache when there isn't one, and not fire a notification" do
245
+ expect(@low_card_model).to receive(:reset_column_information).once
246
+
247
+ @instance.flush_cache!
248
+ @cache_loads.length.should == 0
249
+ @cache_flushes.length.should == 0
250
+ end
251
+
252
+ it "should flush the cache if asked to manually, and fire a notification" do
253
+ cache1 = expect_cache_creation
254
+ expect(cache1).to receive(:all_rows).once.and_return(:allrows1)
255
+
256
+ cache2 = expect_cache_creation
257
+ expect(cache2).to receive(:all_rows).once.and_return(:allrows2)
258
+
259
+ expect(@low_card_model).to receive(:reset_column_information).once
260
+
261
+ @instance.all_rows.should == :allrows1
262
+
263
+ @instance.flush_cache!
264
+
265
+ @instance.all_rows.should == :allrows2
266
+
267
+ @cache_loads.length.should == 2
268
+ @cache_loads[0].should == { :low_card_model => @low_card_model }
269
+ @cache_loads[1].should == { :low_card_model => @low_card_model }
270
+
271
+ @cache_flushes.length.should == 1
272
+ @cache_flushes[0].should == { :low_card_model => @low_card_model, :reason => :manually_requested }
273
+ end
274
+ end
275
+
276
+ %w{rows_for_ids row_for_id}.each do |method_name|
277
+ describe "##{method_name}" do
278
+ it "should return a single row from cache if present" do
279
+ row1 = double("row1")
280
+ cache = expect_cache_creation
281
+ expect(cache).to receive(:rows_for_ids).with(12345).and_return(row1)
282
+
283
+ @instance.send(method_name, 12345).should be(row1)
284
+
285
+ @cache_loads.length.should == 1
286
+ @cache_loads[0].should == { :low_card_model => @low_card_model }
287
+ @cache_flushes.length.should == 0
288
+ end
289
+
290
+ it "should flush the cache and try again if not present" do
291
+ row1 = double("row1")
292
+ cache1 = expect_cache_creation
293
+ expect(cache1).to receive(:rows_for_ids).with(12345).and_raise(LowCardTables::Errors::LowCardIdNotFoundError.new("not found yo", 12345))
294
+
295
+ cache2 = expect_cache_creation
296
+ expect(cache2).to receive(:rows_for_ids).with(12345).and_return(row1)
297
+
298
+ @instance.send(method_name, 12345).should be(row1)
299
+
300
+ @cache_loads.length.should == 2
301
+ @cache_loads[0].should == { :low_card_model => @low_card_model }
302
+ @cache_loads[1].should == { :low_card_model => @low_card_model }
303
+
304
+ @cache_flushes.length.should == 1
305
+ @cache_flushes[0].should == { :low_card_model => @low_card_model, :reason => :id_not_found, :ids => 12345 }
306
+ end
307
+ end
308
+ end
309
+
310
+ %w{ids_matching rows_matching}.each do |method_name|
311
+ describe "##{method_name}" do
312
+ it "should call through to the cache" do
313
+ hash = { :foo => 'bar' }
314
+
315
+ cache = expect_cache_creation
316
+ expect(cache).to receive(method_name).once.with([ { :foo => "bar" } ]).and_return({ { :foo => :bar } => :foobar })
317
+
318
+ @instance.send(method_name, hash).should == :foobar
319
+ end
320
+
321
+ it "should reject hashes containing invalid data, but only after flushing and trying again" do
322
+ cache = expect_cache_creation
323
+ expect(cache).to receive(:all_rows).once.and_return(:allrows)
324
+
325
+ @instance.all_rows.should == :allrows
326
+
327
+ lambda { @instance.send(method_name, { :quux => :a }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotPresentError, /quux/)
328
+ @cache_flushes.length.should == 1
329
+ @cache_flushes[0].should == { :low_card_model => @low_card_model, :reason => :schema_change }
330
+ end
331
+
332
+ it "should accept hashes containing valid data, if it isn't found the first time through" do
333
+ column_baz = double("column_bar")
334
+ allow(column_baz).to receive(:name).and_return("baz")
335
+ allow(column_baz).to receive(:primary).and_return(false)
336
+
337
+ columns_to_return = [
338
+ [ @column_id, @column_foo, @column_bar ],
339
+ [ @column_id, @column_foo, @column_bar, column_baz ]
340
+ ]
341
+
342
+ allow(@low_card_model).to receive(:columns) do
343
+ columns_to_return.shift || raise("too many calls")
344
+ end
345
+
346
+ cache = expect_cache_creation
347
+ expect(cache).to receive(method_name).once.with([ { :baz => "bonk" } ]).and_return({ { :baz => 'bonk' } => :foobar })
348
+ @instance.send(method_name, { :baz => 'bonk' }).should == :foobar
349
+ end
350
+ end
351
+ end
352
+
353
+ describe "#find_rows_for" do
354
+ it "should require a complete Hash" do
355
+ lambda { @instance.find_rows_for({ :bar => 'baz' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotSpecifiedError)
356
+ lambda { @instance.find_rows_for({ :foo => 'bar', :quux => 'aa' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotPresentError)
357
+ end
358
+
359
+ it "should return a single row if a single Hash is specified" do
360
+ row = double("row")
361
+
362
+ cache = expect_cache_creation
363
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'baz' } ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ row ])
364
+
365
+ @instance.find_rows_for({ :foo => 'bar', :bar => 'baz' }).should be(row)
366
+ end
367
+
368
+ it "should return nil if no rows match" do
369
+ cache = expect_cache_creation
370
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'baz' } ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
371
+
372
+ @instance.find_rows_for({ :foo => 'bar', :bar => 'baz' }).should == nil
373
+ end
374
+
375
+ it "should return a Hash if multiple Hashes are specified" do
376
+ row1 = double("row1")
377
+ row2 = double("row2")
378
+
379
+ cache = expect_cache_creation
380
+ rows_matching_args = [ ]
381
+ expect(cache).to receive(:rows_matching).once do |*args|
382
+ rows_matching_args << args
383
+ { { 'foo' => 'bar', 'bar' => 'baz' } => [ row1 ],
384
+ { 'foo' => 'a', 'bar' => 'b' } => [ row2 ],
385
+ { 'foo' => 'c', 'bar' => 'd' } => [ ] }
386
+ end
387
+
388
+ @instance.find_rows_for([ { :foo => 'bar', :bar => 'baz' }, { :foo => 'a', :bar => 'b' }, { :foo => 'c', :bar => 'd'} ]).should == {
389
+ { :foo => 'bar', :bar => 'baz' } => row1,
390
+ { :foo => 'a', :bar => 'b' } => row2,
391
+ { :foo => 'c', :bar => 'd' } => nil }
392
+
393
+ rows_matching_args.length.should == 1
394
+ call_1 = rows_matching_args[0]
395
+ call_1.length.should == 1
396
+ input_array = call_1[0]
397
+ input_array.length.should == 3
398
+
399
+ input_array.detect { |e| e == { 'foo' => 'bar', 'bar' => 'baz' } }.should be
400
+ input_array.detect { |e| e == { 'foo' => 'a', 'bar' => 'b' } }.should be
401
+ input_array.detect { |e| e == { 'foo' => 'c', 'bar' => 'd' } }.should be
402
+ end
403
+
404
+ it "should fill in default values correctly" do
405
+ row = double("row")
406
+
407
+ cache = expect_cache_creation
408
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'yohoho' } ]).and_return({ 'foo' => 'bar', 'bar' => 'yohoho' } => [ row ])
409
+
410
+ @instance.find_rows_for({ :foo => 'bar' }).should be(row)
411
+ end
412
+ end
413
+
414
+ describe "#find_ids_for" do
415
+ it "should require a complete Hash" do
416
+ lambda { @instance.find_ids_for({ :bar => 'baz' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotSpecifiedError)
417
+ lambda { @instance.find_ids_for({ :foo => 'bar', :quux => 'aa' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotPresentError)
418
+ end
419
+
420
+ it "should return a single row if a single Hash is specified" do
421
+ row = double("row")
422
+ allow(row).to receive(:id).and_return(123)
423
+
424
+ cache = expect_cache_creation
425
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'baz' } ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ row ])
426
+
427
+ @instance.find_ids_for({ :foo => 'bar', :bar => 'baz' }).should be(123)
428
+ end
429
+
430
+ it "should return nil if no rows match" do
431
+ cache = expect_cache_creation
432
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'baz' } ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
433
+
434
+ @instance.find_ids_for({ :foo => 'bar', :bar => 'baz' }).should == nil
435
+ end
436
+
437
+ it "should return a Hash if multiple Hashes are specified" do
438
+ row1 = double("row1")
439
+ allow(row1).to receive(:id).and_return(123)
440
+ row2 = double("row2")
441
+ allow(row2).to receive(:id).and_return(345)
442
+
443
+ cache = expect_cache_creation
444
+ rows_matching_args = [ ]
445
+ expect(cache).to receive(:rows_matching).once do |*args|
446
+ rows_matching_args << args
447
+ { { 'foo' => 'bar', 'bar' => 'baz' } => [ row1 ],
448
+ { 'foo' => 'a', 'bar' => 'b' } => [ row2 ],
449
+ { 'foo' => 'c', 'bar' => 'd' } => [ ] }
450
+ end
451
+
452
+ @instance.find_ids_for([ { :foo => 'bar', :bar => 'baz' }, { :foo => 'a', :bar => 'b' }, { :foo => 'c', :bar => 'd'} ]).should == {
453
+ { :foo => 'bar', :bar => 'baz' } => 123,
454
+ { :foo => 'a', :bar => 'b' } => 345,
455
+ { :foo => 'c', :bar => 'd' } => nil }
456
+
457
+ rows_matching_args.length.should == 1
458
+ call_1 = rows_matching_args[0]
459
+ call_1.length.should == 1
460
+ input_array = call_1[0]
461
+ input_array.length.should == 3
462
+
463
+ input_array.detect { |e| e == { 'foo' => 'bar', 'bar' => 'baz' } }.should be
464
+ input_array.detect { |e| e == { 'foo' => 'a', 'bar' => 'b' } }.should be
465
+ input_array.detect { |e| e == { 'foo' => 'c', 'bar' => 'd' } }.should be
466
+ end
467
+
468
+ it "should fill in default values correctly" do
469
+ row = double("row")
470
+ allow(row).to receive(:id).and_return(123)
471
+
472
+ cache = expect_cache_creation
473
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'yohoho' } ]).and_return({ 'foo' => 'bar', 'bar' => 'yohoho' } => [ row ])
474
+
475
+ @instance.find_ids_for({ :foo => 'bar' }).should be(123)
476
+ end
477
+ end
478
+
479
+ describe "#find_or_create_rows_for" do
480
+ it "should require a complete Hash" do
481
+ lambda { @instance.find_or_create_rows_for({ :bar => 'baz' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotSpecifiedError)
482
+ lambda { @instance.find_or_create_rows_for({ :foo => 'bar', :quux => 'aa' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotPresentError)
483
+ end
484
+
485
+ it "should return a single row if a single Hash is specified" do
486
+ row = double("row")
487
+
488
+ cache = expect_cache_creation
489
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'baz' } ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ row ])
490
+
491
+ @instance.find_or_create_rows_for({ :foo => 'bar', :bar => 'baz' }).should be(row)
492
+ end
493
+
494
+ context "with setup to actually run import" do
495
+ before :each do
496
+ connection = double("connection")
497
+ allow(@low_card_model).to receive(:connection).and_return(connection)
498
+ allow(connection).to receive(:quote_table_name) { |tn| "<#{tn}>" }
499
+ connection_class = double("connection_class")
500
+ allow(connection_class).to receive(:name).and_return("some_postgresql_connection")
501
+ allow(connection).to receive(:class).and_return(connection_class)
502
+ allow(@low_card_model).to receive(:sanitize_sql).once.with([ "LOCK TABLE <thetablename>", { } ]).and_return("quoted-lock-tables")
503
+
504
+ expect(@low_card_model).to receive(:transaction).once { |*args, &block| block.call }
505
+ expect(connection).to receive(:execute).once.with("quoted-lock-tables")
506
+ end
507
+
508
+ it "should raise a LowCardInvalidLowCardRowsError if the database refuses to import the rows with an exception" do
509
+ cache1 = expect_cache_creation
510
+ expect(cache1).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
511
+
512
+ cache2 = expect_cache_creation
513
+ expect(cache2).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
514
+
515
+ expect(@low_card_model).to receive(:import).once.and_raise(ActiveRecord::StatementInvalid.new("boom"))
516
+
517
+ exception = capture_exception { @instance.find_or_create_rows_for([ :foo => 'bar', :bar => 'baz' ]) }
518
+ exception.class.should == LowCardTables::Errors::LowCardInvalidLowCardRowsError
519
+ exception.message.should match(/thetablename/i)
520
+ exception.message.should match(/ActiveRecord::StatementInvalid/i)
521
+ exception.message.should match(/foo.*bar/i)
522
+ exception.message.should match(/bar.*baz/i)
523
+
524
+ @rows_created.length.should == 0
525
+ end
526
+
527
+ it "should raise a LowCardInvalidLowCardRowsError if the database refuses to import the rows with an import failure" do
528
+ cache1 = expect_cache_creation
529
+ expect(cache1).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
530
+
531
+ cache2 = expect_cache_creation
532
+ expect(cache2).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
533
+
534
+ failed_instance_1 = double("failed_instance_1")
535
+ errors_1 = double("errors_1")
536
+ allow(failed_instance_1).to receive(:errors).and_return(errors_1)
537
+ allow(errors_1).to receive(:full_messages).and_return(%w{error1a error1b error1c})
538
+
539
+ failed_instance_2 = double("failed_instance_2")
540
+ errors_2 = double("errors_2")
541
+ allow(failed_instance_2).to receive(:errors).and_return(errors_2)
542
+ allow(errors_2).to receive(:full_messages).and_return(%w{error2a error2b error2c})
543
+
544
+ import_result = double("import_result")
545
+ expect(import_result).to receive(:failed_instances).at_least(:once).and_return([ failed_instance_1, failed_instance_2 ])
546
+ expect(@low_card_model).to receive(:import).once.and_return(import_result)
547
+
548
+ exception = capture_exception { @instance.find_or_create_rows_for([ :foo => 'bar', :bar => 'baz' ]) }
549
+ exception.class.should == LowCardTables::Errors::LowCardInvalidLowCardRowsError
550
+ exception.message.should match(/thetablename/i)
551
+ exception.message.should match(/failed_instance_1/i)
552
+ exception.message.should match(/failed_instance_2/i)
553
+ exception.message.should match(/foo.*bar/i)
554
+ exception.message.should match(/error1a.*error1b.*error1c/i)
555
+ exception.message.should match(/error2a.*error2b.*error2c/i)
556
+
557
+ @rows_created.length.should == 0
558
+ end
559
+
560
+ it "should create new rows if not present, with one import command, and apply defaults" do
561
+ cache1 = expect_cache_creation
562
+ expect(cache1).to receive(:all_rows).once.and_return(:allrows)
563
+ allow(cache1).to receive(:loaded_at).once.and_return(12345)
564
+ expect_cache_validation(cache1, 12345, 2, false)
565
+
566
+ @instance.all_rows.should == :allrows
567
+
568
+ existing_row = double("existing_row")
569
+ cache_input = [
570
+ { 'foo' => 'bar', 'bar' => 'baz' },
571
+ { 'foo' => 'a', 'bar' => 'b' },
572
+ { 'foo' => 'c', 'bar' => 'yohoho' }
573
+ ]
574
+ cache_output = {
575
+ { 'foo' => 'bar', 'bar' => 'baz' } => [ existing_row ],
576
+ { 'foo' => 'a', 'bar' => 'b' } => [ ],
577
+ { 'foo' => 'c', 'bar' => 'yohoho' } => [ ]
578
+ }
579
+
580
+ cache_1_input = [ ]
581
+ expect(cache1).to receive(:rows_matching).once { |*args| cache_1_input << args; cache_output }
582
+
583
+ cache2 = expect_cache_creation
584
+ cache_2_input = [ ]
585
+ expect(cache2).to receive(:rows_matching).once { |*args| cache_2_input << args; cache_output }
586
+
587
+ import_result = double("import_result")
588
+ expect(import_result).to receive(:failed_instances).and_return([ ])
589
+ import_args = [ ]
590
+ expect(@low_card_model).to receive(:import).once { |*args| import_args << args; import_result }
591
+
592
+ new_row_1 = double("new_row_1")
593
+ new_row_2 = double("new_row_2")
594
+ cache3 = expect_cache_creation
595
+ cache_3_input = [ ]
596
+ expect(cache3).to receive(:rows_matching).once do |*args|
597
+ cache_3_input << args
598
+
599
+ { { 'foo' => 'bar', 'bar' => 'baz' } => [ existing_row ],
600
+ { 'foo' => 'a', 'bar' => 'b' } => [ new_row_1 ],
601
+ { 'foo' => 'c', 'bar' => 'yohoho' } => [ new_row_2 ] }
602
+ end
603
+
604
+ result = @instance.find_or_create_rows_for([ { :foo => 'bar', :bar => 'baz' }, { :foo => 'a', :bar => 'b' }, { :foo => 'c' } ])
605
+ result.size.should == 3
606
+ result[{ :foo => 'bar', :bar => 'baz' }].should be(existing_row)
607
+ result[{ :foo => 'a', :bar => 'b' }].should be(new_row_1)
608
+ result[{ :foo => 'c' }].should be(new_row_2)
609
+
610
+ cache_1_input.length.should == 1
611
+ cache_1_input[0].length.should == 1
612
+ cache_1_input[0][0].length.should == 3
613
+ cache_1_input[0][0].detect { |x| x == { 'foo' => 'bar', 'bar' => 'baz' }}.should be
614
+ cache_1_input[0][0].detect { |x| x == { 'foo' => 'a', 'bar' => 'b' }}.should be
615
+ cache_1_input[0][0].detect { |x| x == { 'foo' => 'c', 'bar' => 'yohoho' }}.should be
616
+
617
+ cache_2_input.length.should == 1
618
+ cache_2_input[0].length.should == 1
619
+ cache_2_input[0][0].length.should == 3
620
+ cache_2_input[0][0].detect { |x| x == { 'foo' => 'bar', 'bar' => 'baz' }}.should be
621
+ cache_2_input[0][0].detect { |x| x == { 'foo' => 'a', 'bar' => 'b' }}.should be
622
+ cache_2_input[0][0].detect { |x| x == { 'foo' => 'c', 'bar' => 'yohoho' }}.should be
623
+
624
+ cache_3_input.length.should == 1
625
+ cache_3_input[0].length.should == 1
626
+ cache_3_input[0][0].length.should == 3
627
+ cache_3_input[0][0].detect { |x| x == { 'foo' => 'bar', 'bar' => 'baz' }}.should be
628
+ cache_3_input[0][0].detect { |x| x == { 'foo' => 'a', 'bar' => 'b' }}.should be
629
+ cache_3_input[0][0].detect { |x| x == { 'foo' => 'c', 'bar' => 'yohoho' }}.should be
630
+
631
+ import_args.length.should == 1
632
+ import_args[0].length.should == 3
633
+ import_args[0][0].should == [ 'foo', 'bar' ]
634
+ import_args[0][1].length.should == 2
635
+ import_args[0][1].detect { |a| a == ['a', 'b'] }.should be
636
+ import_args[0][1].detect { |a| a == ['c', 'yohoho'] }.should be
637
+ import_args[0][2].should == { :validate => true }
638
+
639
+ @cache_flushes.length.should == 2
640
+
641
+ @cache_flushes[0][:reason].should == :creating_rows
642
+ @cache_flushes[0][:low_card_model].should be(@low_card_model)
643
+ @cache_flushes[0][:context].should == :before_import
644
+ @cache_flushes[0][:new_rows].length.should == 3
645
+ @cache_flushes[0][:new_rows].detect { |e| e == { 'foo' => 'bar', 'bar' => 'baz' } }.should be
646
+ @cache_flushes[0][:new_rows].detect { |e| e == { 'foo' => 'a', 'bar' => 'b' } }.should be
647
+ @cache_flushes[0][:new_rows].detect { |e| e == { 'foo' => 'c', 'bar' => 'yohoho' } }.should be
648
+
649
+ @cache_flushes[1][:reason].should == :creating_rows
650
+ @cache_flushes[1][:low_card_model].should be(@low_card_model)
651
+ @cache_flushes[1][:context].should == :after_import
652
+ @cache_flushes[1][:new_rows].length.should == 3
653
+ @cache_flushes[1][:new_rows].detect { |e| e == { 'foo' => 'bar', 'bar' => 'baz' } }.should be
654
+ @cache_flushes[1][:new_rows].detect { |e| e == { 'foo' => 'a', 'bar' => 'b' } }.should be
655
+ @cache_flushes[1][:new_rows].detect { |e| e == { 'foo' => 'c', 'bar' => 'yohoho' } }.should be
656
+
657
+ @rows_created.length.should == 1
658
+ @rows_created[0][:keys].should == %w{foo bar}
659
+ @rows_created[0][:low_card_model].should be(@low_card_model)
660
+ @rows_created[0][:values].length.should == 2
661
+ @rows_created[0][:values].detect { |e| e == %w{a b} }.should be
662
+ @rows_created[0][:values].detect { |e| e == %w{c yohoho} }.should be
663
+ end
664
+ end
665
+ end
666
+
667
+ describe "#find_or_create_ids_for" do
668
+ it "should require a complete Hash" do
669
+ lambda { @instance.find_or_create_ids_for({ :bar => 'baz' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotSpecifiedError)
670
+ lambda { @instance.find_or_create_ids_for({ :foo => 'bar', :quux => 'aa' }) }.should raise_error(LowCardTables::Errors::LowCardColumnNotPresentError)
671
+ end
672
+
673
+ it "should return a single ID if a single Hash is specified" do
674
+ row = double("row")
675
+ allow(row).to receive(:id).and_return(123)
676
+
677
+ cache = expect_cache_creation
678
+ expect(cache).to receive(:rows_matching).once.with([ { 'foo' => 'bar', 'bar' => 'baz' } ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ row ])
679
+
680
+ @instance.find_or_create_ids_for({ :foo => 'bar', :bar => 'baz' }).should be(123)
681
+ end
682
+
683
+ context "with setup to actually run import" do
684
+ before :each do
685
+ connection = double("connection")
686
+ allow(@low_card_model).to receive(:connection).and_return(connection)
687
+ allow(connection).to receive(:quote_table_name) { |tn| "<#{tn}>" }
688
+ connection_class = double("connection_class")
689
+ allow(connection_class).to receive(:name).and_return("some_postgresql_connection")
690
+ allow(connection).to receive(:class).and_return(connection_class)
691
+ allow(@low_card_model).to receive(:sanitize_sql).once.with([ "LOCK TABLE <thetablename>", { } ]).and_return("quoted-lock-tables")
692
+
693
+ expect(@low_card_model).to receive(:transaction).once { |*args, &block| block.call }
694
+ expect(connection).to receive(:execute).once.with("quoted-lock-tables")
695
+ end
696
+
697
+ it "should raise a LowCardInvalidLowCardRowsError if the database refuses to import the rows with an exception" do
698
+ cache1 = expect_cache_creation
699
+ expect(cache1).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
700
+
701
+ cache2 = expect_cache_creation
702
+ expect(cache2).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
703
+
704
+ expect(@low_card_model).to receive(:import).once.and_raise(ActiveRecord::StatementInvalid.new("boom"))
705
+
706
+ exception = capture_exception { @instance.find_or_create_ids_for([ :foo => 'bar', :bar => 'baz' ]) }
707
+ exception.class.should == LowCardTables::Errors::LowCardInvalidLowCardRowsError
708
+ exception.message.should match(/thetablename/i)
709
+ exception.message.should match(/ActiveRecord::StatementInvalid/i)
710
+ exception.message.should match(/foo.*bar/i)
711
+ exception.message.should match(/bar.*baz/i)
712
+
713
+ @rows_created.length.should == 0
714
+ end
715
+
716
+ it "should raise a LowCardInvalidLowCardRowsError if the database refuses to import the rows with an import failure" do
717
+ cache1 = expect_cache_creation
718
+ expect(cache1).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
719
+
720
+ cache2 = expect_cache_creation
721
+ expect(cache2).to receive(:rows_matching).once.with([ 'foo' => 'bar', 'bar' => 'baz' ]).and_return({ 'foo' => 'bar', 'bar' => 'baz' } => [ ])
722
+
723
+ failed_instance_1 = double("failed_instance_1")
724
+ errors_1 = double("errors_1")
725
+ allow(failed_instance_1).to receive(:errors).and_return(errors_1)
726
+ allow(errors_1).to receive(:full_messages).and_return(%w{error1a error1b error1c})
727
+
728
+ failed_instance_2 = double("failed_instance_2")
729
+ errors_2 = double("errors_2")
730
+ allow(failed_instance_2).to receive(:errors).and_return(errors_2)
731
+ allow(errors_2).to receive(:full_messages).and_return(%w{error2a error2b error2c})
732
+
733
+ import_result = double("import_result")
734
+ expect(import_result).to receive(:failed_instances).at_least(:once).and_return([ failed_instance_1, failed_instance_2 ])
735
+ expect(@low_card_model).to receive(:import).once.and_return(import_result)
736
+
737
+ exception = capture_exception { @instance.find_or_create_ids_for([ :foo => 'bar', :bar => 'baz' ]) }
738
+ exception.class.should == LowCardTables::Errors::LowCardInvalidLowCardRowsError
739
+ exception.message.should match(/thetablename/i)
740
+ exception.message.should match(/failed_instance_1/i)
741
+ exception.message.should match(/failed_instance_2/i)
742
+ exception.message.should match(/foo.*bar/i)
743
+ exception.message.should match(/error1a.*error1b.*error1c/i)
744
+ exception.message.should match(/error2a.*error2b.*error2c/i)
745
+
746
+ @rows_created.length.should == 0
747
+ end
748
+
749
+ it "should create new rows if not present, with one import command, and apply defaults" do
750
+ cache1 = expect_cache_creation
751
+ expect(cache1).to receive(:all_rows).once.and_return(:allrows)
752
+ allow(cache1).to receive(:loaded_at).once.and_return(12345)
753
+ expect_cache_validation(cache1, 12345, 2, false)
754
+
755
+ @instance.all_rows.should == :allrows
756
+
757
+ existing_row = double("existing_row")
758
+ allow(existing_row).to receive(:id).and_return(123)
759
+ cache_input = [
760
+ { 'foo' => 'bar', 'bar' => 'baz' },
761
+ { 'foo' => 'a', 'bar' => 'b' },
762
+ { 'foo' => 'c', 'bar' => 'yohoho' }
763
+ ]
764
+ cache_output = {
765
+ { 'foo' => 'bar', 'bar' => 'baz' } => [ existing_row ],
766
+ { 'foo' => 'a', 'bar' => 'b' } => [ ],
767
+ { 'foo' => 'c', 'bar' => 'yohoho' } => [ ]
768
+ }
769
+
770
+ cache_1_input = [ ]
771
+ expect(cache1).to receive(:rows_matching).once { |*args| cache_1_input << args; cache_output }
772
+
773
+ cache2 = expect_cache_creation
774
+ cache_2_input = [ ]
775
+ expect(cache2).to receive(:rows_matching).once { |*args| cache_2_input << args; cache_output }
776
+
777
+ import_result = double("import_result")
778
+ expect(import_result).to receive(:failed_instances).and_return([ ])
779
+ import_args = [ ]
780
+ expect(@low_card_model).to receive(:import).once { |*args| import_args << args; import_result }
781
+
782
+ new_row_1 = double("new_row_1")
783
+ allow(new_row_1).to receive(:id).and_return(345)
784
+ new_row_2 = double("new_row_2")
785
+ allow(new_row_2).to receive(:id).and_return(567)
786
+ cache3 = expect_cache_creation
787
+ cache_3_input = [ ]
788
+ expect(cache3).to receive(:rows_matching).once do |*args|
789
+ cache_3_input << args
790
+
791
+ { { 'foo' => 'bar', 'bar' => 'baz' } => [ existing_row ],
792
+ { 'foo' => 'a', 'bar' => 'b' } => [ new_row_1 ],
793
+ { 'foo' => 'c', 'bar' => 'yohoho' } => [ new_row_2 ] }
794
+ end
795
+
796
+ result = @instance.find_or_create_ids_for([ { :foo => 'bar', :bar => 'baz' }, { :foo => 'a', :bar => 'b' }, { :foo => 'c' } ])
797
+ result.size.should == 3
798
+ result[{ :foo => 'bar', :bar => 'baz' }].should be(123)
799
+ result[{ :foo => 'a', :bar => 'b' }].should be(345)
800
+ result[{ :foo => 'c' }].should be(567)
801
+
802
+ cache_1_input.length.should == 1
803
+ cache_1_input[0].length.should == 1
804
+ cache_1_input[0][0].length.should == 3
805
+ cache_1_input[0][0].detect { |x| x == { 'foo' => 'bar', 'bar' => 'baz' }}.should be
806
+ cache_1_input[0][0].detect { |x| x == { 'foo' => 'a', 'bar' => 'b' }}.should be
807
+ cache_1_input[0][0].detect { |x| x == { 'foo' => 'c', 'bar' => 'yohoho' }}.should be
808
+
809
+ cache_2_input.length.should == 1
810
+ cache_2_input[0].length.should == 1
811
+ cache_2_input[0][0].length.should == 3
812
+ cache_2_input[0][0].detect { |x| x == { 'foo' => 'bar', 'bar' => 'baz' }}.should be
813
+ cache_2_input[0][0].detect { |x| x == { 'foo' => 'a', 'bar' => 'b' }}.should be
814
+ cache_2_input[0][0].detect { |x| x == { 'foo' => 'c', 'bar' => 'yohoho' }}.should be
815
+
816
+ cache_3_input.length.should == 1
817
+ cache_3_input[0].length.should == 1
818
+ cache_3_input[0][0].length.should == 3
819
+ cache_3_input[0][0].detect { |x| x == { 'foo' => 'bar', 'bar' => 'baz' }}.should be
820
+ cache_3_input[0][0].detect { |x| x == { 'foo' => 'a', 'bar' => 'b' }}.should be
821
+ cache_3_input[0][0].detect { |x| x == { 'foo' => 'c', 'bar' => 'yohoho' }}.should be
822
+
823
+ import_args.length.should == 1
824
+ import_args[0].length.should == 3
825
+ import_args[0][0].should == [ 'foo', 'bar' ]
826
+ import_args[0][1].length.should == 2
827
+ import_args[0][1].detect { |a| a == ['a', 'b'] }.should be
828
+ import_args[0][1].detect { |a| a == ['c', 'yohoho'] }.should be
829
+ import_args[0][2].should == { :validate => true }
830
+
831
+ @cache_flushes.length.should == 2
832
+
833
+ @cache_flushes[0][:reason].should == :creating_rows
834
+ @cache_flushes[0][:low_card_model].should be(@low_card_model)
835
+ @cache_flushes[0][:context].should == :before_import
836
+ @cache_flushes[0][:new_rows].length.should == 3
837
+ @cache_flushes[0][:new_rows].detect { |e| e == { 'foo' => 'bar', 'bar' => 'baz' } }.should be
838
+ @cache_flushes[0][:new_rows].detect { |e| e == { 'foo' => 'a', 'bar' => 'b' } }.should be
839
+ @cache_flushes[0][:new_rows].detect { |e| e == { 'foo' => 'c', 'bar' => 'yohoho' } }.should be
840
+
841
+ @cache_flushes[1][:reason].should == :creating_rows
842
+ @cache_flushes[1][:low_card_model].should be(@low_card_model)
843
+ @cache_flushes[1][:context].should == :after_import
844
+ @cache_flushes[1][:new_rows].length.should == 3
845
+ @cache_flushes[1][:new_rows].detect { |e| e == { 'foo' => 'bar', 'bar' => 'baz' } }.should be
846
+ @cache_flushes[1][:new_rows].detect { |e| e == { 'foo' => 'a', 'bar' => 'b' } }.should be
847
+ @cache_flushes[1][:new_rows].detect { |e| e == { 'foo' => 'c', 'bar' => 'yohoho' } }.should be
848
+
849
+ @rows_created.length.should == 1
850
+ @rows_created[0][:keys].should == %w{foo bar}
851
+ @rows_created[0][:low_card_model].should be(@low_card_model)
852
+ @rows_created[0][:values].length.should == 2
853
+ @rows_created[0][:values].detect { |e| e == %w{a b} }.should be
854
+ @rows_created[0][:values].detect { |e| e == %w{c yohoho} }.should be
855
+ end
856
+ end
857
+ end
858
+
859
+ describe "#value_column_names" do
860
+ it "should return nothing if the table doesn't exist" do
861
+ allow(@low_card_model).to receive(:table_exists?).and_return(false)
862
+
863
+ @instance.value_column_names.should == [ ]
864
+ end
865
+
866
+ it "should exclude primary keys, created/updated_at, and options-specified column names" do
867
+ column_created_at = double("column_created_at")
868
+ allow(column_created_at).to receive(:primary).and_return(false)
869
+ allow(column_created_at).to receive(:name).and_return("created_at")
870
+
871
+ column_updated_at = double("column_updated_at")
872
+ allow(column_updated_at).to receive(:primary).and_return(false)
873
+ allow(column_updated_at).to receive(:name).and_return("updated_at")
874
+
875
+ column_skipped = double("column_skipped")
876
+ allow(column_skipped).to receive(:primary).and_return(false)
877
+ allow(column_skipped).to receive(:name).and_return("FooFle")
878
+
879
+ columns = [ @column_id, @column_foo, @column_bar, @column_created_at, @column_updated_at, @column_skipped ]
880
+
881
+ allow(@low_card_model).to receive(:low_card_options).and_return({ :exclude_column_names => :fooFLe })
882
+
883
+ @instance.value_column_names.should == %w{foo bar}
884
+ end
885
+ end
886
+
887
+ it "should call through to RowCollapser on #collapse_rows_and_update_referrers!" do
888
+ cache = expect_cache_creation
889
+ expect(cache).to receive(:all_rows).once.and_return(:allrows)
890
+
891
+ @instance.all_rows.should == :allrows
892
+
893
+ collapser = double("collapser")
894
+ expect(LowCardTables::LowCardTable::RowCollapser).to receive(:new).once.with(@low_card_model, { :abc => :def }).and_return(collapser)
895
+
896
+ collapse_map = double("collapse_map")
897
+ expect(collapser).to receive(:collapse!).once.with().and_return(collapse_map)
898
+
899
+ @instance.collapse_rows_and_update_referrers!(:abc => :def).should be(collapse_map)
900
+
901
+ @cache_flushes.length.should == 1
902
+ @cache_flushes[0].should == { :reason => :collapse_rows_and_update_referrers, :low_card_model => @low_card_model }
903
+ end
904
+
905
+ it "should call through to the TableUniqueIndex on #ensure_has_unique_index!" do
906
+ expect(@table_unique_index).to receive(:ensure_present!).once.with(false)
907
+ @instance.ensure_has_unique_index!
908
+
909
+ expect(@table_unique_index).to receive(:ensure_present!).once.with(true)
910
+ @instance.ensure_has_unique_index!(true)
911
+ end
912
+
913
+ it "should call through to the TableUniqueIndex on #remove_unique_index!" do
914
+ expect(@table_unique_index).to receive(:remove!).once
915
+ @instance.remove_unique_index!
916
+ end
917
+ end
918
+ end