Add the has_many_polymorphs plugin
[cs356-p2-videostore.git] / vendor / plugins / has_many_polymorphs / lib / has_many_polymorphs / class_methods.rb
1
2 module ActiveRecord #:nodoc:
3   module Associations #:nodoc:
4
5 =begin rdoc
6
7 Class methods added to ActiveRecord::Base for setting up polymorphic associations.
8
9 == Notes
10   
11 STI association targets must enumerated and named. For example, if Dog and Cat both inherit from Animal, you still need to say <tt>[:dogs, :cats]</tt>, and not <tt>[:animals]</tt>.
12
13 Namespaced models follow the Rails <tt>underscore</tt> convention. ZooAnimal::Lion becomes <tt>:'zoo_animal/lion'</tt>.
14
15 You do not need to set up any other associations other than for either the regular method or the double. The join associations and all individual and reverse associations are generated for you. However, a join model and table are required. 
16
17 There is a tentative report that you can make the parent model be its own join model, but this is untested.
18
19 =end
20
21    module PolymorphicClassMethods
22    
23      RESERVED_DOUBLES_KEYS = [:conditions, :order, :limit, :offset, :extend, :skip_duplicates, 
24                                    :join_extend, :dependent, :rename_individual_collections,
25                                    :namespace] #:nodoc:
26  
27 =begin rdoc
28
29 This method creates a doubled-sided polymorphic relationship. It must be called on the join model:
30
31   class Devouring < ActiveRecord::Base
32     belongs_to :eater, :polymorphic => true
33     belongs_to :eaten, :polymorphic => true
34   
35     acts_as_double_polymorphic_join(
36       :eaters => [:dogs, :cats], 
37       :eatens => [:cats, :birds]
38     )       
39   end
40
41 The method works by defining one or more special <tt>has_many_polymorphs</tt> association on every model in the target lists, depending on which side of the association it is on. Double self-references will work.
42
43 The two association names and their value arrays are the only required parameters.
44
45 == Available options
46
47 These options are passed through to targets on both sides of the association. If you want to affect only one side, prepend the key with the name of that side. For example, <tt>:eaters_extend</tt>.
48
49 <tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, or <tt>:delete_all</tt>. Controls how the join record gets treated on any association delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
50 <tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
51 <tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with <tt>"\_of_#{association_name}"</tt>.
52 <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
53 <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
54 <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause. 
55 <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
56 <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
57 <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
58 <tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
59
60 =end
61
62       def acts_as_double_polymorphic_join options={}, &extension      
63         
64         collections, options = extract_double_collections(options)
65         
66         # handle the block
67         options[:extend] = (if options[:extend]
68           Array(options[:extend]) + [extension]
69         else 
70           extension
71         end) if extension 
72         
73         collection_option_keys = make_general_option_keys_specific!(options, collections)
74   
75         join_name = self.name.tableize.to_sym
76         collections.each do |association_id, children|
77           parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array
78           
79           begin
80             parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name
81           rescue NoMethodError
82             raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key
83           end
84
85           parents = collections[parent_hash_key]
86           conflicts = (children & parents) # set intersection          
87           parents.each do |plural_parent_name| 
88   
89             parent_class = plural_parent_name._as_class
90             singular_reverse_association_id = parent_hash_key._singularize 
91               
92             internal_options = {
93               :is_double => true,
94               :from => children, 
95               :as => singular_reverse_association_id,
96               :through => join_name.to_sym, 
97               :foreign_key => parent_foreign_key, 
98               :foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
99               :singular_reverse_association_id => singular_reverse_association_id,
100               :conflicts => conflicts
101             }
102             
103             general_options = Hash[*options._select do |key, value|
104               collection_option_keys[association_id].include? key and !value.nil?
105             end.map do |key, value|
106               [key.to_s[association_id.to_s.length+1..-1].to_sym, value]
107             end._flatten_once] # rename side-specific options to general names
108             
109             general_options.each do |key, value|
110               # avoid clobbering keys that appear in both option sets
111               if internal_options[key]
112                 general_options[key] = Array(value) + Array(internal_options[key])
113               end
114             end
115
116             parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
117   
118             if conflicts.include? plural_parent_name 
119               # unify the alternate sides of the conflicting children
120               (conflicts).each do |method_name|
121                 unless parent_class.instance_methods.include?(method_name)
122                   parent_class.send(:define_method, method_name) do
123                     (self.send("#{singular_reverse_association_id}_#{method_name}") + 
124                       self.send("#{association_id._singularize}_#{method_name}")).freeze
125                   end
126                 end     
127               end            
128               
129               # unify the join model... join model is always renamed for doubles, unlike child associations
130               unless parent_class.instance_methods.include?(join_name)
131                 parent_class.send(:define_method, join_name) do
132                   (self.send("#{join_name}_as_#{singular_reverse_association_id}") + 
133                     self.send("#{join_name}_as_#{association_id._singularize}")).freeze
134                 end              
135               end                         
136             else
137               unless parent_class.instance_methods.include?(join_name)
138                 parent_class.send(:alias_method, join_name, "#{join_name}_as_#{singular_reverse_association_id}")
139               end
140             end                      
141   
142           end
143         end
144       end
145       
146       private
147       
148       def extract_double_collections(options)
149         collections = options._select do |key, value| 
150           value.is_a? Array and key.to_s !~ /(#{RESERVED_DOUBLES_KEYS.map(&:to_s).join('|')})$/
151         end
152         
153         raise PolymorphicError, "Couldn't understand options in acts_as_double_polymorphic_join. Valid parameters are your two class collections, and then #{RESERVED_DOUBLES_KEYS.inspect[1..-2]}, with optionally your collection names prepended and joined with an underscore." unless collections.size == 2
154         
155         options = options._select do |key, value| 
156           !collections[key]
157         end
158         
159         [collections, options]
160       end
161       
162       def make_general_option_keys_specific!(options, collections)
163         collection_option_keys = Hash[*collections.keys.map do |key|
164           [key, RESERVED_DOUBLES_KEYS.map{|option| "#{key}_#{option}".to_sym}] 
165         end._flatten_once]    
166       
167         collections.keys.each do |collection|      
168           options.each do |key, value|
169             next if collection_option_keys.values.flatten.include? key
170             # shift the general options to the individual sides
171             collection_key = "#{collection}_#{key}".to_sym
172             collection_value = options[collection_key]
173             case key
174               when :conditions
175                 collection_value, value = sanitize_sql(collection_value), sanitize_sql(value)
176                 options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value)
177               when :order
178                 options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value)
179               when :extend, :join_extend
180                 options[collection_key] = Array(collection_value) + Array(value)
181               else
182                 options[collection_key] ||= value
183             end     
184           end
185         end
186         
187         collection_option_keys
188       end
189       
190       
191       
192       public
193
194 =begin rdoc
195
196 This method createds a single-sided polymorphic relationship. 
197
198   class Petfood < ActiveRecord::Base
199     has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
200   end
201
202 The only required parameter, aside from the association name, is <tt>:from</tt>. 
203
204 The method generates a number of associations aside from the polymorphic one. In this example Petfood also gets <tt>dogs</tt>, <tt>cats</tt>, and <tt>birds</tt>, and Dog, Cat, and Bird get <tt>petfoods</tt>. (The reverse association to the parents is always plural.)
205
206 == Available options
207
208 <tt>:from</tt>:: An array of symbols representing the target models. Required.
209 <tt>:as</tt>:: A symbol for the parent's interface in the join--what the parent 'acts as'.
210 <tt>:through</tt>:: A symbol representing the class of the join model. Follows Rails defaults if not supplied (the parent and the association names, alphabetized, concatenated with an underscore, and singularized).
211 <tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, <tt>:delete_all</tt>. Controls how the join record gets treated on any associate delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
212 <tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
213 <tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with "_of_#{association_name}"</tt>. For example, <tt>zoos</tt> becomes <tt>zoos_of_animals</tt>. This is to help avoid method name collisions in crowded classes.
214 <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
215 <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
216 <tt>:parent_extend</tt>:: One or an array of mixed modules and procs, which are applied to the target models' association to the parents.
217 <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause. 
218 <tt>:parent_conditions</tt>:: An array or string of conditions which are applied to the target models' association to the parents.
219 <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
220 <tt>:parent_order</tt>:: A string for the SQL <tt>ORDER BY</tt> which is applied to the target models' association to the parents.
221 <tt>:group</tt>:: An array or string of conditions for the SQL <tt>GROUP BY</tt> clause. Affects the polymorphic and individual associations.
222 <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
223 <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
224 <tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
225 <tt>:uniq</tt>:: If <tt>true</tt>, the records returned are passed through a pure-Ruby <tt>uniq</tt> before they are returned. Rarely needed.
226 <tt>:foreign_key</tt>:: The column name for the parent's id in the join. 
227 <tt>:foreign_type_key</tt>:: The column name for the parent's class name in the join, if the parent itself is polymorphic. Rarely needed--if you're thinking about using this, you almost certainly want to use <tt>acts_as_double_polymorphic_join()</tt> instead.
228 <tt>:polymorphic_key</tt>:: The column name for the child's id in the join.
229 <tt>:polymorphic_type_key</tt>:: The column name for the child's class name in the join.
230
231 If you pass a block, it gets converted to a Proc and added to <tt>:extend</tt>. 
232
233 == On condition nullification
234
235 When you request an individual association, non-applicable but fully-qualified fields in the polymorphic association's <tt>:conditions</tt>, <tt>:order</tt>, and <tt>:group</tt> options get changed to <tt>NULL</tt>. For example, if you set <tt>:conditions => "dogs.name != 'Spot'"</tt>, when you request <tt>.cats</tt>, the conditions string is changed to <tt>NULL != 'Spot'</tt>. 
236
237 Be aware, however, that <tt>NULL != 'Spot'</tt> returns <tt>false</tt> due to SQL's 3-value logic. Instead, you need to use the <tt>:conditions</tt> string <tt>"dogs.name IS NULL OR dogs.name != 'Spot'"</tt> to get the behavior you probably expect for negative matches.
238
239 =end
240
241       def has_many_polymorphs (association_id, options = {}, &extension)
242         _logger_debug "associating #{self}.#{association_id}"
243         reflection = create_has_many_polymorphs_reflection(association_id, options, &extension)
244         # puts "Created reflection #{reflection.inspect}"
245         # configure_dependency_for_has_many(reflection)
246         collection_reader_method(reflection, PolymorphicAssociation)
247       end
248   
249       # Composed method that assigns option defaults,  builds the reflection object, and sets up all the related associations on the parent, join, and targets.
250       def create_has_many_polymorphs_reflection(association_id, options, &extension) #:nodoc:
251         options.assert_valid_keys(
252           :from,
253           :as,
254           :through,
255           :foreign_key,
256           :foreign_type_key,
257           :polymorphic_key, # same as :association_foreign_key
258           :polymorphic_type_key,
259           :dependent, # default :destroy, only affects the join table
260           :skip_duplicates, # default true, only affects the polymorphic collection
261           :ignore_duplicates, # deprecated
262           :is_double,
263           :rename_individual_collections,
264           :reverse_association_id, # not used
265           :singular_reverse_association_id,
266           :conflicts,
267           :extend,
268           :join_class_name,
269           :join_extend,
270           :parent_extend,
271           :table_aliases,
272           :select, # applies to the polymorphic relationship
273           :conditions, # applies to the polymorphic relationship, the children, and the join
274   #        :include,
275           :parent_conditions,
276           :parent_order,
277           :order, # applies to the polymorphic relationship, the children, and the join
278           :group, # only applies to the polymorphic relationship and the children
279           :limit, # only applies to the polymorphic relationship and the children
280           :offset, # only applies to the polymorphic relationship
281           :parent_order,
282           :parent_group,
283           :parent_limit,
284           :parent_offset,
285   #        :source,
286           :namespace,
287           :uniq, # XXX untested, only applies to the polymorphic relationship
288   #        :finder_sql,
289   #        :counter_sql,
290   #        :before_add,
291   #        :after_add,
292   #        :before_remove,
293   #        :after_remove
294            :dummy)
295   
296         # validate against the most frequent configuration mistakes
297         verify_pluralization_of(association_id)            
298         raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array            
299         options[:from].each{|plural| verify_pluralization_of(plural)}
300   
301         options[:as] ||= self.name.demodulize.underscore.to_sym
302         options[:conflicts] = Array(options[:conflicts])      
303         options[:foreign_key] ||= "#{options[:as]}_id"
304         
305         options[:association_foreign_key] = 
306           options[:polymorphic_key] ||= "#{association_id._singularize}_id"
307         options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"
308         
309         if options.has_key? :ignore_duplicates
310           _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
311           options[:skip_duplicates] = options[:ignore_duplicates]
312         end
313         options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
314         options[:dependent] = :destroy unless options.has_key? :dependent
315         options[:conditions] = sanitize_sql(options[:conditions])
316         
317         # options[:finder_sql] ||= "(options[:polymorphic_key]
318         
319         options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))
320         
321         # set up namespaces if we have a namespace key
322         # XXX needs test coverage
323         if options[:namespace]
324           namespace = options[:namespace].to_s.chomp("/") + "/"
325           options[:from].map! do |child|
326             "#{namespace}#{child}".to_sym
327           end
328           options[:through] = "#{namespace}#{options[:through]}".to_sym
329         end
330         
331         options[:join_class_name] ||= options[:through]._classify      
332         options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
333         options[:select] ||= build_select(association_id, options[:table_aliases]) 
334   
335         options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
336         options[:through] = demodulate(options[:through]).to_sym
337   
338         options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension)) 
339         options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join") 
340         options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent") 
341         
342         # create the reflection object      
343         returning(create_reflection(:has_many_polymorphs, association_id, options, self)) do |reflection|
344           if defined? Dependencies and defined? RAILS_ENV and RAILS_ENV == "development"                    
345             inject_dependencies(association_id, reflection) if Dependencies.mechanism == :load
346           end
347           
348           # set up the other related associations      
349           create_join_association(association_id, reflection)
350           create_has_many_through_associations_for_parent_to_children(association_id, reflection)
351           create_has_many_through_associations_for_children_to_parent(association_id, reflection)      
352         end             
353       end
354   
355       private
356   
357
358       # table mapping for use at the instantiation point      
359       
360       def build_table_aliases(from)
361         # for the targets
362         returning({}) do |aliases|
363           from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index|
364             begin
365               table = plural._as_class.table_name
366             rescue NameError => e
367               raise PolymorphicError, "Could not find a valid class for #{plural.inspect}. If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead."
368             end
369             plural._as_class.columns.map(&:name).each_with_index do |field, f_index|
370               aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}"
371             end
372           end
373         end
374       end
375   
376       def build_select(association_id, aliases)
377         # <tt>instantiate</tt> has to know which reflection the results are coming from
378         (["\'#{self.name}\' AS polymorphic_parent_class", 
379            "\'#{association_id}\' AS polymorphic_association_id"] + 
380         aliases.map do |table, _alias|
381           "#{table} AS #{_alias}"
382         end.sort).join(", ")
383       end
384   
385       # model caching         
386       def inject_dependencies(association_id, reflection)
387         _logger_debug "injecting dependencies"
388         requirements = [self, reflection.klass].map{|klass| [klass, klass.base_class]}.flatten.uniq
389         (all_classes_for(association_id, reflection) - requirements).each do |target_klass|
390           Dependencies.inject_dependency(target_klass, *requirements)        
391         end
392       end
393      
394       # method sub-builders
395    
396       def create_join_association(association_id, reflection)
397   
398         options = {
399           :foreign_key => reflection.options[:foreign_key], 
400           :dependent => reflection.options[:dependent], 
401           :class_name => reflection.klass.name, 
402           :extend => reflection.options[:join_extend]
403           # :limit => reflection.options[:limit],
404           # :offset => reflection.options[:offset],
405           # :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass, true),
406           # :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass, true)
407           }
408           
409         if reflection.options[:foreign_type_key]         
410           type_check = "#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}"
411           conjunction = options[:conditions] ? " AND " : nil
412           options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}"
413           options[:as] = reflection.options[:as]
414         end
415         
416         has_many(reflection.options[:through], options)
417         
418         inject_before_save_into_join_table(association_id, reflection)          
419       end
420       
421       def inject_before_save_into_join_table(association_id, reflection)
422         sti_hook = "sti_class_rewrite"
423         rewrite_procedure = %[self.send(:#{reflection.options[:polymorphic_type_key]}=, self.#{reflection.options[:polymorphic_type_key]}.constantize.base_class.name)]
424         
425         # XXX should be abstracted?
426         reflection.klass.class_eval %[          
427           unless instance_methods.include? "before_save_with_#{sti_hook}"
428             if instance_methods.include? "before_save"                             
429               alias_method :before_save_without_#{sti_hook}, :before_save 
430               def before_save_with_#{sti_hook}
431                 before_save_without_#{sti_hook}
432                 #{rewrite_procedure}
433               end
434             else
435               def before_save_with_#{sti_hook}
436                 #{rewrite_procedure}
437               end  
438             end
439             alias_method :before_save, :before_save_with_#{sti_hook}
440           end
441         ]      
442       end
443               
444       def create_has_many_through_associations_for_children_to_parent(association_id, reflection)
445         
446         child_pluralization_map(association_id, reflection).each do |plural, singular|
447           if singular == reflection.options[:as]
448             raise PolymorphicError, if reflection.options[:is_double]
449               "You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes."
450             else
451               "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model." 
452             end
453           end
454                   
455           parent = self
456           plural._as_class.instance_eval do          
457             # this shouldn't be called at all during doubles; there is no way to traverse to a double polymorphic parent (XXX is that right?)
458             unless reflection.options[:is_double] or reflection.options[:conflicts].include? self.name.tableize.to_sym  
459   
460               # the join table
461               through = "#{reflection.options[:through]}#{'_as_child' if parent == self}".to_sym
462               has_many(through,
463                 :as => association_id._singularize, 
464 #                :source => association_id._singularize, 
465 #                :source_type => reflection.options[:polymorphic_type_key], 
466                 :class_name => reflection.klass.name,
467                 :dependent => reflection.options[:dependent], 
468                 :extend => reflection.options[:join_extend],
469   #              :limit => reflection.options[:limit],
470   #              :offset => reflection.options[:offset],
471                 :order => devolve(association_id, reflection, reflection.options[:parent_order], reflection.klass),
472                 :conditions => devolve(association_id, reflection, reflection.options[:parent_conditions], reflection.klass)
473                 )
474   
475               # the association to the target's parents
476               association = "#{reflection.options[:as]._pluralize}#{"_of_#{association_id}" if reflection.options[:rename_individual_collections]}".to_sym                        
477               has_many(association, 
478                 :through => through, 
479                 :class_name => parent.name,
480                 :source => reflection.options[:as], 
481                 :foreign_key => reflection.options[:foreign_key],
482                 :extend => reflection.options[:parent_extend],
483                 :conditions => reflection.options[:parent_conditions],
484                 :order => reflection.options[:parent_order],
485                 :offset => reflection.options[:parent_offset],
486                 :limit => reflection.options[:parent_limit],
487                 :group => reflection.options[:parent_group])
488                 
489 #                debugger if association == :parents
490 #                
491 #                nil
492                       
493             end                    
494           end
495         end
496       end
497         
498       def create_has_many_through_associations_for_parent_to_children(association_id, reflection)
499         child_pluralization_map(association_id, reflection).each do |plural, singular|
500           #puts ":source => #{child}"
501           current_association = demodulate(child_association_map(association_id, reflection)[plural])
502           source = demodulate(singular)
503           
504           if reflection.options[:conflicts].include? plural
505             # XXX check this
506             current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym
507             source = "#{source}_as_#{association_id._singularize}".to_sym
508           end        
509             
510           # make push/delete accessible from the individual collections but still operate via the general collection
511           extension_module = self.class_eval %[
512             module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"}
513               def push *args; proxy_owner.send(:#{association_id}).send(:push, *args); self; end               
514               alias :<< :push
515               def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end
516               def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end
517               self
518             end]            
519                       
520           has_many(current_association.to_sym, 
521               :through => reflection.options[:through], 
522               :source => association_id._singularize,
523               :source_type => plural._as_class.base_class.name,
524               :class_name => plural._as_class.name, # make STI not conflate subtypes
525               :extend => (Array(extension_module) + reflection.options[:extend]),
526               :limit => reflection.options[:limit],
527   #        :offset => reflection.options[:offset],
528               :order => devolve(association_id, reflection, reflection.options[:order], plural._as_class),
529               :conditions => devolve(association_id, reflection, reflection.options[:conditions], plural._as_class),
530               :group => devolve(association_id, reflection, reflection.options[:group], plural._as_class)
531               )
532             
533         end
534       end
535   
536       # some support methods
537             
538       def child_pluralization_map(association_id, reflection)
539         Hash[*reflection.options[:from].map do |plural|
540           [plural,  plural._singularize]
541         end.flatten]
542       end
543       
544       def child_association_map(association_id, reflection)            
545         Hash[*reflection.options[:from].map do |plural|
546           [plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym]
547         end.flatten]
548       end         
549   
550       def demodulate(s)
551         s.to_s.gsub('/', '_').to_sym
552       end
553               
554       def build_join_table_symbol(association_id, name)
555         [name.to_s, association_id.to_s].sort.join("_").to_sym
556       end
557       
558       def all_classes_for(association_id, reflection)
559         klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)]
560         klasses += klasses.map(&:base_class)
561         klasses.uniq
562       end
563       
564       def devolve(association_id, reflection, string, klass, remove_inappropriate_clauses = false) 
565         # XXX remove_inappropriate_clauses is not implemented; we'll wait until someone actually needs it
566         return unless string
567         string = string.dup
568         # _logger_debug "devolving #{string} for #{klass}"
569         inappropriate_classes = (all_classes_for(association_id, reflection) - # the join class must always be preserved
570           [klass, klass.base_class, reflection.klass, reflection.klass.base_class])
571         inappropriate_classes.map do |klass|
572           klass.columns.map do |column| 
573             [klass.table_name, column.name]
574           end.map do |table, column|
575             ["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"]
576           end
577         end.flatten.sort_by(&:size).reverse.each do |quoted_reference|        
578           # _logger_debug "devolved #{quoted_reference} to NULL"
579           # XXX clause removal would go here 
580           string.gsub!(quoted_reference, "NULL")
581         end
582         # _logger_debug "altered to #{string}"
583         string
584       end
585       
586       def verify_pluralization_of(sym)
587         sym = sym.to_s
588         singular = sym.singularize
589         plural = singular.pluralize
590         raise PolymorphicError, "Pluralization rules not set up correctly. You passed :#{sym}, which singularizes to :#{singular}, but that pluralizes to :#{plural}, which is different. Maybe you meant :#{plural} to begin with?" unless sym == plural
591       end      
592     
593       def spiked_create_extension_module(association_id, extensions, identifier = nil)    
594         module_extensions = extensions.select{|e| e.is_a? Module}
595         proc_extensions = extensions.select{|e| e.is_a? Proc }
596         
597         # support namespaced anonymous blocks as well as multiple procs
598         proc_extensions.each_with_index do |proc_extension, index|
599           module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}"
600           the_module = self.class_eval "module #{module_name}; self; end" # XXX hrm
601           the_module.class_eval &proc_extension
602           module_extensions << the_module
603         end
604         module_extensions
605       end
606                 
607     end
608   end
609 end