store_base_sti_class 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.
Files changed (58) hide show
  1. data/Appraisals +25 -0
  2. data/CHANGELOG +2 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +45 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +63 -0
  7. data/Rakefile +47 -0
  8. data/VERSION +1 -0
  9. data/db/storebasestiname_unittest.sql +0 -0
  10. data/gemfiles/rails_3.0.10.gemfile +11 -0
  11. data/gemfiles/rails_3.0.10.gemfile.lock +43 -0
  12. data/gemfiles/rails_3.0.11.gemfile +11 -0
  13. data/gemfiles/rails_3.0.11.gemfile.lock +43 -0
  14. data/gemfiles/rails_3.0.12.gemfile +11 -0
  15. data/gemfiles/rails_3.0.12.gemfile.lock +43 -0
  16. data/gemfiles/rails_3.0.3.gemfile +11 -0
  17. data/gemfiles/rails_3.0.3.gemfile.lock +43 -0
  18. data/gemfiles/rails_3.0.4.gemfile +11 -0
  19. data/gemfiles/rails_3.0.4.gemfile.lock +43 -0
  20. data/gemfiles/rails_3.0.5.gemfile +11 -0
  21. data/gemfiles/rails_3.0.5.gemfile.lock +43 -0
  22. data/gemfiles/rails_3.0.6.gemfile +11 -0
  23. data/gemfiles/rails_3.0.6.gemfile.lock +43 -0
  24. data/gemfiles/rails_3.0.7.gemfile +11 -0
  25. data/gemfiles/rails_3.0.7.gemfile.lock +43 -0
  26. data/gemfiles/rails_3.0.8.gemfile +11 -0
  27. data/gemfiles/rails_3.0.8.gemfile.lock +43 -0
  28. data/gemfiles/rails_3.0.9.gemfile +11 -0
  29. data/gemfiles/rails_3.0.9.gemfile.lock +43 -0
  30. data/gemfiles/rails_3.1.0.gemfile +11 -0
  31. data/gemfiles/rails_3.1.0.gemfile.lock +47 -0
  32. data/gemfiles/rails_3.1.1.gemfile +11 -0
  33. data/gemfiles/rails_3.1.1.gemfile.lock +45 -0
  34. data/gemfiles/rails_3.1.2.gemfile +11 -0
  35. data/gemfiles/rails_3.1.2.gemfile.lock +45 -0
  36. data/gemfiles/rails_3.1.3.gemfile +11 -0
  37. data/gemfiles/rails_3.1.3.gemfile.lock +45 -0
  38. data/gemfiles/rails_3.1.4.gemfile +11 -0
  39. data/gemfiles/rails_3.1.4.gemfile.lock +45 -0
  40. data/gemfiles/rails_3.2.0.gemfile +11 -0
  41. data/gemfiles/rails_3.2.0.gemfile.lock +45 -0
  42. data/gemfiles/rails_3.2.1.gemfile +11 -0
  43. data/gemfiles/rails_3.2.1.gemfile.lock +45 -0
  44. data/gemfiles/rails_3.2.2.gemfile +11 -0
  45. data/gemfiles/rails_3.2.2.gemfile.lock +45 -0
  46. data/gemfiles/rails_3.2.3.gemfile +11 -0
  47. data/gemfiles/rails_3.2.3.gemfile.lock +45 -0
  48. data/lib/store_base_sti_class.rb +7 -0
  49. data/lib/store_base_sti_class_for_3_0.rb +470 -0
  50. data/lib/store_base_sti_class_for_3_1_and_above.rb +304 -0
  51. data/store_base_sti_class.gemspec +111 -0
  52. data/storebasestiname_unittest.sql +0 -0
  53. data/test/connection.rb +22 -0
  54. data/test/helper.rb +63 -0
  55. data/test/models.rb +48 -0
  56. data/test/schema.rb +35 -0
  57. data/test/test_store_base_sti_class.rb +161 -0
  58. metadata +205 -0
@@ -0,0 +1,304 @@
1
+ require 'active_record'
2
+
3
+ if ActiveRecord::VERSION::STRING =~ /^3\.(1|2)/
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
+ # 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 = "store_base_sti_class"
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 = ["Andrew Mutz"]
12
+ s.date = "2012-05-09"
13
+ s.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 "
14
+ s.email = "andrew.mutz@appfolio.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "Appraisals",
21
+ "CHANGELOG",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "db/storebasestiname_unittest.sql",
29
+ "gemfiles/rails_3.0.10.gemfile",
30
+ "gemfiles/rails_3.0.10.gemfile.lock",
31
+ "gemfiles/rails_3.0.11.gemfile",
32
+ "gemfiles/rails_3.0.11.gemfile.lock",
33
+ "gemfiles/rails_3.0.12.gemfile",
34
+ "gemfiles/rails_3.0.12.gemfile.lock",
35
+ "gemfiles/rails_3.0.3.gemfile",
36
+ "gemfiles/rails_3.0.3.gemfile.lock",
37
+ "gemfiles/rails_3.0.4.gemfile",
38
+ "gemfiles/rails_3.0.4.gemfile.lock",
39
+ "gemfiles/rails_3.0.5.gemfile",
40
+ "gemfiles/rails_3.0.5.gemfile.lock",
41
+ "gemfiles/rails_3.0.6.gemfile",
42
+ "gemfiles/rails_3.0.6.gemfile.lock",
43
+ "gemfiles/rails_3.0.7.gemfile",
44
+ "gemfiles/rails_3.0.7.gemfile.lock",
45
+ "gemfiles/rails_3.0.8.gemfile",
46
+ "gemfiles/rails_3.0.8.gemfile.lock",
47
+ "gemfiles/rails_3.0.9.gemfile",
48
+ "gemfiles/rails_3.0.9.gemfile.lock",
49
+ "gemfiles/rails_3.1.0.gemfile",
50
+ "gemfiles/rails_3.1.0.gemfile.lock",
51
+ "gemfiles/rails_3.1.1.gemfile",
52
+ "gemfiles/rails_3.1.1.gemfile.lock",
53
+ "gemfiles/rails_3.1.2.gemfile",
54
+ "gemfiles/rails_3.1.2.gemfile.lock",
55
+ "gemfiles/rails_3.1.3.gemfile",
56
+ "gemfiles/rails_3.1.3.gemfile.lock",
57
+ "gemfiles/rails_3.1.4.gemfile",
58
+ "gemfiles/rails_3.1.4.gemfile.lock",
59
+ "gemfiles/rails_3.2.0.gemfile",
60
+ "gemfiles/rails_3.2.0.gemfile.lock",
61
+ "gemfiles/rails_3.2.1.gemfile",
62
+ "gemfiles/rails_3.2.1.gemfile.lock",
63
+ "gemfiles/rails_3.2.2.gemfile",
64
+ "gemfiles/rails_3.2.2.gemfile.lock",
65
+ "gemfiles/rails_3.2.3.gemfile",
66
+ "gemfiles/rails_3.2.3.gemfile.lock",
67
+ "lib/store_base_sti_class.rb",
68
+ "lib/store_base_sti_class_for_3_0.rb",
69
+ "lib/store_base_sti_class_for_3_1_and_above.rb",
70
+ "store_base_sti_class.gemspec",
71
+ "storebasestiname_unittest.sql",
72
+ "test/connection.rb",
73
+ "test/helper.rb",
74
+ "test/models.rb",
75
+ "test/schema.rb",
76
+ "test/test_store_base_sti_class.rb"
77
+ ]
78
+ s.homepage = "http://github.com/appfolio/store_base_sti_class"
79
+ s.licenses = ["MIT"]
80
+ s.require_paths = ["lib"]
81
+ s.rubygems_version = "1.8.21"
82
+ s.summary = "Modifies ActiveRecord 3.0.5+ with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI"
83
+
84
+ if s.respond_to? :specification_version then
85
+ s.specification_version = 3
86
+
87
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
88
+ s.add_runtime_dependency(%q<appraisal>, [">= 0"])
89
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
90
+ s.add_runtime_dependency(%q<sqlite3>, [">= 0"])
91
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
92
+ s.add_development_dependency(%q<rcov>, [">= 0"])
93
+ s.add_development_dependency(%q<bundler>, [">= 0"])
94
+ else
95
+ s.add_dependency(%q<appraisal>, [">= 0"])
96
+ s.add_dependency(%q<activerecord>, [">= 0"])
97
+ s.add_dependency(%q<sqlite3>, [">= 0"])
98
+ s.add_dependency(%q<jeweler>, [">= 0"])
99
+ s.add_dependency(%q<rcov>, [">= 0"])
100
+ s.add_dependency(%q<bundler>, [">= 0"])
101
+ end
102
+ else
103
+ s.add_dependency(%q<appraisal>, [">= 0"])
104
+ s.add_dependency(%q<activerecord>, [">= 0"])
105
+ s.add_dependency(%q<sqlite3>, [">= 0"])
106
+ s.add_dependency(%q<jeweler>, [">= 0"])
107
+ s.add_dependency(%q<rcov>, [">= 0"])
108
+ s.add_dependency(%q<bundler>, [">= 0"])
109
+ end
110
+ end
111
+
Binary file
@@ -0,0 +1,22 @@
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
+ ActiveRecord::Base.configurations = {
16
+ 'unittest' => {
17
+ :adapter => 'sqlite3',
18
+ :database => 'db/storebasestiname_unittest.sql',
19
+ }
20
+ }
21
+
22
+ ActiveRecord::Base.establish_connection 'unittest'
data/test/helper.rb ADDED
@@ -0,0 +1,63 @@
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'
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'
28
+
29
+ # the following is needed because ActiveRecord::TestCase uses ActiveRecord::SQLCounter, which is
30
+ # not bundled as part of the gem
31
+ if ActiveRecord::VERSION::STRING =~ /^3\.2/
32
+ module ActiveRecord
33
+ class SQLCounter
34
+ cattr_accessor :ignored_sql
35
+ self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
36
+
37
+ # FIXME: this needs to be refactored so specific database can add their own
38
+ # ignored SQL. This ignored SQL is for Oracle.
39
+ ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
40
+
41
+ cattr_accessor :log
42
+ self.log = []
43
+
44
+ attr_reader :ignore
45
+
46
+ def initialize(ignore = self.class.ignored_sql)
47
+ @ignore = ignore
48
+ end
49
+
50
+ def call(name, start, finish, message_id, values)
51
+ sql = values[:sql]
52
+
53
+ # FIXME: this seems bad. we should probably have a better way to indicate
54
+ # the query was cached
55
+ return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
56
+ self.class.log << sql
57
+ end
58
+ end
59
+
60
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
61
+ end
62
+ end
63
+
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