]> git.openstreetmap.org Git - rails.git/blob - vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb
Merge 14394:14533 from trunk.
[rails.git] / vendor / gems / composite_primary_keys-1.1.0 / lib / composite_primary_keys / associations.rb
1 module CompositePrimaryKeys
2   module ActiveRecord
3     module Associations
4       def self.append_features(base)
5         super
6         base.send(:extend, ClassMethods)
7       end
8
9       # Composite key versions of Association functions
10       module ClassMethods
11
12         def construct_counter_sql_with_included_associations(options, join_dependency)
13           scope = scope(:find)
14           sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
15
16           # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
17           if !self.connection.supports_count_distinct?
18             sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
19           end
20           
21           sql << " FROM #{quoted_table_name} "
22           sql << join_dependency.join_associations.collect{|join| join.association_join }.join
23
24           add_joins!(sql, options, scope)
25           add_conditions!(sql, options[:conditions], scope)
26           add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
27
28           add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
29
30           if !self.connection.supports_count_distinct?
31             sql << ")"
32           end
33
34           return sanitize_sql(sql)
35         end
36
37         def construct_finder_sql_with_included_associations(options, join_dependency)
38           scope = scope(:find)
39           sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
40           sql << join_dependency.join_associations.collect{|join| join.association_join }.join
41
42           add_joins!(sql, options, scope)
43           add_conditions!(sql, options[:conditions], scope)
44           add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
45
46           sql << "ORDER BY #{options[:order]} " if options[:order]
47
48           add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
49
50           return sanitize_sql(sql)
51         end
52
53         def table_columns(columns)
54           columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"}
55         end
56
57         def quoted_table_columns(columns)
58           table_columns(columns).join(ID_SEP)
59         end
60
61       end
62
63     end
64   end
65 end
66
67 module ActiveRecord::Associations::ClassMethods
68   class JoinDependency
69     def construct_association(record, join, row)
70       case join.reflection.macro
71         when :has_many, :has_and_belongs_to_many
72           collection = record.send(join.reflection.name)
73           collection.loaded
74
75           join_aliased_primary_keys = join.active_record.composite? ?
76             join.aliased_primary_key : [join.aliased_primary_key]
77           return nil if
78             record.id.to_s != join.parent.record_id(row).to_s or not
79             join_aliased_primary_keys.select {|key| row[key].nil?}.blank?
80           association = join.instantiate(row)
81           collection.target.push(association) unless collection.target.include?(association)
82         when :has_one, :belongs_to
83           return if record.id.to_s != join.parent.record_id(row).to_s or
84                     [*join.aliased_primary_key].any? { |key| row[key].nil? }
85           association = join.instantiate(row)
86           record.send("set_#{join.reflection.name}_target", association)
87         else
88           raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
89       end
90       return association
91     end
92
93     class JoinBase
94       def aliased_primary_key
95         active_record.composite? ?
96           primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
97           "#{ aliased_prefix }_r0"
98       end
99
100       def record_id(row)
101         active_record.composite? ?
102           aliased_primary_key.map {|key| row[key]}.to_composite_ids :
103           row[aliased_primary_key]
104       end
105
106       def column_names_with_alias
107         unless @column_names_with_alias
108           @column_names_with_alias = []
109           keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
110           (keys + (column_names - keys)).each_with_index do |column_name, i|
111             @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
112           end
113         end
114         return @column_names_with_alias
115       end
116     end
117
118     class JoinAssociation < JoinBase
119       alias single_association_join association_join
120       def association_join
121         reflection.active_record.composite? ? composite_association_join : single_association_join
122       end
123
124       def composite_association_join
125         join = case reflection.macro
126           when :has_and_belongs_to_many
127             " LEFT OUTER JOIN %s ON %s " % [
128               table_alias_for(options[:join_table], aliased_join_table_name),
129                 composite_join_clause(
130                   full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
131                   full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
132                 )
133               ] +
134              " LEFT OUTER JOIN %s ON %s " % [
135                 table_name_and_alias,
136                 composite_join_clause(
137                   full_keys(aliased_table_name, klass.primary_key),
138                   full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
139                 )
140               ]
141           when :has_many, :has_one
142             case
143               when reflection.macro == :has_many && reflection.options[:through]
144                 through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
145                 if through_reflection.options[:as] # has_many :through against a polymorphic join
146                   raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
147                 else
148                   if source_reflection.macro == :has_many && source_reflection.options[:as]
149                     raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
150                   else
151                     case source_reflection.macro
152                       when :belongs_to
153                         first_key  = primary_key
154                         second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
155                       when :has_many
156                         first_key  = through_reflection.klass.to_s.classify.foreign_key
157                         second_key = options[:foreign_key] || primary_key
158                     end
159
160                     " LEFT OUTER JOIN %s ON %s "  % [
161                       table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
162                        composite_join_clause(
163                          full_keys(aliased_join_table_name, through_reflection.primary_key_name),
164                          full_keys(parent.aliased_table_name, parent.primary_key)
165                        )
166                      ] +
167                      " LEFT OUTER JOIN %s ON %s " % [
168                        table_name_and_alias,
169                        composite_join_clause(
170                          full_keys(aliased_table_name, first_key),
171                          full_keys(aliased_join_table_name, second_key)
172                        )
173                     ]
174                   end
175                 end
176
177               when reflection.macro == :has_many && reflection.options[:as]
178                 raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
179               when reflection.macro == :has_one && reflection.options[:as]
180                 raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
181               else
182                 foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
183                 " LEFT OUTER JOIN %s ON %s " % [
184                   table_name_and_alias,
185                   composite_join_clause(
186                     full_keys(aliased_table_name, foreign_key),
187                     full_keys(parent.aliased_table_name, parent.primary_key)),
188                 ]
189             end
190           when :belongs_to
191             " LEFT OUTER JOIN %s ON %s " % [
192                table_name_and_alias,
193                composite_join_clause(
194                  full_keys(aliased_table_name, reflection.klass.primary_key),
195                  full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
196               ]
197           else
198             ""
199         end || ''
200         join << %(AND %s.%s = %s ) % [
201           aliased_table_name,
202           reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
203           klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
204         join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
205         join
206       end
207
208       def full_keys(table_name, keys)
209         connection = reflection.active_record.connection
210         quoted_table_name = connection.quote_table_name(table_name)
211         if keys.is_a?(Array) 
212           keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) 
213         else
214           "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
215         end
216       end
217
218       def composite_join_clause(full_keys1, full_keys2)
219         full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
220         full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
221         where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
222           "#{key1}=#{key2}"
223         end.join(" AND ")
224         "(#{where_clause})"
225       end
226     end
227   end
228 end
229
230 module ActiveRecord::Associations
231   class AssociationProxy #:nodoc:
232
233     def composite_where_clause(full_keys, ids)
234       full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
235
236       if ids.is_a?(String)
237         ids = [[ids]]
238       elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
239         ids = [ids.to_composite_ids]
240       end
241
242       where_clause = ids.map do |id_set|
243         transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
244         transposed.map do |full_key, id|
245           "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
246         end.join(" AND ")
247       end.join(") OR (")
248
249       "(#{where_clause})"
250     end
251
252     def composite_join_clause(full_keys1, full_keys2)
253       full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
254       full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
255
256       where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
257         "#{key1}=#{key2}"
258       end.join(" AND ")
259
260       "(#{where_clause})"
261     end
262
263     def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
264       connection = @reflection.active_record.connection
265       full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
266       full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
267
268       quoted1 = connection.quote_table_name(table1)
269       quoted2 = connection.quote_table_name(table2)
270       
271       where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
272         "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
273       end.join(" AND ")
274
275       "(#{where_clause})"
276     end
277
278     def full_keys(table_name, keys)
279       connection = @reflection.active_record.connection
280       quoted_table_name = connection.quote_table_name(table_name)
281       keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
282       if keys.is_a?(Array) 
283         keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) 
284       else
285         "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
286       end
287     end
288
289     def full_columns_equals(table_name, keys, quoted_ids)
290       connection = @reflection.active_record.connection
291       quoted_table_name = connection.quote_table_name(table_name)
292       if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
293         return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}"
294       end
295       keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
296       quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
297       keys_ids = [keys, quoted_ids].transpose
298       keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ')
299     end 
300
301     def set_belongs_to_association_for(record)
302       if @reflection.options[:as]
303         record["#{@reflection.options[:as]}_id"]   = @owner.id unless @owner.new_record?
304         record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
305       else
306         key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
307         key_values.each{|key, value| record[key] = value} unless @owner.new_record?
308       end
309     end
310   end
311
312   class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
313     def construct_sql
314       @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
315
316       if @reflection.options[:finder_sql]
317         @finder_sql = @reflection.options[:finder_sql]
318       else
319         @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
320         @finder_sql << " AND (#{conditions})" if conditions
321       end
322
323       @join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " +
324       full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
325     end
326   end
327
328   class HasManyAssociation < AssociationCollection #:nodoc:
329     def construct_sql
330       case
331         when @reflection.options[:finder_sql]
332           @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
333
334         when @reflection.options[:as]
335           @finder_sql = 
336             "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + 
337             "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
338           @finder_sql << " AND (#{conditions})" if conditions
339
340         else
341           @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
342           @finder_sql << " AND (#{conditions})" if conditions
343       end
344
345       if @reflection.options[:counter_sql]
346         @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
347       elsif @reflection.options[:finder_sql]
348         # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
349         @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
350         @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
351       else
352         @counter_sql = @finder_sql
353       end
354     end
355
356     def delete_records(records)
357       if @reflection.options[:dependent]
358         records.each { |r| r.destroy }
359       else
360         connection = @reflection.active_record.connection
361         field_names = @reflection.primary_key_name.split(',')
362         field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"}
363         records.each do |r|
364           where_clause = nil
365           
366           if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP)
367             where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair|
368               "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
369             end
370             where_clause = where_clause_terms.join(" AND ")
371           else
372             where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' +  r.quoted_id
373           end
374           
375           @reflection.klass.update_all(  field_names.join(',') , where_clause)
376         end
377       end
378     end
379   end
380
381   class HasOneAssociation < BelongsToAssociation #:nodoc:
382     def construct_sql
383       case
384         when @reflection.options[:as]
385           @finder_sql = 
386             "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + 
387             "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
388         else
389           @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
390       end
391
392       @finder_sql << " AND (#{conditions})" if conditions
393     end
394   end
395
396   class HasManyThroughAssociation < HasManyAssociation #:nodoc:
397     def construct_conditions_with_composite_keys
398       if @reflection.through_reflection.options[:as]
399         construct_conditions_without_composite_keys
400       else
401         conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
402         conditions << " AND (#{sql_conditions})" if sql_conditions
403         conditions
404       end
405     end
406     alias_method_chain :construct_conditions, :composite_keys
407
408     def construct_joins_with_composite_keys(custom_joins = nil)
409       if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
410         construct_joins_without_composite_keys(custom_joins)
411       else
412         if @reflection.source_reflection.macro == :belongs_to
413           reflection_primary_key = @reflection.klass.primary_key
414           source_primary_key     = @reflection.source_reflection.primary_key_name
415         else
416           reflection_primary_key = @reflection.source_reflection.primary_key_name
417           source_primary_key     = @reflection.klass.primary_key
418         end
419
420         "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
421           @reflection.through_reflection.quoted_table_name,
422           composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
423         ]
424       end
425     end
426     alias_method_chain :construct_joins, :composite_keys
427   end
428 end