low_card_tables 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +59 -0
- data/Gemfile +17 -0
- data/LICENSE +21 -0
- data/README.md +75 -0
- data/Rakefile +6 -0
- data/lib/low_card_tables.rb +72 -0
- data/lib/low_card_tables/active_record/base.rb +55 -0
- data/lib/low_card_tables/active_record/migrations.rb +223 -0
- data/lib/low_card_tables/active_record/relation.rb +35 -0
- data/lib/low_card_tables/active_record/scoping.rb +87 -0
- data/lib/low_card_tables/errors.rb +74 -0
- data/lib/low_card_tables/has_low_card_table/base.rb +114 -0
- data/lib/low_card_tables/has_low_card_table/low_card_association.rb +273 -0
- data/lib/low_card_tables/has_low_card_table/low_card_associations_manager.rb +143 -0
- data/lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb +224 -0
- data/lib/low_card_tables/has_low_card_table/low_card_objects_manager.rb +80 -0
- data/lib/low_card_tables/low_card_table/base.rb +184 -0
- data/lib/low_card_tables/low_card_table/cache.rb +214 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/exponential_cache_expiration_policy.rb +151 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/fixed_cache_expiration_policy.rb +23 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/has_cache_expiration.rb +100 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/no_caching_expiration_policy.rb +13 -0
- data/lib/low_card_tables/low_card_table/cache_expiration/unlimited_cache_expiration_policy.rb +13 -0
- data/lib/low_card_tables/low_card_table/row_collapser.rb +175 -0
- data/lib/low_card_tables/low_card_table/row_manager.rb +681 -0
- data/lib/low_card_tables/low_card_table/table_unique_index.rb +134 -0
- data/lib/low_card_tables/version.rb +4 -0
- data/lib/low_card_tables/version_support.rb +52 -0
- data/low_card_tables.gemspec +69 -0
- data/spec/low_card_tables/helpers/database_helper.rb +148 -0
- data/spec/low_card_tables/helpers/query_spy_helper.rb +47 -0
- data/spec/low_card_tables/helpers/system_helpers.rb +63 -0
- data/spec/low_card_tables/system/basic_system_spec.rb +254 -0
- data/spec/low_card_tables/system/bulk_system_spec.rb +334 -0
- data/spec/low_card_tables/system/caching_system_spec.rb +531 -0
- data/spec/low_card_tables/system/migrations_system_spec.rb +747 -0
- data/spec/low_card_tables/system/options_system_spec.rb +581 -0
- data/spec/low_card_tables/system/queries_system_spec.rb +142 -0
- data/spec/low_card_tables/system/validations_system_spec.rb +88 -0
- data/spec/low_card_tables/unit/active_record/base_spec.rb +53 -0
- data/spec/low_card_tables/unit/active_record/migrations_spec.rb +207 -0
- data/spec/low_card_tables/unit/active_record/relation_spec.rb +47 -0
- data/spec/low_card_tables/unit/active_record/scoping_spec.rb +101 -0
- data/spec/low_card_tables/unit/has_low_card_table/base_spec.rb +79 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_association_spec.rb +287 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_associations_manager_spec.rb +190 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_dynamic_method_manager_spec.rb +234 -0
- data/spec/low_card_tables/unit/has_low_card_table/low_card_objects_manager_spec.rb +70 -0
- data/spec/low_card_tables/unit/low_card_table/base_spec.rb +207 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/exponential_cache_expiration_policy_spec.rb +128 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/fixed_cache_expiration_policy_spec.rb +25 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/has_cache_expiration_policy_spec.rb +100 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/no_caching_expiration_policy_spec.rb +14 -0
- data/spec/low_card_tables/unit/low_card_table/cache_expiration/unlimited_cache_expiration_policy_spec.rb +14 -0
- data/spec/low_card_tables/unit/low_card_table/cache_spec.rb +282 -0
- data/spec/low_card_tables/unit/low_card_table/row_collapser_spec.rb +109 -0
- data/spec/low_card_tables/unit/low_card_table/row_manager_spec.rb +918 -0
- data/spec/low_card_tables/unit/low_card_table/table_unique_index_spec.rb +117 -0
- metadata +206 -0
data/spec/low_card_tables/unit/low_card_table/cache_expiration/no_caching_expiration_policy_spec.rb
ADDED
@@ -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
|