1 module CompositePrimaryKeys
4 def self.append_features(base)
6 base.send(:extend, ClassMethods)
9 # Composite key versions of Association functions
12 def construct_counter_sql_with_included_associations(options, join_dependency)
14 sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
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)}"
21 sql << " FROM #{quoted_table_name} "
22 sql << join_dependency.join_associations.collect{|join| join.association_join }.join
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])
28 add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
30 if !self.connection.supports_count_distinct?
34 return sanitize_sql(sql)
37 def construct_finder_sql_with_included_associations(options, join_dependency)
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
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]
46 sql << "ORDER BY #{options[:order]} " if options[:order]
48 add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
50 return sanitize_sql(sql)
53 def table_columns(columns)
54 columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"}
57 def quoted_table_columns(columns)
58 table_columns(columns).join(ID_SEP)
67 module ActiveRecord::Associations::ClassMethods
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)
75 join_aliased_primary_keys = join.active_record.composite? ?
76 join.aliased_primary_key : [join.aliased_primary_key]
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)
88 raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
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"
101 active_record.composite? ?
102 aliased_primary_key.map {|key| row[key]}.to_composite_ids :
103 row[aliased_primary_key]
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 }"]
114 return @column_names_with_alias
118 class JoinAssociation < JoinBase
119 alias single_association_join association_join
121 reflection.active_record.composite? ? composite_association_join : single_association_join
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)
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)
141 when :has_many, :has_one
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"
148 if source_reflection.macro == :has_many && source_reflection.options[:as]
149 raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
151 case source_reflection.macro
153 first_key = primary_key
154 second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
156 first_key = through_reflection.klass.to_s.classify.foreign_key
157 second_key = options[:foreign_key] || primary_key
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)
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)
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"
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)),
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)),
200 join << %(AND %s.%s = %s ) % [
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]
208 def full_keys(table_name, keys)
209 connection = reflection.active_record.connection
210 quoted_table_name = connection.quote_table_name(table_name)
212 keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
214 "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
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|
230 module ActiveRecord::Associations
231 class AssociationProxy #:nodoc:
233 def composite_where_clause(full_keys, ids)
234 full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
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]
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)}"
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)
256 where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
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)
268 quoted1 = connection.quote_table_name(table1)
269 quoted2 = connection.quote_table_name(table2)
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)}"
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)
283 keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
285 "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
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}"
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 ')
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
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?
312 class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
314 @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
316 if @reflection.options[:finder_sql]
317 @finder_sql = @reflection.options[:finder_sql]
319 @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
320 @finder_sql << " AND (#{conditions})" if conditions
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)
328 class HasManyAssociation < AssociationCollection #:nodoc:
331 when @reflection.options[:finder_sql]
332 @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
334 when @reflection.options[:as]
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
341 @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
342 @finder_sql << " AND (#{conditions})" if conditions
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])
352 @counter_sql = @finder_sql
356 def delete_records(records)
357 if @reflection.options[:dependent]
358 records.each { |r| r.destroy }
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"}
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]})"
370 where_clause = where_clause_terms.join(" AND ")
372 where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' + r.quoted_id
375 @reflection.klass.update_all( field_names.join(',') , where_clause)
381 class HasOneAssociation < BelongsToAssociation #:nodoc:
384 when @reflection.options[:as]
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)}"
389 @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
392 @finder_sql << " AND (#{conditions})" if conditions
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
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
406 alias_method_chain :construct_conditions, :composite_keys
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)
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
416 reflection_primary_key = @reflection.source_reflection.primary_key_name
417 source_primary_key = @reflection.klass.primary_key
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))
426 alias_method_chain :construct_joins, :composite_keys