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,190 @@
1
+ require 'low_card_tables'
2
+
3
+ describe LowCardTables::HasLowCardTable::LowCardAssociationsManager do
4
+ describe "instantiation" do
5
+ it "should require a Class that descends from ActiveRecord::Base" do
6
+ klass = Class.new(String)
7
+
8
+ lambda do
9
+ LowCardTables::HasLowCardTable::LowCardAssociationsManager.new(klass)
10
+ end.should raise_error(ArgumentError)
11
+ end
12
+
13
+ it "should require a Class that is not itself a low-card class" do
14
+ klass = Class.new(::ActiveRecord::Base)
15
+ allow(klass).to receive(:is_low_card_table?).and_return(true)
16
+
17
+ lambda do
18
+ LowCardTables::HasLowCardTable::LowCardAssociationsManager.new(klass)
19
+ end.should raise_error(ArgumentError)
20
+ end
21
+ end
22
+
23
+ context "with a normal model class" do
24
+ before :each do
25
+ @model_class = Class.new
26
+ allow(@model_class).to receive(:superclass).and_return(::ActiveRecord::Base)
27
+ allow(@model_class).to receive(:is_low_card_table?).and_return(false)
28
+ expect(@model_class).to receive(:before_save).once.with(:low_card_update_foreign_keys!)
29
+
30
+ @manager = LowCardTables::HasLowCardTable::LowCardAssociationsManager.new(@model_class)
31
+ end
32
+
33
+ it "should have no associations by default" do
34
+ @manager.associations.should == [ ]
35
+ end
36
+
37
+ it "should have a default :low_card_value_collapsing_update_scheme" do
38
+ @manager.low_card_value_collapsing_update_scheme.should == :default
39
+ end
40
+
41
+ describe "#low_card_value_collapsing_update_scheme" do
42
+ it "should return :default by default" do
43
+ @manager.low_card_value_collapsing_update_scheme.should == :default
44
+ end
45
+
46
+ it "should be settable to :none or :default" do
47
+ @manager.low_card_value_collapsing_update_scheme :none
48
+ @manager.low_card_value_collapsing_update_scheme.should == :none
49
+ @manager.low_card_value_collapsing_update_scheme :default
50
+ @manager.low_card_value_collapsing_update_scheme.should == :default
51
+ end
52
+
53
+ it "should be settable to a positive integer" do
54
+ @manager.low_card_value_collapsing_update_scheme 1
55
+ @manager.low_card_value_collapsing_update_scheme.should == 1
56
+ @manager.low_card_value_collapsing_update_scheme 345
57
+ @manager.low_card_value_collapsing_update_scheme.should == 345
58
+
59
+ lambda { @manager.low_card_value_collapsing_update_scheme 0 }.should raise_error(ArgumentError)
60
+ lambda { @manager.low_card_value_collapsing_update_scheme -27 }.should raise_error(ArgumentError)
61
+ end
62
+
63
+ it "should be settable to something that responds to :call" do
64
+ callable = double("callable")
65
+ allow(callable).to receive(:call)
66
+
67
+ @manager.low_card_value_collapsing_update_scheme callable
68
+ @manager.low_card_value_collapsing_update_scheme.should be(callable)
69
+ end
70
+
71
+ it "should not be settable to anything else" do
72
+ lambda { @manager.low_card_value_collapsing_update_scheme "foo" }.should raise_error(ArgumentError)
73
+ end
74
+ end
75
+
76
+ describe "#has_low_card_table" do
77
+ it "should require a non-blank association name" do
78
+ lambda { @manager.has_low_card_table(nil) }.should raise_error(ArgumentError)
79
+ lambda { @manager.has_low_card_table("") }.should raise_error(ArgumentError)
80
+ lambda { @manager.has_low_card_table(" ") }.should raise_error(ArgumentError)
81
+ end
82
+
83
+ context "with one association" do
84
+ before :each do
85
+ options = { :a => :b, :c => :d }
86
+
87
+ @association = double("low_card_association")
88
+ expect(LowCardTables::HasLowCardTable::LowCardAssociation).to receive(:new).once.with(@model_class, 'foo', options).and_return(@association)
89
+
90
+ lcdmm = double("low_card_dynamic_method_manager")
91
+ expect(@model_class).to receive(:_low_card_dynamic_method_manager).at_least(:once).and_return(lcdmm)
92
+
93
+ expect(lcdmm).to receive(:sync_methods!).at_least(:once)
94
+
95
+ @manager.has_low_card_table(:foo, options)
96
+ end
97
+
98
+ it "should create a new association and add it to the list" do
99
+ @manager.associations.should == [ @association ]
100
+ end
101
+
102
+ it "should remove any previous associations with the same name" do
103
+ new_options = { :x => :y, :z => :a }
104
+ allow(@association).to receive(:association_name).and_return("foo")
105
+
106
+ association2 = double("association2")
107
+
108
+ expect(LowCardTables::HasLowCardTable::LowCardAssociation).to receive(:new).once.with(@model_class, 'foo', new_options).and_return(association2)
109
+
110
+ @manager.has_low_card_table('foo', new_options)
111
+ @manager.associations.should == [ association2 ]
112
+ end
113
+
114
+ context "and another association" do
115
+ before :each do
116
+ new_options = { :x => :y, :z => :a }
117
+ allow(@association).to receive(:association_name).and_return("foo")
118
+
119
+ @association2 = double("association2")
120
+ allow(@association2).to receive(:association_name).and_return("bar")
121
+
122
+ expect(LowCardTables::HasLowCardTable::LowCardAssociation).to receive(:new).once.with(@model_class, 'bar', new_options).and_return(@association2)
123
+
124
+ @manager.has_low_card_table('bar', new_options)
125
+ @manager.associations.should == [ @association, @association2 ]
126
+ end
127
+
128
+ it "should retrieve them by name" do
129
+ @manager._low_card_association("foo").should == @association
130
+ @manager._low_card_association("bar").should == @association2
131
+ @manager.maybe_low_card_association("foo").should == @association
132
+ @manager.maybe_low_card_association("bar").should == @association2
133
+ end
134
+
135
+ it "should do the right thing when they're not found" do
136
+ @manager.maybe_low_card_association("baz").should_not be
137
+ lambda { @manager._low_card_association("baz") }.should raise_error(LowCardTables::Errors::LowCardAssociationNotFoundError, /bar[\s,]+foo/)
138
+ end
139
+
140
+ it "should update all foreign keys on #low_card_update_foreign_keys!" do
141
+ model_instance = double("model_instance")
142
+ allow(model_instance).to receive(:kind_of?).with(@model_class).and_return(true)
143
+
144
+ @association.should receive(:update_foreign_key!).once.with(model_instance)
145
+ @association2.should receive(:update_foreign_key!).once.with(model_instance)
146
+
147
+ @manager.low_card_update_foreign_keys!(model_instance)
148
+ end
149
+
150
+ it "should blow up if passed something that isn't of the correct class to #low_card_update_foreign_keys!" do
151
+ model_instance = double("model_instance")
152
+ allow(model_instance).to receive(:kind_of?).with(@model_class).and_return(false)
153
+
154
+ lambda { @manager.low_card_update_foreign_keys!(model_instance) }.should raise_error(ArgumentError)
155
+ end
156
+
157
+ describe "_low_card_update_collapsed_rows" do
158
+ it "should call #update_collapsed_rows on associations that match the low-card model class passed" do
159
+ @manager.low_card_value_collapsing_update_scheme 345
160
+
161
+ low_card_class = double("low_card_class")
162
+ other_low_card_class = double("other_low_card_class")
163
+
164
+ collapse_map = double("collapse_map")
165
+
166
+ allow(@association).to receive(:low_card_class).and_return(other_low_card_class)
167
+ allow(@association2).to receive(:low_card_class).and_return(low_card_class)
168
+
169
+ expect(@association2).to receive(:update_collapsed_rows).with(collapse_map, 345).once
170
+
171
+ @manager._low_card_update_collapsed_rows(low_card_class, collapse_map)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ describe "#low_card_column_information_reset!" do
179
+ it "should call through to the model class" do
180
+ low_card_model = double("low_card_model")
181
+
182
+ lcdmm = double("low_card_dynamic_method_manager")
183
+ expect(@model_class).to receive(:_low_card_dynamic_method_manager).and_return(lcdmm)
184
+ expect(lcdmm).to receive(:sync_methods!).once
185
+
186
+ @manager.low_card_column_information_reset!(low_card_model)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,234 @@
1
+ require 'low_card_tables'
2
+
3
+ describe LowCardTables::HasLowCardTable::LowCardDynamicMethodManager do
4
+ before :each do
5
+ @model_class = double("model_class")
6
+ @lcam = double("low_card_associations_manager")
7
+ allow(@model_class).to receive(:_low_card_associations_manager).and_return(@lcam)
8
+
9
+ @methods_module = Module.new
10
+ allow(@model_class).to receive(:_low_card_dynamic_methods_module).and_return(@methods_module)
11
+
12
+ @manager = LowCardTables::HasLowCardTable::LowCardDynamicMethodManager.new(@model_class)
13
+ end
14
+
15
+ context "with two associations and installed methods" do
16
+ before :each do
17
+ @association1 = double("association1")
18
+ allow(@association1).to receive(:association_name).and_return("foo")
19
+ allow(@association1).to receive(:foreign_key_column_name).and_return("a1fk")
20
+ allow(@association1).to receive(:class_method_name_to_low_card_method_name_map).and_return({
21
+ 'cm1' => 'lc1m1', 'cm2' => 'lc1m2' })
22
+
23
+ @association2 = double("association2")
24
+ allow(@association2).to receive(:association_name).and_return("bar")
25
+ allow(@association2).to receive(:foreign_key_column_name).and_return("a2fk")
26
+ allow(@association2).to receive(:class_method_name_to_low_card_method_name_map).and_return({
27
+ 'cm2' => 'lc2m1', 'cm3' => 'lc2m2' })
28
+
29
+ allow(@lcam).to receive(:associations).and_return([ @association1, @association2 ])
30
+
31
+ @manager.sync_methods!
32
+ end
33
+
34
+ describe "#scope_from_query" do
35
+ before :each do
36
+ @base_scope = double("base_scope")
37
+ @end_scope = double("end_scope")
38
+ end
39
+
40
+ it "should pass through non-low-card constraints" do
41
+ expect(@base_scope).to receive(:where).once.with({ :name => 'bonk', :_low_card_direct => true }).and_return(@end_scope)
42
+ @manager.scope_from_query(@base_scope, { :name => 'bonk' }).should be(@end_scope)
43
+ end
44
+
45
+ it "should apply low-card constraints" do
46
+ low_card_class_1 = double("low_card_class_1")
47
+ allow(@association1).to receive(:low_card_class).and_return(low_card_class_1)
48
+ expect(low_card_class_1).to receive(:low_card_ids_matching).with({ 'lc1m1' => false }).and_return([ 3, 9, 12 ])
49
+
50
+ expect(@base_scope).to receive(:where).once.with("a1fk IN (:ids)", { :ids => [ 3, 9, 12 ] }).and_return(@end_scope)
51
+ @manager.scope_from_query(@base_scope, { :cm1 => false }).should be(@end_scope)
52
+ end
53
+
54
+ it "should apply multiple low-card constraints combined with non-low-card constraints" do
55
+ low_card_class_1 = double("low_card_class_1")
56
+ allow(@association1).to receive(:low_card_class).and_return(low_card_class_1)
57
+ expect(low_card_class_1).to receive(:low_card_ids_matching).with({ 'lc1m1' => false }).and_return([ 3, 9, 12 ])
58
+
59
+ low_card_class_2 = double("low_card_class_2")
60
+ allow(@association2).to receive(:low_card_class).and_return(low_card_class_2)
61
+ expect(low_card_class_2).to receive(:low_card_ids_matching).with({ 'lc2m2' => [ :a, :b ], 'lc2m1' => 'yohoho' }).and_return([ 4, 6, 8 ])
62
+
63
+ mid_scope = double("mid_scope")
64
+
65
+ # expect(@base_scope).to receive(:where).once.with("a1fk IN (:ids)", { :ids => [ 3, 9, 12 ] }).and_return(mid_scope)
66
+ # expect(mid_scope).to receive(:where).once.with("a2fk IN (:ids)", { :ids => [ 4, 6, 8 ] }).and_return(@end_scope)
67
+
68
+ base_args = mid_args = nil
69
+
70
+ expect(@base_scope).to receive(:where).once { |*args| base_args = args; mid_scope }
71
+ expect(mid_scope).to receive(:where).once { |*args| mid_args = args; @end_scope }
72
+
73
+ @manager.scope_from_query(@base_scope, { :cm1 => false, :cm3 => [ :a, :b ], :bar => { 'lc2m1' => "yohoho" } }).should be(@end_scope)
74
+
75
+ # Perfectly valid for this to happen in either order
76
+ if base_args[0] =~ /^a1fk/
77
+ base_args.should == [ 'a1fk IN (:ids)', { :ids => [ 3, 9, 12 ]}]
78
+ mid_args.should == [ 'a2fk IN (:ids)', { :ids => [ 4, 6, 8 ]}]
79
+ else
80
+ base_args.should == [ 'a2fk IN (:ids)', { :ids => [ 4, 6, 8 ]}]
81
+ mid_args.should == [ 'a1fk IN (:ids)', { :ids => [ 3, 9, 12 ]}]
82
+ end
83
+ end
84
+
85
+ it "should apply low-card constraints in combination with direct foreign-key constraints" do
86
+ low_card_class_1 = double("low_card_class_1")
87
+ allow(@association1).to receive(:low_card_class).and_return(low_card_class_1)
88
+ expect(low_card_class_1).to receive(:low_card_ids_matching).with({ 'lc1m1' => false }).and_return([ 3, 9, 12 ])
89
+
90
+ mid_scope = double("mid_scope")
91
+
92
+ expect(@base_scope).to receive(:where).once.with({ :a2fk => [ 1, 3, 12 ], :_low_card_direct => true }).and_return(mid_scope)
93
+ expect(mid_scope).to receive(:where).once.with("a1fk IN (:ids)", { :ids => [ 3, 9, 12 ] }).and_return(@end_scope)
94
+
95
+ @manager.scope_from_query(@base_scope, { :cm1 => false, :a2fk => [ 1, 3, 12] }).should be(@end_scope)
96
+ end
97
+ end
98
+
99
+ describe "method delegation and invocation" do
100
+ before :each do
101
+ @invoked_object = double("invoked_object")
102
+ @low_card_object = double("low_card_object")
103
+ @args = double("args")
104
+ @rv = double("rv")
105
+
106
+ allow(@invoked_object).to receive(:kind_of?).with(@model_class).and_return(true)
107
+ end
108
+
109
+ def check_invocation(method_name, association_name, low_card_method_name)
110
+ expect(@invoked_object).to receive(association_name).and_return(@low_card_object)
111
+ expect(@low_card_object).to receive(low_card_method_name).with(@args).and_return(@rv)
112
+
113
+ @methods_module.instance_methods.map(&:to_s).include?(method_name.to_s).should be
114
+
115
+ @manager.run_low_card_method(@invoked_object, method_name, @args).should be(@rv)
116
+ end
117
+
118
+ context "after changing associations" do
119
+ before :each do
120
+ @manager.sync_methods!
121
+
122
+ @association3 = double("association3")
123
+ allow(@association3).to receive(:association_name).and_return("baz")
124
+ allow(@association3).to receive(:foreign_key_column_name).and_return("a3fk")
125
+ allow(@association3).to receive(:class_method_name_to_low_card_method_name_map).and_return({
126
+ 'cm2' => 'lc3m1', 'cm4' => 'lc3m2' })
127
+
128
+ allow(@lcam).to receive(:associations).and_return([ @association1, @association3 ])
129
+
130
+ @manager.sync_methods!
131
+ end
132
+
133
+ it "should run the right method for cm1" do
134
+ check_invocation("cm1", "foo", "lc1m1")
135
+ end
136
+
137
+ it "should run the right method for cm2" do
138
+ check_invocation("cm2", "baz", "lc3m1")
139
+ end
140
+
141
+ it "should run the right method for cm3" do
142
+ @methods_module.instance_methods.map(&:to_s).include?("cm3").should_not be
143
+
144
+ lambda { @manager.run_low_card_method(@invoked_object, "cm3", @args) }.should raise_error(NameError, /cm3/)
145
+ end
146
+
147
+ it "should run the right method for cm4" do
148
+ check_invocation("cm4", "baz", "lc3m2")
149
+ end
150
+ end
151
+
152
+ it "should run the right method for cm1" do
153
+ check_invocation("cm1", "foo", "lc1m1")
154
+ end
155
+
156
+ it "should run the right method for cm2" do
157
+ check_invocation("cm2", "bar", "lc2m1")
158
+ end
159
+
160
+ it "should run the right method for cm3" do
161
+ check_invocation("cm3", "bar", "lc2m2")
162
+ end
163
+
164
+ def check_association(method_name, association)
165
+ lcom = double("low_card_objects_manager")
166
+ expect(@invoked_object).to receive(:_low_card_objects_manager).and_return(lcom)
167
+ expect(lcom).to receive(:object_for).with(association).and_return(@low_card_object)
168
+
169
+ @methods_module.instance_methods.map(&:to_s).include?(method_name).should be
170
+
171
+ @manager.run_low_card_method(@invoked_object, method_name, [ ]).should be(@low_card_object)
172
+ end
173
+
174
+ it "should return the right association for foo" do
175
+ check_association("foo", @association1)
176
+ end
177
+
178
+ it "should return the right association for bar" do
179
+ check_association("bar", @association2)
180
+ end
181
+
182
+ def check_foreign_key_get(method_name, association)
183
+ lcom = double("low_card_objects_manager")
184
+ expect(@invoked_object).to receive(:_low_card_objects_manager).and_return(lcom)
185
+ expect(lcom).to receive(:foreign_key_for).with(association).and_return(12345)
186
+
187
+ @methods_module.instance_methods.map(&:to_s).include?(method_name).should be
188
+
189
+ @manager.run_low_card_method(@invoked_object, method_name, [ ]).should == 12345
190
+ end
191
+
192
+ it "should return the right foreign key for a1fk" do
193
+ check_foreign_key_get("a1fk", @association1)
194
+ end
195
+
196
+ it "should return the right foreign key for a2fk" do
197
+ check_foreign_key_get("a2fk", @association2)
198
+ end
199
+
200
+ def check_foreign_key_set(method_name, association)
201
+ args = double("args")
202
+ lcom = double("low_card_objects_manager")
203
+ expect(@invoked_object).to receive(:_low_card_objects_manager).and_return(lcom)
204
+ expect(lcom).to receive(:set_foreign_key_for).with(association, args)
205
+
206
+ @methods_module.instance_methods.map(&:to_s).include?(method_name).should be
207
+
208
+ @manager.run_low_card_method(@invoked_object, method_name, args)
209
+ end
210
+
211
+ it "should set the right foreign key for a1fk" do
212
+ check_foreign_key_set("a1fk=", @association1)
213
+ end
214
+
215
+ it "should set the right foreign key for a2fk" do
216
+ check_foreign_key_set("a2fk=", @association2)
217
+ end
218
+
219
+ it "should blow up if asked to invoke a method that doesn't exist" do
220
+ lambda do
221
+ @manager.run_low_card_method(@invoked_object, "quux", [ ])
222
+ end.should raise_error(/quux/i)
223
+ end
224
+
225
+ it "should blow up if given an object of the wrong class" do
226
+ allow(@invoked_object).to receive(:kind_of?).with(@model_class).and_return(false)
227
+
228
+ lambda do
229
+ @manager.run_low_card_method(@invoked_object, "foo", [ ])
230
+ end.should raise_error(ArgumentError)
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,70 @@
1
+ require 'low_card_tables'
2
+
3
+ describe LowCardTables::HasLowCardTable::LowCardObjectsManager do
4
+ before :each do
5
+ @model_instance = double("model_instance")
6
+ @manager = LowCardTables::HasLowCardTable::LowCardObjectsManager.new(@model_instance)
7
+
8
+ @model_class = double("model_class")
9
+ allow(@model_instance).to receive(:class).and_return(@model_class)
10
+
11
+ @lcam = double("low_card_associations_manager")
12
+ allow(@model_class).to receive(:_low_card_associations_manager).and_return(@lcam)
13
+
14
+ @association1 = double("LowCardAssociation")
15
+ allow(@association1).to receive(:association_name).and_return("foo")
16
+ allow(@association1).to receive(:foreign_key_column_name).and_return("blahblah_id")
17
+ end
18
+
19
+ describe "#object_for" do
20
+ it "should call through to the association to create the object, and only once" do
21
+ associated_object = double("associated_object")
22
+
23
+ expect(@lcam).to receive(:_low_card_association).with("foo").and_return(@association1)
24
+ expect(@association1).to receive(:create_low_card_object_for).once.with(@model_instance).and_return(associated_object)
25
+
26
+ @manager.object_for(@association1).should be(associated_object)
27
+ @manager.object_for(@association1).should be(associated_object)
28
+ end
29
+
30
+ it "should maintain multiple associcated objects separately" do
31
+ associated_object_1 = double("associated_object_1")
32
+ associated_object_2 = double("associated_object_2")
33
+
34
+ @association2 = double("LowCardAssociation2")
35
+ allow(@association2).to receive(:association_name).and_return("bar")
36
+
37
+ expect(@lcam).to receive(:_low_card_association).with("foo").and_return(@association1)
38
+ expect(@association1).to receive(:create_low_card_object_for).once.with(@model_instance).and_return(associated_object_1)
39
+
40
+ expect(@lcam).to receive(:_low_card_association).with("bar").and_return(@association2)
41
+ expect(@association2).to receive(:create_low_card_object_for).once.with(@model_instance).and_return(associated_object_2)
42
+
43
+ @manager.object_for(@association1).should be(associated_object_1)
44
+ @manager.object_for(@association2).should be(associated_object_2)
45
+ @manager.object_for(@association1).should be(associated_object_1)
46
+ @manager.object_for(@association2).should be(associated_object_2)
47
+ end
48
+ end
49
+
50
+ describe "foreign-key support" do
51
+ it "should call through to the model instance on get" do
52
+ expect(@model_instance).to receive(:[]).with("blahblah_id").and_return(12345)
53
+ @manager.foreign_key_for(@association1).should == 12345
54
+ end
55
+
56
+ it "should call through to the model instance on set, invalidate the object, and return the new value" do
57
+ associated_object_1 = double("associated_object_1")
58
+ expect(@lcam).to receive(:_low_card_association).with("foo").at_least(:once).and_return(@association1)
59
+ expect(@association1).to receive(:create_low_card_object_for).once.with(@model_instance).and_return(associated_object_1)
60
+ @manager.object_for(@association1).should be(associated_object_1)
61
+
62
+ expect(@model_instance).to receive(:[]=).with("blahblah_id", 23456)
63
+ @manager.set_foreign_key_for(@association1, 23456).should == 23456
64
+
65
+ associated_object_2 = double("associated_object_2")
66
+ expect(@association1).to receive(:create_low_card_object_for).once.with(@model_instance).and_return(associated_object_2)
67
+ @manager.object_for(@association1).should be(associated_object_2)
68
+ end
69
+ end
70
+ end