Skip to content

Instantly share code, notes, and snippets.

@Bertg
Created January 10, 2014 13:18
Show Gist options
  • Save Bertg/8351770 to your computer and use it in GitHub Desktop.
Save Bertg/8351770 to your computer and use it in GitHub Desktop.
This version of spawn methods removes the dangerous behaviour where rails 3.2 (after 3.2.13) clobbers conditions.
raise "Remove patch after rails 3.2" if Rails.version =~ /\A4/
# copy of lib/active_record/relation/spawn_methods.rb
# Because updating to this version 3.2.16 might change the behaviour of some of our queries,
# we took a copy of this class and remove the behaviour where
module ActiveRecord
module SpawnMethods
def merge(r)
return self unless r
return to_a & r if r.is_a?(Array)
merged_relation = clone
r = r.with_default_scope if r.default_scoped? && r.klass != klass
Relation::ASSOCIATION_METHODS.each do |method|
value = r.send(:"#{method}_values")
unless value.empty?
if method == :includes
merged_relation = merged_relation.includes(value)
else
merge_relation_method(merged_relation, method, value)
end
end
end
(Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method|
value = r.send(:"#{method}_values")
merge_relation_method(merged_relation, method, value) if value.present?
end
merge_joins(merged_relation, r)
merged_wheres = @where_values + r.where_values
# -- PATCH
# unless @where_values.empty?
# # Remove duplicate ARel attributes. Last one wins.
# seen = Hash.new { |h,table| h[table] = Hash.new { |i,name| i[name] = [] } }
# merged_wheres = merged_wheres.reverse.reject { |w|
# nuke = false
# if w.respond_to?(:operator) && w.operator == :== &&
# w.left.respond_to?(:relation)
# name = w.left.name
# table = w.left.relation.name
# nuke = !seen[table][name].empty?
# seen[table][name] << w.right
# end
# nuke
# }.reverse
# end
# -- END PATCH
merged_relation.where_values = merged_wheres
(Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method|
value = r.send(:"#{method}_value")
merged_relation.send(:"#{method}_value=", value) unless value.nil?
end
merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty?
if (r.reordering_value)
# override any order specified in the original relation
merged_relation.reordering_value = true
merged_relation.order_values = r.order_values
else
# merge in order_values from r
merged_relation.order_values += r.order_values
end
# Apply scope extension modules
merged_relation.send :apply_modules, r.extensions
merged_relation
end
# Removes from the query the condition(s) specified in +skips+.
#
# Example:
#
# Post.order('id asc').except(:order) # discards the order condition
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
#
def except(*skips)
result = self.class.new(@klass, table)
result.default_scoped = default_scoped
((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method|
result.send(:"#{method}_values=", send(:"#{method}_values"))
end
(Relation::SINGLE_VALUE_METHODS - skips).each do |method|
result.send(:"#{method}_value=", send(:"#{method}_value"))
end
# Apply scope extension modules
result.send(:apply_modules, extensions)
result
end
# Removes any condition from the query other than the one(s) specified in +onlies+.
#
# Example:
#
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
#
def only(*onlies)
result = self.class.new(@klass, table)
result.default_scoped = default_scoped
((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method|
result.send(:"#{method}_values=", send(:"#{method}_values"))
end
(Relation::SINGLE_VALUE_METHODS & onlies).each do |method|
result.send(:"#{method}_value=", send(:"#{method}_value"))
end
# Apply scope extension modules
result.send(:apply_modules, extensions)
result
end
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend,
:order, :select, :readonly, :group, :having, :from, :lock ]
def apply_finder_options(options)
relation = clone
return relation unless options
options.assert_valid_keys(VALID_FIND_OPTIONS)
finders = options.dup
finders.delete_if { |key, value| value.nil? && key != :limit }
([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder|
relation = relation.send(finder, finders[finder])
end
relation = relation.where(finders[:conditions]) if options.has_key?(:conditions)
relation = relation.includes(finders[:include]) if options.has_key?(:include)
relation = relation.extending(finders[:extend]) if options.has_key?(:extend)
relation
end
private
def merge_joins(relation, other)
values = other.joins_values
return if values.blank?
if other.klass >= relation.klass
relation.joins_values += values
else
joins_dependency, rest = values.partition do |join|
case join
when Hash, Symbol, Array
true
else
false
end
end
join_dependency = ActiveRecord::Associations::JoinDependency.new(
other.klass,
joins_dependency,
[]
)
relation.joins_values += join_dependency.join_associations + rest
end
end
def merge_relation_method(relation, method, value)
relation.send(:"#{method}_values=", relation.send(:"#{method}_values") + value)
end
end
end
@shyam-habarakada
Copy link

bert, have you shipped this patch and found it to work as expected? Want to pick it up, but wondering how stable and tested it is :-) Thanks for putting it together...

@shyam-habarakada
Copy link

Here's the same patch, updated for rails 3.2.17 and remove already defined constants warnings

raise "Remove patch after rails 3.2" if Rails.version =~ /\A4/

# copy of lib/active_record/relation/spawn_methods.rb
# Because updating to this version 3.2.16 might change the behaviour of some of our queries,
# we took a copy of this class and remove the behaviour where 
module ActiveRecord
  module SpawnMethods
    def merge(r)
      return self unless r
      return to_a & r if r.is_a?(Array)

      merged_relation = clone

      r = r.with_default_scope if r.default_scoped? && r.klass != klass

      Relation::ASSOCIATION_METHODS.each do |method|
        value = r.send(:"#{method}_values")

        unless value.empty?
          if method == :includes
            merged_relation = merged_relation.includes(value)
          else
            merge_relation_method(merged_relation, method, value)
          end
        end
      end

      (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method|
        value = r.send(:"#{method}_values")
        merge_relation_method(merged_relation, method, value) if value.present?
      end

      merge_joins(merged_relation, r)

      merged_wheres = @where_values + r.where_values

      # -- PATCH
      # unless @where_values.empty?
      #   # Remove duplicate ARel attributes. Last one wins.
      #   seen = Hash.new { |h,table| h[table] = {} }
      #   merged_wheres = merged_wheres.reverse.reject { |w|
      #     nuke = false
      #     if w.respond_to?(:operator) && w.operator == :== &&
      #       w.left.respond_to?(:relation)
      #       name              = w.left.name
      #       table             = w.left.relation.name
      #       nuke              = seen[table][name]
      #       seen[table][name] = true
      #     end
      #     nuke
      #   }.reverse
      # end
      # -- END PATCH

      merged_relation.where_values = merged_wheres

      (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method|
        value = r.send(:"#{method}_value")
        merged_relation.send(:"#{method}_value=", value) unless value.nil?
      end

      merged_relation.lock_value = r.lock_value unless merged_relation.lock_value

      merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty?

      if (r.reordering_value)
        # override any order specified in the original relation
        merged_relation.reordering_value = true
        merged_relation.order_values = r.order_values
      else
        # merge in order_values from r
        merged_relation.order_values += r.order_values
      end

      # Apply scope extension modules
      merged_relation.send :apply_modules, r.extensions

      merged_relation
    end

    # Removes from the query the condition(s) specified in +skips+.
    #
    # Example:
    #
    #   Post.order('id asc').except(:order)                  # discards the order condition
    #   Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
    #
    def except(*skips)
      result = self.class.new(@klass, table)
      result.default_scoped = default_scoped

      ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method|
        result.send(:"#{method}_values=", send(:"#{method}_values"))
      end

      (Relation::SINGLE_VALUE_METHODS - skips).each do |method|
        result.send(:"#{method}_value=", send(:"#{method}_value"))
      end

      # Apply scope extension modules
      result.send(:apply_modules, extensions)

      result
    end

    # Removes any condition from the query other than the one(s) specified in +onlies+.
    #
    # Example:
    #
    #   Post.order('id asc').only(:where)         # discards the order condition
    #   Post.order('id asc').only(:where, :order) # uses the specified order
    #
    def only(*onlies)
      result = self.class.new(@klass, table)
      result.default_scoped = default_scoped

      ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method|
        result.send(:"#{method}_values=", send(:"#{method}_values"))
      end

      (Relation::SINGLE_VALUE_METHODS & onlies).each do |method|
        result.send(:"#{method}_value=", send(:"#{method}_value"))
      end

      # Apply scope extension modules
      result.send(:apply_modules, extensions)

      result
    end

    # -- PATCH
    # VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend,
    #                        :order, :select, :readonly, :group, :having, :from, :lock ]
    # -- END PATCH

    def apply_finder_options(options)
      relation = clone
      return relation unless options

      options.assert_valid_keys(VALID_FIND_OPTIONS)
      finders = options.dup
      finders.delete_if { |key, value| value.nil? && key != :limit }

      ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder|
        relation = relation.send(finder, finders[finder])
      end

      relation = relation.where(finders[:conditions]) if options.has_key?(:conditions)
      relation = relation.includes(finders[:include]) if options.has_key?(:include)
      relation = relation.extending(finders[:extend]) if options.has_key?(:extend)

      relation
    end

    private

      def merge_joins(relation, other)
        values = other.joins_values
        return if values.blank?

        if other.klass >= relation.klass
          relation.joins_values += values
        else
          joins_dependency, rest = values.partition do |join|
            case join
            when Hash, Symbol, Array
              true
            else
              false
            end
          end

          join_dependency = ActiveRecord::Associations::JoinDependency.new(
            other.klass,
            joins_dependency,
            []
          )

          relation.joins_values += join_dependency.join_associations + rest
        end
      end

      def merge_relation_method(relation, method, value)
        relation.send(:"#{method}_values=", relation.send(:"#{method}_values") + value)
      end
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment