store_base_sti_class_for_3_1 0.0.1

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