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
@@ -0,0 +1,747 @@
|
|
1
|
+
require 'low_card_tables'
|
2
|
+
require 'low_card_tables/helpers/database_helper'
|
3
|
+
require 'low_card_tables/helpers/system_helpers'
|
4
|
+
|
5
|
+
describe "LowCardTables migration support" do
|
6
|
+
include LowCardTables::Helpers::SystemHelpers
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
@dh = LowCardTables::Helpers::DatabaseHelper.new
|
10
|
+
@dh.setup_activerecord!
|
11
|
+
|
12
|
+
# We need to use a different table name for every single spec in this test. That's because one of the things that
|
13
|
+
# migrations take a look at is whether, for a given table, there's a model pointing to it that declares itself as
|
14
|
+
# a low-card model. Once defined, it's impossible to remove these classes from ActiveRecord::Base.descendants,
|
15
|
+
# which is what we use to look for these classes.
|
16
|
+
@table_name = "lctables_sus_#{rand(1_000_000_000)}".to_sym
|
17
|
+
|
18
|
+
LowCardTables::VersionSupport.clear_schema_cache!(::ActiveRecord::Base)
|
19
|
+
end
|
20
|
+
|
21
|
+
after :each do
|
22
|
+
tn = @table_name
|
23
|
+
migrate do
|
24
|
+
drop_table tn rescue nil
|
25
|
+
drop_table :lctables_spec_users rescue nil
|
26
|
+
drop_table :non_low_card_table rescue nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_user!(name, deleted, deceased, gender, donation_level = nil, awesomeness = nil)
|
31
|
+
user = ::User.new
|
32
|
+
user.name = name
|
33
|
+
user.deleted = deleted
|
34
|
+
user.deceased = deceased
|
35
|
+
user.gender = gender
|
36
|
+
user.donation_level = donation_level if donation_level
|
37
|
+
user.awesomeness = awesomeness if awesomeness
|
38
|
+
user.save!
|
39
|
+
user
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be able to migrate non-low-card tables" do
|
43
|
+
migrate do
|
44
|
+
create_table :non_low_card_table do |t|
|
45
|
+
t.string :name
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
migrate do
|
50
|
+
add_column :non_low_card_table, :a, :integer
|
51
|
+
end
|
52
|
+
|
53
|
+
migrate do
|
54
|
+
remove_column :non_low_card_table, :a
|
55
|
+
end
|
56
|
+
|
57
|
+
migrate do
|
58
|
+
drop_table :non_low_card_table
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should handle schema changes to the low-card table" do
|
63
|
+
tn = @table_name
|
64
|
+
migrate do
|
65
|
+
drop_table tn rescue nil
|
66
|
+
create_table tn, :low_card => true do |t|
|
67
|
+
t.boolean :deleted, :null => false
|
68
|
+
t.boolean :deceased
|
69
|
+
t.string :gender, :null => false
|
70
|
+
t.integer :donation_level
|
71
|
+
end
|
72
|
+
|
73
|
+
drop_table :lctables_spec_users rescue nil
|
74
|
+
create_table :lctables_spec_users do |t|
|
75
|
+
t.string :name, :null => false
|
76
|
+
t.integer :user_status_id, :null => false, :limit => 2
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
81
|
+
define_model_class(:User, :lctables_spec_users) { has_low_card_table :status }
|
82
|
+
|
83
|
+
@user1 = create_user!('User1', false, true, 'male', 5)
|
84
|
+
@user2 = create_user!('User2', false, false, 'female', 5)
|
85
|
+
|
86
|
+
migrate do
|
87
|
+
remove_column tn, :donation_level
|
88
|
+
add_column tn, :awesomeness, :integer, :null => false, :default => 123
|
89
|
+
end
|
90
|
+
|
91
|
+
::UserStatus.reset_column_information
|
92
|
+
@user3 = create_user!('User3', false, true, 'male', nil)
|
93
|
+
@user3.status.awesomeness.should == 123
|
94
|
+
@user3.awesomeness.should == 123
|
95
|
+
|
96
|
+
@user3.awesomeness = 345
|
97
|
+
|
98
|
+
@user3.respond_to?(:donation_level).should_not be
|
99
|
+
@user3.respond_to?(:donation_level=).should_not be
|
100
|
+
|
101
|
+
@user3.save!
|
102
|
+
|
103
|
+
@user3_again = ::User.find(@user3.id)
|
104
|
+
@user3_again.status.awesomeness.should == 345
|
105
|
+
@user3_again.awesomeness.should == 345
|
106
|
+
|
107
|
+
@user3_again.respond_to?(:donation_level).should_not be
|
108
|
+
@user3_again.respond_to?(:donation_level=).should_not be
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should automatically add a unique index in migrations if explicitly told it's a low-card table" do
|
112
|
+
tn = @table_name
|
113
|
+
migrate do
|
114
|
+
drop_table tn rescue nil
|
115
|
+
create_table tn, :low_card => true do |t|
|
116
|
+
t.boolean :deleted, :null => false
|
117
|
+
t.boolean :deceased
|
118
|
+
t.string :gender, :null => false
|
119
|
+
t.integer :donation_level
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# This is deliberately *not* a low-card table
|
124
|
+
define_model_class(:UserStatus, @table_name) { }
|
125
|
+
|
126
|
+
status_1 = ::UserStatus.create!(:deleted => false, :deceased => false, :gender => 'male', :donation_level => 5)
|
127
|
+
# make sure we can create a different one
|
128
|
+
status_2 = ::UserStatus.create!(:deleted => false, :deceased => false, :gender => 'male', :donation_level => 10)
|
129
|
+
# now, make sure we can't create a duplicate
|
130
|
+
lambda {
|
131
|
+
::UserStatus.create!(:deleted => false, :deceased => false, :gender => 'male', :donation_level => 5)
|
132
|
+
}.should raise_error(ActiveRecord::StatementInvalid)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should automatically add a unique index in migrations if there's a model saying it's a low-card table" do
|
136
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
137
|
+
tn = @table_name
|
138
|
+
|
139
|
+
migrate do
|
140
|
+
drop_table tn rescue nil
|
141
|
+
create_table tn do |t|
|
142
|
+
t.boolean :deleted, :null => false
|
143
|
+
t.boolean :deceased
|
144
|
+
t.string :gender, :null => false
|
145
|
+
t.integer :donation_level
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# This is deliberately *not* a low-card table
|
150
|
+
define_model_class(:UserStatusBackdoor, @table_name) { }
|
151
|
+
|
152
|
+
status_1 = ::UserStatusBackdoor.create!(:deleted => false, :deceased => false, :gender => 'male', :donation_level => 5)
|
153
|
+
# make sure we can create a different one
|
154
|
+
status_2 = ::UserStatusBackdoor.create!(:deleted => false, :deceased => false, :gender => 'male', :donation_level => 10)
|
155
|
+
# now, make sure we can't create a duplicate
|
156
|
+
lambda {
|
157
|
+
::UserStatusBackdoor.create!(:deleted => false, :deceased => false, :gender => 'male', :donation_level => 5)
|
158
|
+
}.should raise_error(ActiveRecord::StatementInvalid)
|
159
|
+
end
|
160
|
+
|
161
|
+
def check_unique_index_modification(explicit_or_model, common_hash, first, second, third, &block)
|
162
|
+
if explicit_or_model == :model
|
163
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
164
|
+
end
|
165
|
+
|
166
|
+
tn = @table_name
|
167
|
+
migrate do
|
168
|
+
drop_table tn rescue nil
|
169
|
+
create_table tn do |t|
|
170
|
+
t.boolean :deleted, :null => false
|
171
|
+
t.boolean :deceased
|
172
|
+
t.string :gender, :null => false
|
173
|
+
t.integer :donation_level
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
migrate(&block)
|
178
|
+
|
179
|
+
# This is deliberately *not* a low-card table
|
180
|
+
define_model_class(:UserStatusBackdoor, @table_name) { }
|
181
|
+
::UserStatusBackdoor.reset_column_information
|
182
|
+
|
183
|
+
status_1 = ::UserStatusBackdoor.create!(common_hash.merge(first))
|
184
|
+
# make sure we can create a different one
|
185
|
+
status_2 = ::UserStatusBackdoor.create!(common_hash.merge(second))
|
186
|
+
# now, make sure we can't create a duplicate
|
187
|
+
lambda {
|
188
|
+
::UserStatusBackdoor.create!(common_hash.merge(third))
|
189
|
+
}.should raise_error(ActiveRecord::StatementInvalid)
|
190
|
+
end
|
191
|
+
|
192
|
+
%w{explicit model}.map(&:to_sym).each do |explicit_or_model|
|
193
|
+
def extra_options(explicit_or_model)
|
194
|
+
if explicit_or_model == :explicit then { :low_card => true } else { } end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "should automatically change the unique index in migrations if told it's a low-card table (#{explicit_or_model})" do
|
198
|
+
it "using #add_column" do
|
199
|
+
tn = @table_name
|
200
|
+
eo = extra_options(explicit_or_model)
|
201
|
+
check_unique_index_modification(explicit_or_model, { :deleted => false, :deceased => false, :gender => 'male', :donation_level => 5 },
|
202
|
+
{ :awesomeness => 10 },
|
203
|
+
{ :awesomeness => 5 },
|
204
|
+
{ :awesomeness => 10 }) do
|
205
|
+
add_column tn, :awesomeness, :integer, eo
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
it "using #remove_column" do
|
210
|
+
tn = @table_name
|
211
|
+
eo = extra_options(explicit_or_model)
|
212
|
+
check_unique_index_modification(explicit_or_model, { :deleted => false, :deceased => false },
|
213
|
+
{ :gender => 'male' },
|
214
|
+
{ :gender => 'female' },
|
215
|
+
{ :gender => 'male' }) do
|
216
|
+
if eo.size > 0
|
217
|
+
remove_column tn, :donation_level, eo
|
218
|
+
else
|
219
|
+
remove_column tn, :donation_level
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
it "using #change_table" do
|
225
|
+
tn = @table_name
|
226
|
+
eo = extra_options(explicit_or_model)
|
227
|
+
check_unique_index_modification(explicit_or_model, { :deleted => false, :deceased => false, :gender => 'male', :donation_level => 5 },
|
228
|
+
{ :awesomeness => 10 },
|
229
|
+
{ :awesomeness => 5 },
|
230
|
+
{ :awesomeness => 10 }) do
|
231
|
+
change_table tn, eo do |t|
|
232
|
+
t.integer :awesomeness
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
it "using #change_low_card_table" do
|
238
|
+
tn = @table_name
|
239
|
+
eo = extra_options(explicit_or_model)
|
240
|
+
check_unique_index_modification(explicit_or_model, { :deleted => false, :deceased => false, :gender => 'male', :donation_level => 5 },
|
241
|
+
{ :awesomeness => 10 },
|
242
|
+
{ :awesomeness => 5 },
|
243
|
+
{ :awesomeness => 10 }) do
|
244
|
+
change_low_card_table(tn) do
|
245
|
+
execute "ALTER TABLE #{tn} ADD COLUMN awesomeness INTEGER"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
it "should remove the unique index during #change_low_card_table" do
|
253
|
+
tn = @table_name
|
254
|
+
migrate do
|
255
|
+
drop_table tn rescue nil
|
256
|
+
create_table tn, :low_card => true do |t|
|
257
|
+
t.boolean :deleted, :null => false
|
258
|
+
t.boolean :deceased
|
259
|
+
t.string :gender, :null => false
|
260
|
+
t.integer :donation_level
|
261
|
+
end
|
262
|
+
|
263
|
+
drop_table :lctables_spec_users rescue nil
|
264
|
+
create_table :lctables_spec_users do |t|
|
265
|
+
t.string :name, :null => false
|
266
|
+
t.integer :user_status_id, :null => false, :limit => 2
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
271
|
+
define_model_class(:User, :lctables_spec_users) { has_low_card_table :status }
|
272
|
+
|
273
|
+
user1 = create_user!('User1', false, false, 'male', 5)
|
274
|
+
status_1 = user1.status
|
275
|
+
status_1_id = user1.user_status_id
|
276
|
+
status_1_id.should > 0
|
277
|
+
|
278
|
+
migrate do
|
279
|
+
change_low_card_table(tn) do
|
280
|
+
status_1_attributes = status_1.attributes.dup
|
281
|
+
status_1_attributes.delete(:id)
|
282
|
+
status_1_attributes.delete("id")
|
283
|
+
|
284
|
+
new_status = ::UserStatus.new(status_1_attributes)
|
285
|
+
new_status.save_low_card_row!
|
286
|
+
|
287
|
+
new_status.id.should_not == status_1.id
|
288
|
+
new_status.id.should > 0
|
289
|
+
|
290
|
+
::UserStatus.delete_all("id = #{new_status.id}")
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should be able to collapse now-identical rows and return the collapse map using collapse_rows_and_update_referrers!" do
|
296
|
+
tn = @table_name
|
297
|
+
migrate do
|
298
|
+
drop_table tn rescue nil
|
299
|
+
create_table tn, :low_card => true do |t|
|
300
|
+
t.boolean :deleted, :null => false
|
301
|
+
t.boolean :deceased
|
302
|
+
t.string :gender, :null => false
|
303
|
+
t.integer :donation_level
|
304
|
+
end
|
305
|
+
|
306
|
+
drop_table :lctables_spec_users rescue nil
|
307
|
+
create_table :lctables_spec_users do |t|
|
308
|
+
t.string :name, :null => false
|
309
|
+
t.integer :user_status_id, :null => false, :limit => 2
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
314
|
+
define_model_class(:User, :lctables_spec_users) { has_low_card_table :status }
|
315
|
+
|
316
|
+
user1 = create_user!('User1', false, false, 'male', 5)
|
317
|
+
user2 = create_user!('User2', false, false, 'male', 10)
|
318
|
+
user3 = create_user!('User3', false, false, 'male', 7)
|
319
|
+
user4 = create_user!('User4', false, false, 'female', 5)
|
320
|
+
user5 = create_user!('User5', false, true, 'male', 5)
|
321
|
+
|
322
|
+
competing_ids = [ user1.user_status_id, user2.user_status_id, user3.user_status_id ]
|
323
|
+
|
324
|
+
# Make sure they all have unique status IDs
|
325
|
+
[ user1, user2, user3, user4, user5 ].map(&:user_status_id).uniq.length.should == 5
|
326
|
+
|
327
|
+
define_model_class(:UserStatusBackdoor, @table_name) { }
|
328
|
+
::UserStatusBackdoor.count.should == 5
|
329
|
+
|
330
|
+
count_after_removal = nil
|
331
|
+
collapse_map = nil
|
332
|
+
count_after_collapse = nil
|
333
|
+
|
334
|
+
migrate do
|
335
|
+
change_low_card_table tn do
|
336
|
+
remove_column tn, :donation_level, :low_card_collapse_rows => false
|
337
|
+
count_after_removal = ::UserStatusBackdoor.count
|
338
|
+
|
339
|
+
collapse_map = ::UserStatus.low_card_collapse_rows_and_update_referrers!
|
340
|
+
count_after_collapse = ::UserStatusBackdoor.count
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
count_after_removal.should == 5
|
345
|
+
count_after_collapse.should == 3
|
346
|
+
collapse_map.size.should == 1
|
347
|
+
|
348
|
+
k = collapse_map.keys[0]
|
349
|
+
k.class.should == ::UserStatus
|
350
|
+
competing_ids.include?(k.id).should be
|
351
|
+
|
352
|
+
expected_losers = competing_ids - [ k.id ]
|
353
|
+
actual_losers = collapse_map[k].map(&:id)
|
354
|
+
actual_losers.sort.should == expected_losers.sort
|
355
|
+
|
356
|
+
::UserStatusBackdoor.reset_column_information
|
357
|
+
::UserStatusBackdoor.count.should == 3
|
358
|
+
|
359
|
+
user123_status = ::UserStatusBackdoor.find(user1.user_status_id)
|
360
|
+
user123_status.deleted.should == false
|
361
|
+
user123_status.deceased.should == false
|
362
|
+
user123_status.gender.should == 'male'
|
363
|
+
|
364
|
+
user4_status = ::UserStatusBackdoor.find(user4.user_status_id)
|
365
|
+
user4_status.deleted.should == false
|
366
|
+
user4_status.deceased.should == false
|
367
|
+
user4_status.gender.should == 'female'
|
368
|
+
|
369
|
+
user5_status = ::UserStatusBackdoor.find(user5.user_status_id)
|
370
|
+
user5_status.deleted.should == false
|
371
|
+
user5_status.deceased.should == true
|
372
|
+
user5_status.gender.should == 'male'
|
373
|
+
|
374
|
+
[ ::User.find(user1.id), ::User.find(user2.id), ::User.find(user3.id) ].map(&:user_status_id).uniq.length.should == 1 # all the same
|
375
|
+
[ ::User.find(user1.id), ::User.find(user4.id), ::User.find(user5.id) ].map(&:user_status_id).uniq.length.should == 3 # all different
|
376
|
+
end
|
377
|
+
|
378
|
+
%w{remove_column change_table}.each do |remove_column_type|
|
379
|
+
before :each do
|
380
|
+
@remove_column_proc = if remove_column_type == 'remove_column'
|
381
|
+
lambda do |tn, opts|
|
382
|
+
migrate do
|
383
|
+
remove_column tn, :donation_level, opts
|
384
|
+
end
|
385
|
+
end
|
386
|
+
elsif remove_column_type == 'change_table'
|
387
|
+
lambda do |tn, opts|
|
388
|
+
migrate do
|
389
|
+
change_table tn, opts do |t|
|
390
|
+
t.remove :donation_level
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
else
|
395
|
+
raise "Unknown remove_column_type: #{remove_column_type.inspect}"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
it "should be able to remove low-card columns, collapse now-identical rows, and automatically update associated rows (#{remove_column_type})" do
|
400
|
+
tn = @table_name
|
401
|
+
migrate do
|
402
|
+
drop_table tn rescue nil
|
403
|
+
create_table tn, :low_card => true do |t|
|
404
|
+
t.boolean :deleted, :null => false
|
405
|
+
t.boolean :deceased
|
406
|
+
t.string :gender, :null => false
|
407
|
+
t.integer :donation_level
|
408
|
+
end
|
409
|
+
|
410
|
+
drop_table :lctables_spec_users rescue nil
|
411
|
+
create_table :lctables_spec_users do |t|
|
412
|
+
t.string :name, :null => false
|
413
|
+
t.integer :user_status_id, :null => false, :limit => 2
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
418
|
+
define_model_class(:User, :lctables_spec_users) { has_low_card_table :status }
|
419
|
+
|
420
|
+
user1 = create_user!('User1', false, false, 'male', 5)
|
421
|
+
user2 = create_user!('User2', false, false, 'male', 10)
|
422
|
+
user3 = create_user!('User3', false, false, 'male', 7)
|
423
|
+
user4 = create_user!('User4', false, false, 'female', 5)
|
424
|
+
user5 = create_user!('User5', false, true, 'male', 5)
|
425
|
+
|
426
|
+
# Make sure they all have unique status IDs
|
427
|
+
[ user1, user2, user3, user4, user5 ].map(&:user_status_id).uniq.length.should == 5
|
428
|
+
|
429
|
+
define_model_class(:UserStatusBackdoor, @table_name) { }
|
430
|
+
::UserStatusBackdoor.count.should == 5
|
431
|
+
|
432
|
+
@remove_column_proc.call(tn, { })
|
433
|
+
|
434
|
+
::UserStatusBackdoor.reset_column_information
|
435
|
+
::UserStatusBackdoor.count.should == 3
|
436
|
+
|
437
|
+
user123_status = ::UserStatusBackdoor.find(user1.user_status_id)
|
438
|
+
user123_status.deleted.should == false
|
439
|
+
user123_status.deceased.should == false
|
440
|
+
user123_status.gender.should == 'male'
|
441
|
+
|
442
|
+
user4_status = ::UserStatusBackdoor.find(user4.user_status_id)
|
443
|
+
user4_status.deleted.should == false
|
444
|
+
user4_status.deceased.should == false
|
445
|
+
user4_status.gender.should == 'female'
|
446
|
+
|
447
|
+
user5_status = ::UserStatusBackdoor.find(user5.user_status_id)
|
448
|
+
user5_status.deleted.should == false
|
449
|
+
user5_status.deceased.should == true
|
450
|
+
user5_status.gender.should == 'male'
|
451
|
+
|
452
|
+
[ ::User.find(user1.id), ::User.find(user2.id), ::User.find(user3.id) ].map(&:user_status_id).uniq.length.should == 1 # all the same
|
453
|
+
[ ::User.find(user1.id), ::User.find(user4.id), ::User.find(user5.id) ].map(&:user_status_id).uniq.length.should == 3 # all different
|
454
|
+
end
|
455
|
+
|
456
|
+
context "with several dependent tables" do
|
457
|
+
before :each do
|
458
|
+
tn = @table_name
|
459
|
+
migrate do
|
460
|
+
drop_table tn rescue nil
|
461
|
+
create_table tn, :low_card => true do |t|
|
462
|
+
t.boolean :deleted, :null => false
|
463
|
+
t.boolean :deceased
|
464
|
+
t.string :gender, :null => false
|
465
|
+
t.integer :donation_level
|
466
|
+
end
|
467
|
+
|
468
|
+
drop_table :lctables_spec_users rescue nil
|
469
|
+
create_table :lctables_spec_users do |t|
|
470
|
+
t.string :name, :null => false
|
471
|
+
t.integer :user_status_id, :null => false, :limit => 2
|
472
|
+
t.integer :other_status_id, :null => false, :limit => 2
|
473
|
+
end
|
474
|
+
|
475
|
+
drop_table :lctables_spec_admins rescue nil
|
476
|
+
create_table :lctables_spec_admins do |t|
|
477
|
+
t.string :name, :null => false
|
478
|
+
t.integer :admin_status_id, :null => false, :limit => 2
|
479
|
+
end
|
480
|
+
|
481
|
+
drop_table :lctables_spec_guests rescue nil
|
482
|
+
create_table :lctables_spec_guests do |t|
|
483
|
+
t.string :name, :null => false
|
484
|
+
t.integer :guest_status_id, :null => false, :limit => 2
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
489
|
+
define_model_class(:User, :lctables_spec_users) do
|
490
|
+
has_low_card_table :status
|
491
|
+
has_low_card_table :other_status, :class => ::UserStatus, :foreign_key => :other_status_id
|
492
|
+
end
|
493
|
+
define_model_class(:Admin, :lctables_spec_admins) { has_low_card_table :status, :class => ::UserStatus, :foreign_key => :admin_status_id }
|
494
|
+
define_model_class(:Guest, :lctables_spec_guests) { has_low_card_table :status, :class => ::UserStatus, :foreign_key => :guest_status_id }
|
495
|
+
|
496
|
+
::User.low_card_value_collapsing_update_scheme 10
|
497
|
+
|
498
|
+
class ::Admin
|
499
|
+
class << self
|
500
|
+
def low_card_called(x)
|
501
|
+
@low_card_calls ||= [ ]
|
502
|
+
@low_card_calls << x
|
503
|
+
end
|
504
|
+
|
505
|
+
def low_card_calls
|
506
|
+
@low_card_calls || [ ]
|
507
|
+
end
|
508
|
+
|
509
|
+
def reset_low_card_calls!
|
510
|
+
@low_card_calls = [ ]
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
low_card_value_collapsing_update_scheme(lambda { |map| ::Admin.low_card_called(map) })
|
515
|
+
end
|
516
|
+
|
517
|
+
::Admin.reset_low_card_calls!
|
518
|
+
|
519
|
+
class ::Guest
|
520
|
+
low_card_value_collapsing_update_scheme :none
|
521
|
+
end
|
522
|
+
|
523
|
+
@all_users = [ ]
|
524
|
+
50.times do
|
525
|
+
new_user = ::User.new
|
526
|
+
|
527
|
+
new_user.name = "User#{rand(1_000_000_000)}"
|
528
|
+
|
529
|
+
new_user.status.deleted = !! (rand(2) == 0)
|
530
|
+
new_user.status.deceased = !! (rand(2) == 0)
|
531
|
+
new_user.status.gender = case rand(3); when 0 then 'female'; when 1 then 'male'; when 2 then 'other'; end
|
532
|
+
new_user.status.donation_level = rand(10)
|
533
|
+
|
534
|
+
new_user.other_status.deleted = !! (rand(2) == 0)
|
535
|
+
new_user.other_status.deceased = !! (rand(2) == 0)
|
536
|
+
new_user.other_status.gender = case rand(3); when 0 then 'female'; when 1 then 'male'; when 2 then 'other'; end
|
537
|
+
new_user.other_status.donation_level = rand(10)
|
538
|
+
|
539
|
+
new_user.save!
|
540
|
+
|
541
|
+
@all_users << new_user
|
542
|
+
end
|
543
|
+
|
544
|
+
@all_admins = [ ]
|
545
|
+
25.times do
|
546
|
+
new_admin = Admin.new
|
547
|
+
|
548
|
+
new_admin.name = "Admin#{rand(1_000_000_000)}"
|
549
|
+
|
550
|
+
new_admin.deleted = !! (rand(2) == 0)
|
551
|
+
new_admin.deceased = !! (rand(2) == 0)
|
552
|
+
new_admin.gender = case rand(3); when 0 then 'female'; when 1 then 'male'; when 2 then 'other'; end
|
553
|
+
new_admin.donation_level = rand(10)
|
554
|
+
|
555
|
+
new_admin.save!
|
556
|
+
|
557
|
+
@all_admins << new_admin
|
558
|
+
end
|
559
|
+
|
560
|
+
@all_guests = [ ]
|
561
|
+
5.times do
|
562
|
+
new_guest = Guest.new
|
563
|
+
|
564
|
+
new_guest.name = "Guest#{rand(1_000_000_000)}"
|
565
|
+
|
566
|
+
new_guest.deleted = !! (rand(2) == 0)
|
567
|
+
new_guest.deceased = !! (rand(2) == 0)
|
568
|
+
new_guest.gender = case rand(3); when 0 then 'female'; when 1 then 'male'; when 2 then 'other'; end
|
569
|
+
new_guest.donation_level = rand(10)
|
570
|
+
|
571
|
+
new_guest.save!
|
572
|
+
|
573
|
+
@all_guests << new_guest
|
574
|
+
end
|
575
|
+
|
576
|
+
define_model_class(:UserStatusBackdoor, @table_name) { }
|
577
|
+
|
578
|
+
class UpdateCollector
|
579
|
+
attr_reader :updates
|
580
|
+
|
581
|
+
def initialize
|
582
|
+
@updates = [ ]
|
583
|
+
end
|
584
|
+
|
585
|
+
def call(name, start, finish, message_id, values)
|
586
|
+
sql = values[:sql]
|
587
|
+
@updates << values[:sql] if sql =~ /^\s*UPDATE\s+/
|
588
|
+
end
|
589
|
+
end
|
590
|
+
@collector = UpdateCollector.new
|
591
|
+
|
592
|
+
::ActiveSupport::Notifications.subscribe('sql.active_record', @collector)
|
593
|
+
end
|
594
|
+
|
595
|
+
it "should not attempt to update any associated tables if a column is removed and told not to, but should still collapse IDs (#{remove_column_type})" do
|
596
|
+
tn = @table_name
|
597
|
+
|
598
|
+
::UserStatusBackdoor.count.should > 30
|
599
|
+
::UserStatusBackdoor.count.should <= 120
|
600
|
+
|
601
|
+
@remove_column_proc.call(tn, :low_card_update_referring_models => false)
|
602
|
+
|
603
|
+
# The count will depend on randomization, but the chance of us generating fewer than 6 distinct rows should be
|
604
|
+
# extremely low -- there are 120 possible (2 deleted * 2 deceased * 3 genders)
|
605
|
+
::UserStatusBackdoor.count.should >= 6
|
606
|
+
::UserStatusBackdoor.count.should <= 12
|
607
|
+
|
608
|
+
::User.all.each do |user|
|
609
|
+
previous_user = @all_users.detect { |u| u.id == user.id }
|
610
|
+
user.user_status_id.should == previous_user.user_status_id
|
611
|
+
end
|
612
|
+
|
613
|
+
::Admin.all.each do |admin|
|
614
|
+
previous_admin = @all_admins.detect { |u| u.id == admin.id }
|
615
|
+
admin.admin_status_id.should == previous_admin.admin_status_id
|
616
|
+
end
|
617
|
+
|
618
|
+
::Guest.all.each do |guest|
|
619
|
+
previous_guest = @all_guests.detect { |u| u.id == guest.id }
|
620
|
+
guest.guest_status_id.should == previous_guest.guest_status_id
|
621
|
+
end
|
622
|
+
|
623
|
+
admin_change_maps = ::Admin.low_card_calls
|
624
|
+
admin_change_maps.length.should == 0
|
625
|
+
end
|
626
|
+
|
627
|
+
it "should not attempt to update any associated tables or collapse IDs if a column is removed and told not to (#{remove_column_type})" do
|
628
|
+
tn = @table_name
|
629
|
+
|
630
|
+
::UserStatusBackdoor.count.should > 30
|
631
|
+
::UserStatusBackdoor.count.should <= 120
|
632
|
+
|
633
|
+
@remove_column_proc.call(tn, :low_card_collapse_rows => false)
|
634
|
+
|
635
|
+
::UserStatusBackdoor.count.should > 30
|
636
|
+
::UserStatusBackdoor.count.should <= 120
|
637
|
+
|
638
|
+
::User.all.each do |user|
|
639
|
+
previous_user = @all_users.detect { |u| u.id == user.id }
|
640
|
+
user.user_status_id.should == previous_user.user_status_id
|
641
|
+
end
|
642
|
+
|
643
|
+
::Admin.all.each do |admin|
|
644
|
+
previous_admin = @all_admins.detect { |u| u.id == admin.id }
|
645
|
+
admin.admin_status_id.should == previous_admin.admin_status_id
|
646
|
+
end
|
647
|
+
|
648
|
+
::Guest.all.each do |guest|
|
649
|
+
previous_guest = @all_guests.detect { |u| u.id == guest.id }
|
650
|
+
guest.guest_status_id.should == previous_guest.guest_status_id
|
651
|
+
end
|
652
|
+
|
653
|
+
admin_change_maps = ::Admin.low_card_calls
|
654
|
+
admin_change_maps.length.should == 0
|
655
|
+
end
|
656
|
+
|
657
|
+
it "should update all associated tables, including multiple references to the same low-card table, in chunks as specified, when a column is removed (#{remove_column_type})" do
|
658
|
+
tn = @table_name
|
659
|
+
|
660
|
+
# The count will depend on randomization, but the chance of us generating fewer than 30 distinct rows should be
|
661
|
+
# extremely low -- there are 120 possible (2 deleted * 2 deceased * 3 genders * 10 donation_levels)
|
662
|
+
::UserStatusBackdoor.count.should > 30
|
663
|
+
::UserStatusBackdoor.count.should <= 120
|
664
|
+
|
665
|
+
@remove_column_proc.call(tn, { })
|
666
|
+
|
667
|
+
# The count will depend on randomization, but the chance of us generating fewer than 6 distinct rows should be
|
668
|
+
# extremely low -- there are 120 possible (2 deleted * 2 deceased * 3 genders)
|
669
|
+
::UserStatusBackdoor.count.should >= 6
|
670
|
+
::UserStatusBackdoor.count.should <= 12
|
671
|
+
|
672
|
+
::UserStatusBackdoor.reset_column_information
|
673
|
+
all_user_status_ids = ::UserStatusBackdoor.all.map(&:id)
|
674
|
+
|
675
|
+
::User.all.each do |verify_user|
|
676
|
+
orig_user = @all_users.detect { |u| u.id == verify_user.id }
|
677
|
+
|
678
|
+
verify_user.status.deleted.should == orig_user.status.deleted
|
679
|
+
verify_user.status.deceased.should == orig_user.status.deceased
|
680
|
+
verify_user.status.gender.should == orig_user.status.gender
|
681
|
+
verify_user.status.respond_to?(:donation_level).should_not be
|
682
|
+
verify_user.respond_to?(:donation_level).should_not be
|
683
|
+
end
|
684
|
+
|
685
|
+
new_admin_status_ids = ::Admin.all.sort_by(&:id).map(&:admin_status_id)
|
686
|
+
orig_admin_status_ids = @all_admins.sort_by(&:id).map(&:admin_status_id)
|
687
|
+
|
688
|
+
new_admin_status_ids.should == orig_admin_status_ids # no change
|
689
|
+
|
690
|
+
admin_change_maps = ::Admin.low_card_calls
|
691
|
+
admin_change_maps.length.should == 1
|
692
|
+
|
693
|
+
admin_change_maps[0].each do |new_row, old_rows|
|
694
|
+
old_rows.length.should >= 1
|
695
|
+
old_rows.each do |old_row|
|
696
|
+
old_row.deleted.should == new_row.deleted
|
697
|
+
old_row.deceased.should == new_row.deceased
|
698
|
+
old_row.gender.should == new_row.gender
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
collapses = admin_change_maps[0].size
|
703
|
+
expected_update_count = collapses
|
704
|
+
expected_update_count *= 2 # one for each column in the table
|
705
|
+
expected_update_count *= 5 # 50 rows in batches of 10
|
706
|
+
|
707
|
+
user_updates = @collector.updates.select { |u| u =~ /lctables_spec_users/ }
|
708
|
+
user_updates.length.should == expected_update_count
|
709
|
+
|
710
|
+
::Guest.all.each do |guest|
|
711
|
+
previous_guest = @all_guests.detect { |u| u.id == guest.id }
|
712
|
+
guest.guest_status_id.should == previous_guest.guest_status_id
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
718
|
+
it "should fail if there is no unique index on a low-card table at startup" do
|
719
|
+
tn = @table_name
|
720
|
+
|
721
|
+
migrate do
|
722
|
+
drop_table tn rescue nil
|
723
|
+
create_table tn do |t|
|
724
|
+
t.boolean :deleted, :null => false
|
725
|
+
t.boolean :deceased
|
726
|
+
t.string :gender, :null => false
|
727
|
+
t.integer :donation_level
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
define_model_class(:UserStatus, @table_name) { is_low_card_table }
|
732
|
+
|
733
|
+
e = nil
|
734
|
+
begin
|
735
|
+
::UserStatus.low_card_all_rows
|
736
|
+
rescue LowCardTables::Errors::LowCardNoUniqueIndexError => lcnuie
|
737
|
+
e = lcnuie
|
738
|
+
end
|
739
|
+
|
740
|
+
e.should be
|
741
|
+
e.message.should match(/#{@table_name}/mi)
|
742
|
+
e.message.should match(/deceased/mi)
|
743
|
+
e.message.should match(/deleted/mi)
|
744
|
+
e.message.should match(/gender/mi)
|
745
|
+
e.message.should match(/donation_level/mi)
|
746
|
+
end
|
747
|
+
end
|