store_base_sti_class_for_3_1 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ 0.0.1
2
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source :rubygems
2
+
3
+ gem "activerecord", ">= 3.1.0"
4
+
5
+ group :development do
6
+ gem "mysql2", "=0.3.7"
7
+ gem "bundler", "~> 1.0.0"
8
+ gem "jeweler", "~> 1.5.2"
9
+ gem "rcov", ">= 0"
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,37 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.1.3)
5
+ activesupport (= 3.1.3)
6
+ builder (~> 3.0.0)
7
+ i18n (~> 0.6)
8
+ activerecord (3.1.3)
9
+ activemodel (= 3.1.3)
10
+ activesupport (= 3.1.3)
11
+ arel (~> 2.2.1)
12
+ tzinfo (~> 0.3.29)
13
+ activesupport (3.1.3)
14
+ multi_json (~> 1.0)
15
+ arel (2.2.1)
16
+ builder (3.0.0)
17
+ git (1.2.5)
18
+ i18n (0.6.0)
19
+ jeweler (1.5.2)
20
+ bundler (~> 1.0.0)
21
+ git (>= 1.2.5)
22
+ rake
23
+ multi_json (1.0.4)
24
+ mysql2 (0.3.7)
25
+ rake (0.9.1)
26
+ rcov (0.9.9)
27
+ tzinfo (0.3.31)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ activerecord (>= 3.1.0)
34
+ bundler (~> 1.0.0)
35
+ jeweler (~> 1.5.2)
36
+ mysql2 (= 0.3.7)
37
+ rcov
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 AppFolio, inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ == Description
2
+
3
+ Given the following class definitions,
4
+
5
+ class Address
6
+ belongs_to :addressable, :polymorphic => true
7
+ end
8
+
9
+ class Person
10
+ has_many :addresses, :as => addressable
11
+ end
12
+
13
+ class Vendor < Person
14
+ end
15
+
16
+ and given the following code,
17
+
18
+ vendor = Vendor.create(...)
19
+ address = vendor.addresses.create(...)
20
+
21
+ p vendor
22
+ p address
23
+
24
+ will output,
25
+
26
+ #<Vendor id: 1, type: "Vendor" ...>
27
+ #<Address id: 1, addressable_id: 1, addressable_type: 'Person' ...>
28
+
29
+ Notice that addressable_type column is Person even though the actual class is Vendor.
30
+
31
+ Normally, this isn't a problem, however it can have negative performance characteristic in certain circumstances. The most obvious one is that
32
+ a join with persons or an extra query is required to find out the actual type of addressable.
33
+
34
+ This gem add ActiveRecord::Base.store_base_sti_class configuration option. It defaults to true for backwards compatibility. Setting it false will alter ActiveRecord's behavior to store the actual class in polymorphic _type columns when STI is used.
35
+
36
+ In the example above, if the ActiveRecord::Base.store_base_sti_class is false, the output will be,
37
+
38
+ #<Vendor id: 1, type: "Vendor" ...>
39
+ #<Address id: 1, addressable_id: 1, addressable_type: 'Vendor' ...>
40
+
41
+ == Usage
42
+
43
+ Add the following line to your Gemfile,
44
+
45
+ gem 'store_base_sti_class_for_3_1'
46
+
47
+ then bundle install. Once you have the gem installed, add the following to one of the initializers (or make a new one) in config/initializers,
48
+
49
+ ActiveRecord::Base.store_base_sti_class = false
50
+
51
+ When changing this behavior you will have write a migration to update all of your existing _type columns accordingly. You may also need to change your application if it explicitly relies on the _type columns.
52
+
53
+ == Notes
54
+
55
+ The gem has used the test cases from https://github.com/pkmiec/store_base_sti_class_for_3_0, but has been completely rewritten for 3.1. It currently works with ActiveRecord 3.1.0 through 3.1.3.
56
+
57
+ == Copyright
58
+
59
+ Copyright (c) 2011 AppFolio, inc. See LICENSE.txt for
60
+ further details.
61
+
data/Rakefile ADDED
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "store_base_sti_class_for_3_1"
16
+ gem.homepage = "http://github.com/appfolio/store_base_sti_class_for_3_1"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{
19
+ Modifies ActiveRecord 3.1.x with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI
20
+ }
21
+ gem.description = %Q{
22
+ ActiveRecord has always stored the base class in polymorphic _type columns when using STI. This can have non-trivial
23
+ performance implications in certain cases. This gem adds 'store_base_sti_class' configuration options which controls
24
+ whether ActiveRecord will store the base class or the actual class. Default to true for backwards compatibility.
25
+ }
26
+ gem.email = "andrew.mutz@appfolio.com"
27
+ gem.authors = ["Andrew Mutz"]
28
+
29
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
30
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
31
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
32
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
33
+ end
34
+ Jeweler::RubygemsDotOrgTasks.new
35
+
36
+ require 'rake/testtask'
37
+ Rake::TestTask.new(:test) do |test|
38
+ test.libs << 'lib' << 'test'
39
+ test.pattern = 'test/**/test_*.rb'
40
+ test.verbose = true
41
+ end
42
+
43
+ require 'rcov/rcovtask'
44
+ Rcov::RcovTask.new do |test|
45
+ test.libs << 'test'
46
+ test.pattern = 'test/**/test_*.rb'
47
+ test.verbose = true
48
+ end
49
+
50
+ task :default => :test
51
+
52
+ require 'rake/rdoctask'
53
+ Rake::RDocTask.new do |rdoc|
54
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
55
+
56
+ rdoc.rdoc_dir = 'rdoc'
57
+ rdoc.title = "store_base_sti_class_for_3_1 #{version}"
58
+ rdoc.rdoc_files.include('README*')
59
+ rdoc.rdoc_files.include('lib/**/*.rb')
60
+ end
61
+
62
+ namespace :mysql do
63
+ desc 'Build the MySQL test databases'
64
+ task :build_databases do
65
+ %x( echo "create DATABASE storebasestiname_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=root)
66
+ end
67
+
68
+ desc 'Drop the MySQL test databases'
69
+ task :drop_databases do
70
+ %x( mysqladmin --user=root -f drop storebasestiname_unittest )
71
+ end
72
+
73
+ desc 'Rebuild the MySQL test databases'
74
+ task :rebuild_databases => [:drop_databases, :build_databases]
75
+ end
76
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,304 @@
1
+ require 'active_record'
2
+
3
+ if ActiveRecord::VERSION::STRING =~ /^3\.1/
4
+ module ActiveRecord
5
+
6
+ class Base
7
+ class_attribute :store_base_sti_class
8
+ self.store_base_sti_class = true
9
+ end
10
+
11
+ module Associations
12
+ class Association
13
+
14
+ def creation_attributes
15
+ attributes = {}
16
+
17
+ if reflection.macro.in?([:has_one, :has_many]) && !options[:through]
18
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
19
+
20
+ if reflection.options[:as]
21
+ # START PATCH
22
+ # original:
23
+ # attributes[reflection.type] = owner.class.base_class.name
24
+
25
+ attributes[reflection.type] = ActiveRecord::Base.store_base_sti_class ? owner.class.base_class.name : owner.class.name
26
+
27
+ # END PATCH
28
+ end
29
+ end
30
+
31
+ attributes
32
+ end
33
+
34
+ end
35
+
36
+ class JoinDependency # :nodoc:
37
+ class JoinAssociation < JoinPart # :nodoc:
38
+ def join_to(relation)
39
+
40
+ tables = @tables.dup
41
+ foreign_table = parent_table
42
+ foreign_klass = parent.active_record
43
+
44
+ # The chain starts with the target table, but we want to end with it here (makes
45
+ # more sense in this context), so we reverse
46
+ chain.reverse.each_with_index do |reflection, i|
47
+ table = tables.shift
48
+
49
+ case reflection.source_macro
50
+ when :belongs_to
51
+ key = reflection.association_primary_key
52
+ foreign_key = reflection.foreign_key
53
+ when :has_and_belongs_to_many
54
+ # Join the join table first...
55
+ relation.from(join(
56
+ table,
57
+ table[reflection.foreign_key].
58
+ eq(foreign_table[reflection.active_record_primary_key])
59
+ ))
60
+
61
+ foreign_table, table = table, tables.shift
62
+
63
+ key = reflection.association_primary_key
64
+ foreign_key = reflection.association_foreign_key
65
+ else
66
+ key = reflection.foreign_key
67
+ foreign_key = reflection.active_record_primary_key
68
+ end
69
+
70
+ constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
71
+
72
+ conditions = self.conditions[i].dup
73
+
74
+ # START PATCH
75
+ # original:
76
+ # conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type
77
+
78
+ if ActiveRecord::Base.store_base_sti_class
79
+ conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type
80
+ else
81
+ conditions << { reflection.type => ([foreign_klass] + foreign_klass.descendants).map(&:name) } if reflection.type
82
+ end
83
+
84
+ # END PATCH
85
+
86
+ unless conditions.empty?
87
+ constraint = constraint.and(sanitize(conditions, table))
88
+ end
89
+
90
+ relation.from(join(table, constraint))
91
+
92
+ # The current table in this iteration becomes the foreign table in the next
93
+ foreign_table, foreign_klass = table, reflection.klass
94
+ end
95
+
96
+ relation
97
+ end
98
+ end
99
+ end
100
+
101
+
102
+ class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
103
+
104
+ private
105
+
106
+ def replace_keys(record)
107
+ super
108
+ # START PATCH
109
+ # original: owner[reflection.foreign_type] = record && record.class.base_class.name
110
+ unless ActiveRecord::Base.store_base_sti_class
111
+ owner[reflection.foreign_type] = record && record.class.sti_name
112
+ else
113
+ owner[reflection.foreign_type] = record && record.class.base_class.name
114
+ end
115
+ #END PATCH
116
+ end
117
+ end
118
+ end
119
+ module Associations
120
+ class Preloader
121
+ class Association
122
+ private
123
+ def build_scope
124
+
125
+ scope = klass.scoped
126
+
127
+ scope = scope.where(process_conditions(options[:conditions]))
128
+ scope = scope.where(process_conditions(preload_options[:conditions]))
129
+
130
+ scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
131
+ scope = scope.includes(preload_options[:include] || options[:include])
132
+
133
+
134
+
135
+ if options[:as]
136
+ scope = scope.where(
137
+ klass.table_name => {
138
+ #START PATCH
139
+ #original: reflection.type => model.base_class.sti_name
140
+ reflection.type => ActiveRecord::Base.store_base_sti_class ? model.base_class.sti_name : model.sti_name
141
+ #END PATCH
142
+
143
+ }
144
+ )
145
+ end
146
+
147
+ scope
148
+ end
149
+ end
150
+
151
+ module ThroughAssociation
152
+ def through_options
153
+ through_options = {}
154
+ if options[:source_type]
155
+ #START PATCH
156
+ #original: through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
157
+ through_options[:conditions] = { reflection.foreign_type => ([options[:source_type].constantize] + options[:source_type].constantize.descendants).map(&:to_s) }
158
+ #END PATCH
159
+ else
160
+ if options[:conditions]
161
+ through_options[:include] = options[:include] || options[:source]
162
+ through_options[:conditions] = options[:conditions]
163
+ end
164
+
165
+ through_options[:order] = options[:order]
166
+ end
167
+ through_options
168
+ end
169
+ end
170
+ end
171
+
172
+ class AssociationScope
173
+ def add_constraints(scope)
174
+
175
+ tables = construct_tables
176
+
177
+ chain.each_with_index do |reflection, i|
178
+ table, foreign_table = tables.shift, tables.first
179
+
180
+ if reflection.source_macro == :has_and_belongs_to_many
181
+ join_table = tables.shift
182
+
183
+ scope = scope.joins(join(
184
+ join_table,
185
+ table[reflection.association_primary_key].
186
+ eq(join_table[reflection.association_foreign_key])
187
+ ))
188
+
189
+ table, foreign_table = join_table, tables.first
190
+ end
191
+
192
+ if reflection.source_macro == :belongs_to
193
+ if reflection.options[:polymorphic]
194
+ # START PATCH
195
+ # This line exists to support multiple versions of AR 3.1
196
+ # original in 3.1.3: key = reflection.association_primary_key
197
+
198
+ key = (reflection.method(:association_primary_key).arity == 0) ? reflection.association_primary_key : reflection.association_primary_key(klass)
199
+ # END PATCH
200
+ else
201
+ key = reflection.association_primary_key
202
+ end
203
+
204
+ foreign_key = reflection.foreign_key
205
+ else
206
+ key = reflection.foreign_key
207
+ foreign_key = reflection.active_record_primary_key
208
+ end
209
+
210
+ conditions = self.conditions[i]
211
+
212
+ if reflection == chain.last
213
+ scope = scope.where(table[key].eq(owner[foreign_key]))
214
+
215
+ if reflection.type
216
+ # START PATCH
217
+ # original: scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
218
+
219
+ unless ActiveRecord::Base.store_base_sti_class
220
+ scope = scope.where(table[reflection.type].eq(owner.class.name))
221
+ else
222
+ scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
223
+ end
224
+
225
+ # END PATCH
226
+ end
227
+
228
+ conditions.each do |condition|
229
+ if options[:through] && condition.is_a?(Hash)
230
+ condition = { table.name => condition }
231
+ end
232
+
233
+ scope = scope.where(interpolate(condition))
234
+ end
235
+ else
236
+ constraint = table[key].eq(foreign_table[foreign_key])
237
+
238
+ if reflection.type
239
+ # START PATCH
240
+ # original: type = chain[i + 1].klass.base_class.name
241
+ # constraint = constraint.and(table[reflection.type].eq(type))
242
+
243
+ if ActiveRecord::Base.store_base_sti_class
244
+ type = chain[i + 1].klass.base_class.name
245
+ constraint = constraint.and(table[reflection.type].eq(type))
246
+ else
247
+ klass = chain[i + 1].klass
248
+ constraint = constraint.and(table[reflection.type].in(([klass] + klass.descendants).map(&:name)))
249
+ end
250
+
251
+ # END PATCH
252
+ end
253
+
254
+ scope = scope.joins(join(foreign_table, constraint))
255
+
256
+ unless conditions.empty?
257
+ scope = scope.where(sanitize(conditions, table))
258
+ end
259
+ end
260
+ end
261
+
262
+ scope
263
+ end
264
+
265
+ end
266
+ end
267
+ module Reflection
268
+ class ThroughReflection < AssociationReflection
269
+
270
+ def conditions
271
+ @conditions ||= begin
272
+ conditions = source_reflection.conditions.map { |c| c.dup }
273
+
274
+ # Add to it the conditions from this reflection if necessary.
275
+ conditions.first << options[:conditions] if options[:conditions]
276
+
277
+ through_conditions = through_reflection.conditions
278
+
279
+ if options[:source_type]
280
+ # START PATCH
281
+ # original: through_conditions.first << { foreign_type => options[:source_type] }
282
+
283
+ unless ActiveRecord::Base.store_base_sti_class
284
+ through_conditions.first << { foreign_type => ([options[:source_type].constantize] + options[:source_type].constantize.descendants).map(&:to_s) }
285
+ else
286
+ through_conditions.first << { foreign_type => options[:source_type] }
287
+ end
288
+
289
+ # END PATCH
290
+ end
291
+
292
+ # Recursively fill out the rest of the array from the through reflection
293
+ conditions += through_conditions
294
+
295
+ # And return
296
+ conditions
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ end
303
+
304
+ end
@@ -0,0 +1,111 @@
1
+ diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
2
+ index 9e6d9e7..cbf176e 100644
3
+ --- a/activerecord/lib/active_record/associations/association_scope.rb
4
+ +++ b/activerecord/lib/active_record/associations/association_scope.rb
5
+ @@ -81,7 +81,11 @@ module ActiveRecord
6
+ scope = scope.where(table[key].eq(owner[foreign_key]))
7
+
8
+ if reflection.type
9
+ - scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
10
+ + unless ActiveRecord::Base.store_base_sti_class
11
+ + scope = scope.where(table[reflection.type].eq(owner.class.name))
12
+ + else
13
+ + scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
14
+ + end
15
+ end
16
+
17
+ conditions.each do |condition|
18
+ diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
19
+ index 2ee5dbb..837abfb 100644
20
+ --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
21
+ +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
22
+ @@ -11,7 +11,11 @@ module ActiveRecord
23
+
24
+ def replace_keys(record)
25
+ super
26
+ - owner[reflection.foreign_type] = record && record.class.base_class.name
27
+ + unless ActiveRecord::Base.store_base_sti_class
28
+ + owner[reflection.foreign_type] = record && record.class.sti_name
29
+ + else
30
+ + owner[reflection.foreign_type] = record && record.class.base_class.name
31
+ + end
32
+ end
33
+
34
+ def different_target?(record)
35
+ diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
36
+ index 03963ab..3ad1aeb 100644
37
+ --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
38
+ +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
39
+ @@ -93,7 +93,12 @@ module ActiveRecord
40
+ constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
41
+
42
+ conditions = self.conditions[i].dup
43
+ - conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type
44
+ +
45
+ + if ActiveRecord::Base.store_base_sti_class
46
+ + conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type
47
+ + else
48
+ + conditions << { reflection.type => ([foreign_klass.base_class] + foreign_klass.base_class.descendants).map(&:name) } if reflection.type
49
+ + end
50
+
51
+ unless conditions.empty?
52
+ constraint = constraint.and(sanitize(conditions, table))
53
+ diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
54
+ index 779f816..e99cfcb 100644
55
+ --- a/activerecord/lib/active_record/associations/preloader/association.rb
56
+ +++ b/activerecord/lib/active_record/associations/preloader/association.rb
57
+ @@ -104,7 +104,7 @@ module ActiveRecord
58
+ if options[:as]
59
+ scope = scope.where(
60
+ klass.table_name => {
61
+ - reflection.type => model.base_class.sti_name
62
+ + reflection.type => ActiveRecord::Base.store_base_sti_class ? model.base_class.sti_name : model.sti_name
63
+ }
64
+ )
65
+ end
66
+ diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
67
+ index ad6374d..7e14fa0 100644
68
+ --- a/activerecord/lib/active_record/associations/preloader/through_association.rb
69
+ +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
70
+ @@ -49,7 +49,7 @@ module ActiveRecord
71
+ through_options = {}
72
+
73
+ if options[:source_type]
74
+ - through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
75
+ + through_options[:conditions] = { reflection.foreign_type => ([options[:source_type].constantize] + options[:source_type].constantize.descendants).map(&:to_s) }
76
+ else
77
+ if options[:conditions]
78
+ through_options[:include] = options[:include] || options[:source]
79
+ diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
80
+ index c866736..3673294 100644
81
+ --- a/activerecord/lib/active_record/base.rb
82
+ +++ b/activerecord/lib/active_record/base.rb
83
+ @@ -424,6 +424,11 @@ module ActiveRecord #:nodoc:
84
+ # Determine whether to store the full constant name including namespace when using STI
85
+ class_attribute :store_full_sti_class
86
+ self.store_full_sti_class = true
87
+ +
88
+ + # Store the actual class (instead of the base class) in polymorhic _type columns when using STI
89
+ + class_attribute :store_base_sti_class
90
+ + self.store_base_sti_class = true
91
+ +
92
+
93
+ # Stores the default scope for the class
94
+ class_attribute :default_scopes, :instance_writer => false
95
+ diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
96
+ index 6ddf76e..6affef3 100644
97
+ --- a/activerecord/lib/active_record/reflection.rb
98
+ +++ b/activerecord/lib/active_record/reflection.rb
99
+ @@ -451,7 +451,11 @@ module ActiveRecord
100
+ through_conditions = through_reflection.conditions
101
+
102
+ if options[:source_type]
103
+ - through_conditions.first << { foreign_type => options[:source_type] }
104
+ + unless ActiveRecord::Base.store_base_sti_class
105
+ + through_conditions.first << { foreign_type => ([options[:source_type].constantize] + options[:source_type].constantize.descendants).map(&:to_s) }
106
+ + else
107
+ + through_conditions.first << { foreign_type => options[:source_type] }
108
+ + end
109
+ end
110
+
111
+ # Recursively fill out the rest of the array from the through reflection
@@ -0,0 +1,77 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{store_base_sti_class_for_3_1}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Andrew Mutz}]
12
+ s.date = %q{2011-12-23}
13
+ s.description = %q{
14
+ ActiveRecord has always stored the base class in polymorphic _type columns when using STI. This can have non-trivial
15
+ performance implications in certain cases. This gem adds 'store_base_sti_class' configuration options which controls
16
+ whether ActiveRecord will store the base class or the actual class. Default to true for backwards compatibility.
17
+ }
18
+ s.email = %q{andrew.mutz@appfolio.com}
19
+ s.extra_rdoc_files = [
20
+ "LICENSE.txt",
21
+ "README.rdoc"
22
+ ]
23
+ s.files = [
24
+ "CHANGELOG",
25
+ "Gemfile",
26
+ "Gemfile.lock",
27
+ "LICENSE.txt",
28
+ "README.rdoc",
29
+ "Rakefile",
30
+ "VERSION",
31
+ "lib/store_base_sti_class_for_3_1.rb",
32
+ "polymorphic_and_sti_fix_for_rails_3_1.diff",
33
+ "store_base_sti_class_for_3_1.gemspec",
34
+ "test/connection.rb",
35
+ "test/helper.rb",
36
+ "test/models.rb",
37
+ "test/schema.rb",
38
+ "test/test_store_base_sti_class_for_3_1.rb"
39
+ ]
40
+ s.homepage = %q{http://github.com/appfolio/store_base_sti_class_for_3_1}
41
+ s.licenses = [%q{MIT}]
42
+ s.require_paths = [%q{lib}]
43
+ s.rubygems_version = %q{1.8.5}
44
+ s.summary = %q{Modifies ActiveRecord 3.1.x with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI}
45
+ s.test_files = [
46
+ "test/connection.rb",
47
+ "test/helper.rb",
48
+ "test/models.rb",
49
+ "test/schema.rb",
50
+ "test/test_store_base_sti_class_for_3_1.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ s.specification_version = 3
55
+
56
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
+ s.add_runtime_dependency(%q<activerecord>, [">= 3.1.0"])
58
+ s.add_development_dependency(%q<mysql2>, ["= 0.3.7"])
59
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
60
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
61
+ s.add_development_dependency(%q<rcov>, [">= 0"])
62
+ else
63
+ s.add_dependency(%q<activerecord>, [">= 3.1.0"])
64
+ s.add_dependency(%q<mysql2>, ["= 0.3.7"])
65
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
66
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
67
+ s.add_dependency(%q<rcov>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<activerecord>, [">= 3.1.0"])
71
+ s.add_dependency(%q<mysql2>, ["= 0.3.7"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
74
+ s.add_dependency(%q<rcov>, [">= 0"])
75
+ end
76
+ end
77
+
@@ -0,0 +1,16 @@
1
+ require 'logger'
2
+
3
+ ActiveRecord::Base.logger = Logger.new("debug.log")
4
+
5
+ # GRANT ALL PRIVILEGES ON storebasestiname_unittest.* to 'root'@'localhost';
6
+
7
+ ActiveRecord::Base.configurations = {
8
+ 'unittest' => {
9
+ :adapter => 'mysql2',
10
+ :username => 'root',
11
+ :encoding => 'utf8',
12
+ :database => 'storebasestiname_unittest',
13
+ }
14
+ }
15
+
16
+ ActiveRecord::Base.establish_connection 'unittest'
data/test/helper.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ require 'store_base_sti_class_for_3_1'
15
+
16
+ require 'connection'
17
+
18
+ # silence verbose schema loading
19
+ original_stdout = $stdout
20
+ $stdout = StringIO.new
21
+ begin
22
+ require "schema.rb"
23
+ ensure
24
+ $stdout = original_stdout
25
+ end
26
+
27
+ require 'models'
data/test/models.rb ADDED
@@ -0,0 +1,48 @@
1
+ class Author < ActiveRecord::Base
2
+ has_many :posts
3
+
4
+ has_many :tagging, :through => :posts # through polymorphic has_one
5
+ has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many
6
+ has_many :tags, :through => :posts # through has_many :through
7
+ end
8
+
9
+ class Post < ActiveRecord::Base
10
+ belongs_to :author
11
+
12
+ has_one :tagging, :as => :taggable
13
+ has_many :taggings, :as => :taggable
14
+ has_many :tags, :through => :taggings
15
+ end
16
+
17
+ class SpecialPost < Post
18
+ end
19
+
20
+ class Tagging < ActiveRecord::Base
21
+ belongs_to :tag, :include => :tagging
22
+ belongs_to :polytag, :polymorphic => true
23
+ belongs_to :taggable, :polymorphic => true, :counter_cache => true
24
+ end
25
+
26
+ class Tag < ActiveRecord::Base
27
+ has_one :tagging
28
+
29
+ has_many :taggings
30
+ has_many :taggables, :through => :taggings
31
+ has_many :tagged_posts, :through => :taggings, :source => :taggable, :source_type => 'Post'
32
+
33
+ has_many :polytaggings, :as => :polytag, :class_name => 'Tagging'
34
+ has_many :polytagged_posts, :through => :polytaggings, :source => :taggable, :source_type => 'Post'
35
+
36
+ has_many :authors, :class_name => "Author", :finder_sql => proc {
37
+ <<-SQL
38
+ SELECT authors.* FROM authors
39
+ INNER JOIN posts p ON authors.id = p.author_id
40
+ INNER JOIN taggings tgs ON tgs.taggable_id = p.id AND tgs.taggable_type = "Post"
41
+ WHERE tgs.tag_id = #{self.id}
42
+ SQL
43
+ }
44
+
45
+ end
46
+
47
+ class SpecialTag < Tag
48
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,35 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ # Please keep these create table statements in alphabetical order
4
+ # unless the ordering matters. In which case, define them below
5
+
6
+ create_table :authors, :force => true do |t|
7
+ t.string :name, :null => false
8
+ end
9
+
10
+ create_table :posts, :force => true do |t|
11
+ t.string :type
12
+
13
+ t.integer :author_id
14
+ t.string :title, :null => false
15
+ t.text :body, :null => false
16
+ t.integer :taggings_count, :default => 0
17
+ end
18
+
19
+ create_table :taggings, :force => true do |t|
20
+ t.integer :tag_id
21
+
22
+ t.integer :polytag_id
23
+ t.string :polytag_type
24
+
25
+ t.string :taggable_type
26
+ t.integer :taggable_id
27
+ end
28
+
29
+ create_table :tags, :force => true do |t|
30
+ t.string :type
31
+ t.string :name
32
+ t.integer :taggings_count, :default => 0
33
+ end
34
+
35
+ end
@@ -0,0 +1,161 @@
1
+ require 'helper'
2
+ require 'active_record/test_case'
3
+
4
+ class TestStoreBaseStiNameFor30 < ActiveRecord::TestCase
5
+
6
+ def setup
7
+ @old_store_base_sti_class = ActiveRecord::Base.store_base_sti_class
8
+ ActiveRecord::Base.store_base_sti_class = false
9
+
10
+ @thinking_post = SpecialPost.create(:title => 'Thinking')
11
+ @misc_tag = Tag.create(:name => 'Misc')
12
+ end
13
+
14
+ def teardown
15
+ ActiveRecord::Base.store_base_sti_class = @old_store_base_sti_class
16
+ end
17
+
18
+ def test_polymorphic_belongs_to_assignment_with_inheritance
19
+ # should update when assigning a saved record
20
+ tagging = Tagging.new
21
+ post = SpecialPost.create(:title => 'Budget Forecasts Bigger 2011 Deficit')
22
+ tagging.taggable = post
23
+ assert_equal post.id, tagging.taggable_id
24
+ assert_equal "SpecialPost", tagging.taggable_type
25
+
26
+ # should update when assigning a new record
27
+ tagging = Tagging.new
28
+ post = SpecialPost.new(:title => 'Budget Forecasts Bigger 2011 Deficit')
29
+ tagging.taggable = post
30
+ assert_nil tagging.taggable_id
31
+ assert_equal "SpecialPost", tagging.taggable_type
32
+ end
33
+
34
+ def test_polymorphic_has_many_create_model_with_inheritance
35
+ post = SpecialPost.new(:title => 'Budget Forecasts Bigger 2011 Deficit')
36
+
37
+ tagging = @misc_tag.taggings.create(:taggable => post)
38
+ assert_equal "SpecialPost", tagging.taggable_type
39
+
40
+ post.reload
41
+ assert_equal [tagging], post.taggings
42
+ end
43
+
44
+ def test_polymorphic_has_one_create_model_with_inheritance
45
+ post = SpecialPost.new(:title => 'Budget Forecasts Bigger 2011 Deficit')
46
+
47
+ tagging = @misc_tag.create_tagging(:taggable => post)
48
+ assert_equal "SpecialPost", tagging.taggable_type
49
+
50
+ post.reload
51
+ assert_equal tagging, post.tagging
52
+ end
53
+
54
+ def test_polymorphic_has_many_create_via_association
55
+ tag = SpecialTag.create!(:name => 'Special')
56
+ tagging = tag.polytaggings.create!
57
+
58
+ assert_equal "SpecialTag", tagging.polytag_type
59
+ end
60
+
61
+ def test_polymorphic_has_many_through_create_via_association
62
+ tag = SpecialTag.create!(:name => 'Special')
63
+ post = tag.polytagged_posts.create!(:title => 'To Be or Not To Be?')
64
+
65
+ assert_equal "SpecialTag", tag.polytaggings.first.polytag_type
66
+ end
67
+
68
+ def test_include_polymorphic_has_one
69
+ post = SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
70
+ tagging = post.create_tagging(:tag => @misc_tag)
71
+
72
+ post = Post.find(post.id, :include => :tagging)
73
+ assert_equal tagging, assert_no_queries { post.tagging }
74
+ end
75
+
76
+ def test_include_polymorphic_has_many
77
+ tag = SpecialTag.create!(:name => 'Special')
78
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
79
+ tag.polytagged_posts << @thinking_post
80
+
81
+ tag = Tag.find(tag.id, :include => :polytaggings)
82
+ assert_equal 2, assert_no_queries { tag.polytaggings.length }
83
+ end
84
+
85
+ def test_include_polymorphic_has_many_through
86
+ tag = SpecialTag.create!(:name => 'Special')
87
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
88
+ tag.polytagged_posts << @thinking_post
89
+
90
+ tag = Tag.find(tag.id, :include => :polytagged_posts)
91
+ assert_equal 2, assert_no_queries { tag.polytagged_posts.length }
92
+ end
93
+
94
+ def test_join_polymorhic_has_many
95
+ tag = SpecialTag.create!(:name => 'Special')
96
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
97
+ tag.polytagged_posts << @thinking_post
98
+
99
+ assert Tag.find_by_id(tag.id, :joins => :polytaggings, :conditions => [ 'taggings.id = ?', tag.polytaggings.first.id ])
100
+ end
101
+
102
+ def test_join_polymorhic_has_many_through
103
+ tag = SpecialTag.create!(:name => 'Special')
104
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
105
+ tag.polytagged_posts << @thinking_post
106
+
107
+ assert Tag.find_by_id(tag.id, :joins => :polytagged_posts, :conditions => [ 'posts.id = ?', tag.polytaggings.first.taggable_id ])
108
+ end
109
+
110
+ def test_has_many_through_polymorphic_has_one
111
+ author = Author.create!(:name => 'Bob')
112
+ post = Post.create!(:title => 'Budget Forecasts Bigger 2011 Deficit', :author => author)
113
+ special_post = SpecialPost.create!(:title => 'IBM Watson''s Jeopardy play', :author => author)
114
+ special_tag = SpecialTag.create!(:name => 'SpecialGeneral')
115
+
116
+ taggings = [ post.taggings.create(:tag => special_tag), special_post.taggings.create(:tag => special_tag) ]
117
+ assert_equal taggings.sort_by(&:id), author.tagging.sort_by(&:id)
118
+ end
119
+
120
+ def test_has_many_polymorphic_with_source_type
121
+ tag = SpecialTag.create!(:name => 'Special')
122
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
123
+ tag.polytagged_posts << @thinking_post
124
+
125
+ tag.save!
126
+ tag.reload
127
+
128
+ tag = Tag.find(tag.id)
129
+ assert_equal 2, tag.polytagged_posts.length
130
+ end
131
+
132
+ def test_polymorphic_has_many_through_with_double_sti_on_join_model
133
+ tag = SpecialTag.create!(:name => 'Special')
134
+ post = @thinking_post
135
+
136
+ tag.polytagged_posts << post
137
+
138
+
139
+ tag.reload
140
+
141
+ assert_equal 1, tag.polytaggings.length
142
+
143
+ tagging = tag.polytaggings.first
144
+
145
+ assert_equal 'SpecialTag', tagging.polytag_type
146
+ assert_equal 'SpecialPost', tagging.taggable_type
147
+
148
+ assert_equal tag, tagging.polytag
149
+ assert_equal post, tagging.taggable
150
+ end
151
+
152
+ def test_finder_sql_is_supported
153
+ author = Author.create!(:name => 'Bob')
154
+ post = Post.create!(:title => 'Budget Forecasts Bigger 2011 Deficit', :author => author)
155
+ special_tag = Tag.create!(:name => 'SpecialGeneral')
156
+ post.taggings.create(:tag => special_tag)
157
+
158
+ assert_equal [author], special_tag.authors
159
+ end
160
+
161
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: store_base_sti_class_for_3_1
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Andrew Mutz
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-14 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ type: :runtime
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 3
31
+ - 1
32
+ - 0
33
+ version: 3.1.0
34
+ requirement: *id001
35
+ name: activerecord
36
+ - !ruby/object:Gem::Dependency
37
+ prerelease: false
38
+ type: :development
39
+ version_requirements: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - "="
43
+ - !ruby/object:Gem::Version
44
+ hash: 29
45
+ segments:
46
+ - 0
47
+ - 3
48
+ - 7
49
+ version: 0.3.7
50
+ requirement: *id002
51
+ name: mysql2
52
+ - !ruby/object:Gem::Dependency
53
+ prerelease: false
54
+ type: :development
55
+ version_requirements: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 23
61
+ segments:
62
+ - 1
63
+ - 0
64
+ - 0
65
+ version: 1.0.0
66
+ requirement: *id003
67
+ name: bundler
68
+ - !ruby/object:Gem::Dependency
69
+ prerelease: false
70
+ type: :development
71
+ version_requirements: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 7
77
+ segments:
78
+ - 1
79
+ - 5
80
+ - 2
81
+ version: 1.5.2
82
+ requirement: *id004
83
+ name: jeweler
84
+ - !ruby/object:Gem::Dependency
85
+ prerelease: false
86
+ type: :development
87
+ version_requirements: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ requirement: *id005
97
+ name: rcov
98
+ description: "\n ActiveRecord has always stored the base class in polymorphic _type columns when using STI. This can have non-trivial\n performance implications in certain cases. This gem adds 'store_base_sti_class' configuration options which controls\n whether ActiveRecord will store the base class or the actual class. Default to true for backwards compatibility.\n "
99
+ email: andrew.mutz@appfolio.com
100
+ executables: []
101
+
102
+ extensions: []
103
+
104
+ extra_rdoc_files:
105
+ - LICENSE.txt
106
+ - README.rdoc
107
+ files:
108
+ - CHANGELOG
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - LICENSE.txt
112
+ - README.rdoc
113
+ - Rakefile
114
+ - VERSION
115
+ - lib/store_base_sti_class_for_3_1.rb
116
+ - polymorphic_and_sti_fix_for_rails_3_1.diff
117
+ - store_base_sti_class_for_3_1.gemspec
118
+ - test/connection.rb
119
+ - test/helper.rb
120
+ - test/models.rb
121
+ - test/schema.rb
122
+ - test/test_store_base_sti_class_for_3_1.rb
123
+ homepage: http://github.com/appfolio/store_base_sti_class_for_3_1
124
+ licenses:
125
+ - MIT
126
+ post_install_message:
127
+ rdoc_options: []
128
+
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ hash: 3
137
+ segments:
138
+ - 0
139
+ version: "0"
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ hash: 3
146
+ segments:
147
+ - 0
148
+ version: "0"
149
+ requirements: []
150
+
151
+ rubyforge_project:
152
+ rubygems_version: 1.8.5
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Modifies ActiveRecord 3.1.x with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI
156
+ test_files:
157
+ - test/connection.rb
158
+ - test/helper.rb
159
+ - test/models.rb
160
+ - test/schema.rb
161
+ - test/test_store_base_sti_class_for_3_1.rb