low_card_tables 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +59 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE +21 -0
  6. data/README.md +75 -0
  7. data/Rakefile +6 -0
  8. data/lib/low_card_tables.rb +72 -0
  9. data/lib/low_card_tables/active_record/base.rb +55 -0
  10. data/lib/low_card_tables/active_record/migrations.rb +223 -0
  11. data/lib/low_card_tables/active_record/relation.rb +35 -0
  12. data/lib/low_card_tables/active_record/scoping.rb +87 -0
  13. data/lib/low_card_tables/errors.rb +74 -0
  14. data/lib/low_card_tables/has_low_card_table/base.rb +114 -0
  15. data/lib/low_card_tables/has_low_card_table/low_card_association.rb +273 -0
  16. data/lib/low_card_tables/has_low_card_table/low_card_associations_manager.rb +143 -0
  17. data/lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb +224 -0
  18. data/lib/low_card_tables/has_low_card_table/low_card_objects_manager.rb +80 -0
  19. data/lib/low_card_tables/low_card_table/base.rb +184 -0
  20. data/lib/low_card_tables/low_card_table/cache.rb +214 -0
  21. data/lib/low_card_tables/low_card_table/cache_expiration/exponential_cache_expiration_policy.rb +151 -0
  22. data/lib/low_card_tables/low_card_table/cache_expiration/fixed_cache_expiration_policy.rb +23 -0
  23. data/lib/low_card_tables/low_card_table/cache_expiration/has_cache_expiration.rb +100 -0
  24. data/lib/low_card_tables/low_card_table/cache_expiration/no_caching_expiration_policy.rb +13 -0
  25. data/lib/low_card_tables/low_card_table/cache_expiration/unlimited_cache_expiration_policy.rb +13 -0
  26. data/lib/low_card_tables/low_card_table/row_collapser.rb +175 -0
  27. data/lib/low_card_tables/low_card_table/row_manager.rb +681 -0
  28. data/lib/low_card_tables/low_card_table/table_unique_index.rb +134 -0
  29. data/lib/low_card_tables/version.rb +4 -0
  30. data/lib/low_card_tables/version_support.rb +52 -0
  31. data/low_card_tables.gemspec +69 -0
  32. data/spec/low_card_tables/helpers/database_helper.rb +148 -0
  33. data/spec/low_card_tables/helpers/query_spy_helper.rb +47 -0
  34. data/spec/low_card_tables/helpers/system_helpers.rb +63 -0
  35. data/spec/low_card_tables/system/basic_system_spec.rb +254 -0
  36. data/spec/low_card_tables/system/bulk_system_spec.rb +334 -0
  37. data/spec/low_card_tables/system/caching_system_spec.rb +531 -0
  38. data/spec/low_card_tables/system/migrations_system_spec.rb +747 -0
  39. data/spec/low_card_tables/system/options_system_spec.rb +581 -0
  40. data/spec/low_card_tables/system/queries_system_spec.rb +142 -0
  41. data/spec/low_card_tables/system/validations_system_spec.rb +88 -0
  42. data/spec/low_card_tables/unit/active_record/base_spec.rb +53 -0
  43. data/spec/low_card_tables/unit/active_record/migrations_spec.rb +207 -0
  44. data/spec/low_card_tables/unit/active_record/relation_spec.rb +47 -0
  45. data/spec/low_card_tables/unit/active_record/scoping_spec.rb +101 -0
  46. data/spec/low_card_tables/unit/has_low_card_table/base_spec.rb +79 -0
  47. data/spec/low_card_tables/unit/has_low_card_table/low_card_association_spec.rb +287 -0
  48. data/spec/low_card_tables/unit/has_low_card_table/low_card_associations_manager_spec.rb +190 -0
  49. data/spec/low_card_tables/unit/has_low_card_table/low_card_dynamic_method_manager_spec.rb +234 -0
  50. data/spec/low_card_tables/unit/has_low_card_table/low_card_objects_manager_spec.rb +70 -0
  51. data/spec/low_card_tables/unit/low_card_table/base_spec.rb +207 -0
  52. data/spec/low_card_tables/unit/low_card_table/cache_expiration/exponential_cache_expiration_policy_spec.rb +128 -0
  53. data/spec/low_card_tables/unit/low_card_table/cache_expiration/fixed_cache_expiration_policy_spec.rb +25 -0
  54. data/spec/low_card_tables/unit/low_card_table/cache_expiration/has_cache_expiration_policy_spec.rb +100 -0
  55. data/spec/low_card_tables/unit/low_card_table/cache_expiration/no_caching_expiration_policy_spec.rb +14 -0
  56. data/spec/low_card_tables/unit/low_card_table/cache_expiration/unlimited_cache_expiration_policy_spec.rb +14 -0
  57. data/spec/low_card_tables/unit/low_card_table/cache_spec.rb +282 -0
  58. data/spec/low_card_tables/unit/low_card_table/row_collapser_spec.rb +109 -0
  59. data/spec/low_card_tables/unit/low_card_table/row_manager_spec.rb +918 -0
  60. data/spec/low_card_tables/unit/low_card_table/table_unique_index_spec.rb +117 -0
  61. metadata +206 -0
@@ -0,0 +1,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