Skip to content

Instantly share code, notes, and snippets.

@woahdae
Created July 1, 2010 23:40
Show Gist options
  • Save woahdae/460725 to your computer and use it in GitHub Desktop.
Save woahdae/460725 to your computer and use it in GitHub Desktop.
# If I have two named scopes with a :having clause, ex:
#
# class Product < ActiveRecord::Base
# named_scope :qty_ordered_gt, lambda { |number|
# { :group => "products.id",
# :joins => :line_items,
# :having => ["SUM(line_items.qty) > ?", number]}
# }
# named_scope :revenue_gt, lambda { |number|
# { :group => "products.id",
# :joins => :line_items,
# :having => ["SUM(line_items.price) > ?", number]}
# }
# end
#
# (ps, made these up, don't know why you'd want this particular scope combo,
# but that's beside the point)
#
# Seems like the combined scope should produce:
#
# Product.qty_ordered_gt(5).revenue_gt(10).current_scoped_methods[:find][:having]
# # => ["SUM(line_items.qty) > ? AND SUM(line_items.price) > ?", 5, 10]
#
# Instead, it would produce:
#
# # => ["SUM(line_items.price) > ?", 10]
#
# The documentation for ActiveRecord::Base#with_scope specifies this behavior,
# noting that it will only combine :conditions, :include, and :joins. There's
# gotta be a reason why :having isn't included on that list, but personally
# I don't see it. :limit, :offset, and :order don't make sense to combine,
# but I have a real-world case of needing to combine :having.
#
# This is a one-line change, copied straight out of ActiveRecord::Base.
# Obviously this doesn't come with tests, but I went red-green in
# cucumber and rspec in the project this was needed for. Since then I've
# also applied the patch to rails' 2-3-stable branch and all the mysql
# and sqlite3 tests pass.
#
# I'll update it if I come across anything, and if someone else agrees with
# me (or if I can't sleep at night after copy-pasting this much code out of AR)
# I'll submit a patch to Rails.
class ActiveRecord::Base
# Copied straight out of ActiveRecord::Base, sadly.
class << self
def with_scope(method_scoping = {}, action = :merge, &block)
method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)
# Dup first and second level of hash (method and params).
method_scoping = method_scoping.inject({}) do |hash, (method, params)|
hash[method] = (params == true) ? params : params.dup
hash
end
method_scoping.assert_valid_keys([ :find, :create ])
if f = method_scoping[:find]
f.assert_valid_keys(VALID_FIND_OPTIONS)
set_readonly_option! f
end
# Merge scopings
if [:merge, :reverse_merge].include?(action) && current_scoped_methods
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
case hash[method]
when Hash
if method == :find
(hash[method].keys + params.keys).uniq.each do |key|
merge = hash[method][key] && params[key] # merge if both scopes have the same key
################## Added key == :having case ####################
if (key == :conditions || key == :having) && merge
if params[key].is_a?(Hash) && hash[method][key].is_a?(Hash)
hash[method][key] = merge_conditions(hash[method][key].deep_merge(params[key]))
else
hash[method][key] = merge_conditions(params[key], hash[method][key])
end
elsif key == :include && merge
hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
elsif key == :joins && merge
hash[method][key] = merge_joins(params[key], hash[method][key])
else
hash[method][key] = hash[method][key] || params[key]
end
end
else
if action == :reverse_merge
hash[method] = hash[method].merge(params)
else
hash[method] = params.merge(hash[method])
end
end
else
hash[method] = params
end
hash
end
end
self.scoped_methods << method_scoping
begin
yield
ensure
self.scoped_methods.pop
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment