X-Git-Url: https://git.openstreetmap.org/rails.git/blobdiff_plain/0a57413d3e5d3734f2f3d83df00abd861d80aee2..d74e2196a04a8e0303cd1056a620f4d439be787f:/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb new file mode 100644 index 000000000..4ea4a7b02 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb @@ -0,0 +1,428 @@ +module CompositePrimaryKeys + module ActiveRecord + module Associations + def self.append_features(base) + super + base.send(:extend, ClassMethods) + end + + # Composite key versions of Association functions + module ClassMethods + + def construct_counter_sql_with_included_associations(options, join_dependency) + scope = scope(:find) + sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})" + + # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. + if !self.connection.supports_count_distinct? + sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}" + end + + sql << " FROM #{quoted_table_name} " + sql << join_dependency.join_associations.collect{|join| join.association_join }.join + + add_joins!(sql, options, scope) + add_conditions!(sql, options[:conditions], scope) + add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + + add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections) + + if !self.connection.supports_count_distinct? + sql << ")" + end + + return sanitize_sql(sql) + end + + def construct_finder_sql_with_included_associations(options, join_dependency) + scope = scope(:find) + sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " + sql << join_dependency.join_associations.collect{|join| join.association_join }.join + + add_joins!(sql, options, scope) + add_conditions!(sql, options[:conditions], scope) + add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit] + + sql << "ORDER BY #{options[:order]} " if options[:order] + + add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections) + + return sanitize_sql(sql) + end + + def table_columns(columns) + columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"} + end + + def quoted_table_columns(columns) + table_columns(columns).join(ID_SEP) + end + + end + + end + end +end + +module ActiveRecord::Associations::ClassMethods + class JoinDependency + def construct_association(record, join, row) + case join.reflection.macro + when :has_many, :has_and_belongs_to_many + collection = record.send(join.reflection.name) + collection.loaded + + join_aliased_primary_keys = join.active_record.composite? ? + join.aliased_primary_key : [join.aliased_primary_key] + return nil if + record.id.to_s != join.parent.record_id(row).to_s or not + join_aliased_primary_keys.select {|key| row[key].nil?}.blank? + association = join.instantiate(row) + collection.target.push(association) unless collection.target.include?(association) + when :has_one, :belongs_to + return if record.id.to_s != join.parent.record_id(row).to_s or + [*join.aliased_primary_key].any? { |key| row[key].nil? } + association = join.instantiate(row) + record.send("set_#{join.reflection.name}_target", association) + else + raise ConfigurationError, "unknown macro: #{join.reflection.macro}" + end + return association + end + + class JoinBase + def aliased_primary_key + active_record.composite? ? + primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} : + "#{ aliased_prefix }_r0" + end + + def record_id(row) + active_record.composite? ? + aliased_primary_key.map {|key| row[key]}.to_composite_ids : + row[aliased_primary_key] + end + + def column_names_with_alias + unless @column_names_with_alias + @column_names_with_alias = [] + keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key] + (keys + (column_names - keys)).each_with_index do |column_name, i| + @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"] + end + end + return @column_names_with_alias + end + end + + class JoinAssociation < JoinBase + alias single_association_join association_join + def association_join + reflection.active_record.composite? ? composite_association_join : single_association_join + end + + def composite_association_join + join = case reflection.macro + when :has_and_belongs_to_many + " LEFT OUTER JOIN %s ON %s " % [ + table_alias_for(options[:join_table], aliased_join_table_name), + composite_join_clause( + full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key), + full_keys(reflection.active_record.table_name, reflection.active_record.primary_key) + ) + ] + + " LEFT OUTER JOIN %s ON %s " % [ + table_name_and_alias, + composite_join_clause( + full_keys(aliased_table_name, klass.primary_key), + full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key) + ) + ] + when :has_many, :has_one + case + when reflection.macro == :has_many && reflection.options[:through] + through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : '' + if through_reflection.options[:as] # has_many :through against a polymorphic join + raise AssociationNotSupported, "Polymorphic joins not supported for composite keys" + else + if source_reflection.macro == :has_many && source_reflection.options[:as] + raise AssociationNotSupported, "Polymorphic joins not supported for composite keys" + else + case source_reflection.macro + when :belongs_to + first_key = primary_key + second_key = options[:foreign_key] || klass.to_s.classify.foreign_key + when :has_many + first_key = through_reflection.klass.to_s.classify.foreign_key + second_key = options[:foreign_key] || primary_key + end + + " LEFT OUTER JOIN %s ON %s " % [ + table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), + composite_join_clause( + full_keys(aliased_join_table_name, through_reflection.primary_key_name), + full_keys(parent.aliased_table_name, parent.primary_key) + ) + ] + + " LEFT OUTER JOIN %s ON %s " % [ + table_name_and_alias, + composite_join_clause( + full_keys(aliased_table_name, first_key), + full_keys(aliased_join_table_name, second_key) + ) + ] + end + end + + when reflection.macro == :has_many && reflection.options[:as] + raise AssociationNotSupported, "Polymorphic joins not supported for composite keys" + when reflection.macro == :has_one && reflection.options[:as] + raise AssociationNotSupported, "Polymorphic joins not supported for composite keys" + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + " LEFT OUTER JOIN %s ON %s " % [ + table_name_and_alias, + composite_join_clause( + full_keys(aliased_table_name, foreign_key), + full_keys(parent.aliased_table_name, parent.primary_key)), + ] + end + when :belongs_to + " LEFT OUTER JOIN %s ON %s " % [ + table_name_and_alias, + composite_join_clause( + full_keys(aliased_table_name, reflection.klass.primary_key), + full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)), + ] + else + "" + end || '' + join << %(AND %s.%s = %s ) % [ + aliased_table_name, + reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column), + klass.connection.quote(klass.name)] unless klass.descends_from_active_record? + join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions] + join + end + + def full_keys(table_name, keys) + connection = reflection.active_record.connection + quoted_table_name = connection.quote_table_name(table_name) + if keys.is_a?(Array) + keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) + else + "#{quoted_table_name}.#{connection.quote_column_name(keys)}" + end + end + + def composite_join_clause(full_keys1, full_keys2) + full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String) + full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String) + where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2| + "#{key1}=#{key2}" + end.join(" AND ") + "(#{where_clause})" + end + end + end +end + +module ActiveRecord::Associations + class AssociationProxy #:nodoc: + + def composite_where_clause(full_keys, ids) + full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String) + + if ids.is_a?(String) + ids = [[ids]] + elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1 + ids = [ids.to_composite_ids] + end + + where_clause = ids.map do |id_set| + transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose + transposed.map do |full_key, id| + "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}" + end.join(" AND ") + end.join(") OR (") + + "(#{where_clause})" + end + + def composite_join_clause(full_keys1, full_keys2) + full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String) + full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String) + + where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2| + "#{key1}=#{key2}" + end.join(" AND ") + + "(#{where_clause})" + end + + def full_composite_join_clause(table1, full_keys1, table2, full_keys2) + connection = @reflection.active_record.connection + full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String) + full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String) + + quoted1 = connection.quote_table_name(table1) + quoted2 = connection.quote_table_name(table2) + + where_clause = [full_keys1, full_keys2].transpose.map do |key_pair| + "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}" + end.join(" AND ") + + "(#{where_clause})" + end + + def full_keys(table_name, keys) + connection = @reflection.active_record.connection + quoted_table_name = connection.quote_table_name(table_name) + keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String) + if keys.is_a?(Array) + keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) + else + "#{quoted_table_name}.#{connection.quote_column_name(keys)}" + end + end + + def full_columns_equals(table_name, keys, quoted_ids) + connection = @reflection.active_record.connection + quoted_table_name = connection.quote_table_name(table_name) + if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP)) + return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}" + end + keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String) + quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String) + keys_ids = [keys, quoted_ids].transpose + keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ') + end + + def set_belongs_to_association_for(record) + if @reflection.options[:as] + record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record? + record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s + else + key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten) + key_values.each{|key, value| record[key] = value} unless @owner.new_record? + end + end + end + + class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: + def construct_sql + @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql]) + + if @reflection.options[:finder_sql] + @finder_sql = @reflection.options[:finder_sql] + else + @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id) + @finder_sql << " AND (#{conditions})" if conditions + end + + @join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " + + full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key) + end + end + + class HasManyAssociation < AssociationCollection #:nodoc: + def construct_sql + case + when @reflection.options[:finder_sql] + @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) + + when @reflection.options[:as] + @finder_sql = + "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + @finder_sql << " AND (#{conditions})" if conditions + + else + @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id) + @finder_sql << " AND (#{conditions})" if conditions + end + + if @reflection.options[:counter_sql] + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + elsif @reflection.options[:finder_sql] + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + else + @counter_sql = @finder_sql + end + end + + def delete_records(records) + if @reflection.options[:dependent] + records.each { |r| r.destroy } + else + connection = @reflection.active_record.connection + field_names = @reflection.primary_key_name.split(',') + field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"} + records.each do |r| + where_clause = nil + + if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP) + where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair| + "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" + end + where_clause = where_clause_terms.join(" AND ") + else + where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' + r.quoted_id + end + + @reflection.klass.update_all( field_names.join(',') , where_clause) + end + end + end + end + + class HasOneAssociation < BelongsToAssociation #:nodoc: + def construct_sql + case + when @reflection.options[:as] + @finder_sql = + "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id) + end + + @finder_sql << " AND (#{conditions})" if conditions + end + end + + class HasManyThroughAssociation < HasManyAssociation #:nodoc: + def construct_conditions_with_composite_keys + if @reflection.through_reflection.options[:as] + construct_conditions_without_composite_keys + else + conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id) + conditions << " AND (#{sql_conditions})" if sql_conditions + conditions + end + end + alias_method_chain :construct_conditions, :composite_keys + + def construct_joins_with_composite_keys(custom_joins = nil) + if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as] + construct_joins_without_composite_keys(custom_joins) + else + if @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.klass.primary_key + source_primary_key = @reflection.source_reflection.primary_key_name + else + reflection_primary_key = @reflection.source_reflection.primary_key_name + source_primary_key = @reflection.klass.primary_key + end + + "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [ + @reflection.through_reflection.quoted_table_name, + composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key)) + ] + end + end + alias_method_chain :construct_joins, :composite_keys + end +end