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.
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