Make scopes use relations under the hood

This commit is contained in:
Pratik Naik 2010-01-14 13:36:33 +05:30
parent 1c30ec23fe
commit bed9179aa1
13 changed files with 366 additions and 335 deletions

@ -1703,24 +1703,30 @@ def select_all_rows(options, join_dependency)
end
def construct_finder_arel_with_included_associations(options, join_dependency)
scope = scope(:find)
relation = active_relation
for association in join_dependency.join_associations
relation = association.join_relation(relation)
end
relation = relation.joins(construct_join(options[:joins], scope)).
relation = relation.joins(options[:joins]).
select(column_aliases(join_dependency)).
group(options[:group] || (scope && scope[:group])).
having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
where(construct_conditions(options[:conditions], scope)).
from((scope && scope[:from]) || options[:from])
group(options[:group]).
having(options[:having]).
order(options[:order]).
where(options[:conditions]).
from(options[:from])
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
scoped_relation = current_scoped_methods
scoped_relation_limit = scoped_relation.taken if scoped_relation
relation = current_scoped_methods.except(:limit).merge(relation) if current_scoped_methods
if !using_limitable_reflections?(join_dependency.reflections) && ((scoped_relation && scoped_relation.taken) || options[:limit])
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency))
end
relation = relation.limit(options[:limit] || scoped_relation_limit) if using_limitable_reflections?(join_dependency.reflections)
relation
end
@ -1748,23 +1754,23 @@ def select_limited_ids_array(options, join_dependency)
end
def construct_finder_sql_for_association_limiting(options, join_dependency)
scope = scope(:find)
relation = active_relation
for association in join_dependency.join_associations
relation = association.join_relation(relation)
end
relation = relation.joins(construct_join(options[:joins], scope)).
where(construct_conditions(options[:conditions], scope)).
group(options[:group] || (scope && scope[:group])).
having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_limit(options[:offset], scope)).
from(options[:from]).
select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", construct_order(options[:order], scope(:find)).join(",")))
relation = relation.joins(options[:joins]).
where(options[:conditions]).
group(options[:group]).
having(options[:having]).
order(options[:order]).
limit(options[:limit]).
offset(options[:offset]).
from(options[:from])
relation = current_scoped_methods.except(:select, :includes, :eager_load).merge(relation) if current_scoped_methods
relation = relation.select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order]))
relation.to_sql
end
@ -2030,118 +2036,85 @@ def initialize(reflection, join_dependency, parent = nil)
def association_join
return @join if @join
connection = reflection.active_record.connection
aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => active_relation_engine)
parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => active_relation_engine)
@join = case reflection.macro
when :has_and_belongs_to_many
["%s.%s = %s.%s " % [
connection.quote_table_name(aliased_join_table_name),
options[:foreign_key] || reflection.active_record.to_s.foreign_key,
connection.quote_table_name(parent.aliased_table_name),
reflection.active_record.primary_key],
"%s.%s = %s.%s " % [
connection.quote_table_name(aliased_table_name),
klass.primary_key,
connection.quote_table_name(aliased_join_table_name),
options[:association_foreign_key] || klass.to_s.foreign_key
]
]
when :has_many, :has_one
if reflection.options[:through]
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
first_key = second_key = as_extra = nil
when :has_and_belongs_to_many
join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => active_relation_engine)
fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key
if through_reflection.options[:as] # has_many :through against a polymorphic join
jt_foreign_key = through_reflection.options[:as].to_s + '_id'
jt_as_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(through_reflection.options[:as].to_s + '_type'),
klass.quote_value(parent.active_record.base_class.name)
]
else
jt_foreign_key = through_reflection.primary_key_name
end
[
join_table[fk].eq(parent_table[reflection.active_record.primary_key]),
aliased_table[klass.primary_key].eq(join_table[klass_fk])
]
when :has_many, :has_one
if reflection.options[:through]
join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => active_relation_engine)
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
first_key = second_key = as_extra = nil
case source_reflection.macro
when :has_many
if source_reflection.options[:as]
first_key = "#{source_reflection.options[:as]}_id"
second_key = options[:foreign_key] || primary_key
as_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_table_name),
connection.quote_column_name("#{source_reflection.options[:as]}_type"),
klass.quote_value(source_reflection.active_record.base_class.name)
]
else
first_key = through_reflection.klass.base_class.to_s.foreign_key
second_key = options[:foreign_key] || primary_key
end
unless through_reflection.klass.descends_from_active_record?
jt_sti_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(through_reflection.active_record.inheritance_column),
through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
end
when :belongs_to
first_key = primary_key
if reflection.options[:source_type]
second_key = source_reflection.association_foreign_key
jt_source_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(reflection.source_reflection.options[:foreign_type]),
klass.quote_value(reflection.options[:source_type])
]
else
second_key = source_reflection.primary_key_name
end
end
["(%s.%s = %s.%s%s%s%s) " % [
connection.quote_table_name(parent.aliased_table_name),
connection.quote_column_name(parent.primary_key),
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(jt_foreign_key),
jt_as_extra, jt_source_extra, jt_sti_extra],
"(%s.%s = %s.%s%s) " % [
connection.quote_table_name(aliased_table_name),
connection.quote_column_name(first_key),
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(second_key),
as_extra]
]
elsif reflection.options[:as]
"%s.%s = %s.%s AND %s.%s = %s" % [
connection.quote_table_name(aliased_table_name),
"#{reflection.options[:as]}_id",
connection.quote_table_name(parent.aliased_table_name),
parent.primary_key,
connection.quote_table_name(aliased_table_name),
"#{reflection.options[:as]}_type",
klass.quote_value(parent.active_record.base_class.name)
]
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
"%s.%s = %s.%s " % [
aliased_table_name,
foreign_key,
parent.aliased_table_name,
reflection.options[:primary_key] || parent.primary_key
]
if through_reflection.options[:as] # has_many :through against a polymorphic join
jt_foreign_key = through_reflection.options[:as].to_s + '_id'
jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].eq(parent.active_record.base_class.name)
else
jt_foreign_key = through_reflection.primary_key_name
end
when :belongs_to
"%s.%s = %s.%s " % [
connection.quote_table_name(aliased_table_name),
reflection.klass.primary_key,
connection.quote_table_name(parent.aliased_table_name),
options[:foreign_key] || reflection.primary_key_name
case source_reflection.macro
when :has_many
if source_reflection.options[:as]
first_key = "#{source_reflection.options[:as]}_id"
second_key = options[:foreign_key] || primary_key
as_extra = aliased_table["#{source_reflection.options[:as]}_type"].eq(source_reflection.active_record.base_class.name)
else
first_key = through_reflection.klass.base_class.to_s.foreign_key
second_key = options[:foreign_key] || primary_key
end
unless through_reflection.klass.descends_from_active_record?
jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name)
end
when :belongs_to
first_key = primary_key
if reflection.options[:source_type]
second_key = source_reflection.association_foreign_key
jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type])
else
second_key = source_reflection.primary_key_name
end
end
[
[parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].reject{|x| x.blank? },
aliased_table[first_key].eq(join_table[second_key])
]
elsif reflection.options[:as]
id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key])
type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name)
[id_rel, type_rel]
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
[aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])]
end
when :belongs_to
[aliased_table[reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])]
end
unless klass.descends_from_active_record?
sti_column = aliased_table[klass.inheritance_column]
sti_condition = sti_column.eq(klass.sti_name)
klass.send(:subclasses).each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
@join << sti_condition
end
@join << %(AND %s) % [
klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref|
@join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))} " if ref && ref.options[:conditions]
if ref && ref.options[:conditions]
@join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))
end
end
@join

@ -58,11 +58,14 @@ def find(*args)
find_scope = construct_scope[:find].slice(:conditions, :order)
with_scope(:find => find_scope) do
relation = @reflection.klass.send(:construct_finder_arel, options)
relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods))
case args.first
when :first, :last, :all
when :first, :last
relation.send(args.first)
when :all
records = relation.all
@reflection.options[:uniq] ? uniq(records) : records
else
relation.find(*args)
end
@ -402,7 +405,7 @@ def method_missing(method, *args)
end
elsif @reflection.klass.scopes.include?(method)
@reflection.klass.scopes[method].call(self, *args)
else
else
with_scope(construct_scope) do
if block_given?
@reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }

@ -51,8 +51,6 @@ def target_reflection_has_associated_record?
end
def construct_find_options!(options)
options[:select] = construct_select(options[:select])
options[:from] ||= construct_from
options[:joins] = construct_joins(options[:joins])
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
end

@ -6,8 +6,7 @@ module ThroughAssociationScope
def construct_scope
{ :create => construct_owner_attributes(@reflection),
:find => { :from => construct_from,
:conditions => construct_conditions,
:find => { :conditions => construct_conditions,
:joins => construct_joins,
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
:select => construct_select,

@ -644,7 +644,7 @@ def find(*args)
options = args.extract_options!
set_readonly_option!(options)
relation = construct_finder_arel(options)
relation = construct_finder_arel(options, current_scoped_methods)
case args.first
when :first, :last, :all
@ -870,20 +870,21 @@ def destroy(id)
# # Update all books that match our conditions, but limit it to 5 ordered by date
# Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
def update_all(updates, conditions = nil, options = {})
scope = scope(:find)
relation = active_relation
if conditions = construct_conditions(conditions, scope)
if conditions = construct_conditions(conditions, nil)
relation = relation.where(Arel::SqlLiteral.new(conditions))
end
relation = if options.has_key?(:limit) || (scope && scope[:limit])
relation = relation.limit(options[:limit]) if options[:limit].present?
relation = relation.order(options[:order]) if options[:order].present?
if current_scoped_methods && current_scoped_methods.limit_value.present? && current_scoped_methods.order_values.present?
# Only take order from scope if limit is also provided by scope, this
# is useful for updating a has_many association with a limit.
relation.order(construct_order(options[:order], scope)).limit(construct_limit(options[:limit], scope))
relation = current_scoped_methods.merge(relation) if current_scoped_methods
else
relation.order(options[:order])
relation = current_scoped_methods.except(:limit, :order).merge(relation) if current_scoped_methods
end
relation.update(sanitize_sql_for_assignment(updates))
@ -1572,26 +1573,26 @@ def default_select(qualified)
end
end
def construct_finder_arel(options = {}, scope = scope(:find))
def construct_finder_arel(options = {}, scope = nil)
validate_find_options(options)
relation = active_relation.
joins(construct_join(options[:joins], scope)).
where(construct_conditions(options[:conditions], scope)).
select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))).
group(options[:group] || (scope && scope[:group])).
having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_offset(options[:offset], scope)).
joins(options[:joins]).
where(options[:conditions]).
select(options[:select]).
group(options[:group]).
having(options[:having]).
order(options[:order]).
limit(options[:limit]).
offset(options[:offset]).
from(options[:from]).
includes( merge_includes(scope && scope[:include], options[:include]))
includes(options[:include])
lock = (scope && scope[:lock]) || options[:lock]
relation = relation.lock if lock.present?
relation = relation.readonly if options[:readonly]
relation = relation.where(type_condition) if finder_needs_type_condition?
relation = relation.lock(options[:lock]) if options[:lock].present?
relation = relation.readonly(options[:readonly]) if options.has_key?(:readonly)
relation = scope.merge(relation) if scope
relation
end
@ -1665,10 +1666,10 @@ def build_association_joins(joins)
relation = active_relation.table
join_dependency.join_associations.map { |association|
if (association_relation = association.relation).is_a?(Array)
[Arel::InnerJoin.new(relation, association_relation.first, association.association_join.first).joins(relation),
Arel::InnerJoin.new(relation, association_relation.last, association.association_join.last).joins(relation)].join()
[Arel::InnerJoin.new(relation, association_relation.first, *association.association_join.first).joins(relation),
Arel::InnerJoin.new(relation, association_relation.last, *association.association_join.last).joins(relation)].join()
else
Arel::InnerJoin.new(relation, association_relation, association.association_join).joins(relation)
Arel::InnerJoin.new(relation, association_relation, *association.association_join).joins(relation)
end
}.join(" ")
end
@ -1713,7 +1714,7 @@ def method_missing(method_id, *arguments, &block)
super unless all_attributes_exists?(attribute_names)
if match.finder?
options = arguments.extract_options!
relation = options.any? ? construct_finder_arel(options) : scoped
relation = options.any? ? construct_finder_arel(options, current_scoped_methods) : scoped
relation.send :find_by_attributes, match, attribute_names, *arguments
elsif match.instantiator?
scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
@ -1830,53 +1831,50 @@ def attribute_condition(quoted_column_name, argument)
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
if key == :conditions && 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
if method_scoping.is_a?(Hash)
# 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
relation = construct_finder_arel(method_scoping[:find] || {})
if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create]
scope_for_create = case action
when :merge
current_scoped_methods.create_with_value.merge(method_scoping[:create])
when :reverse_merge
method_scoping[:create].merge(current_scoped_methods.create_with_value)
else
method_scoping[:create]
end
relation = relation.create_with(scope_for_create)
else
scope_for_create = method_scoping[:create]
scope_for_create ||= current_scoped_methods.create_with_value if current_scoped_methods
relation = relation.create_with(scope_for_create) if scope_for_create
end
method_scoping = relation
end
if current_scoped_methods
case action
when :merge
method_scoping = current_scoped_methods.merge(method_scoping)
when :reverse_merge
method_scoping = current_scoped_methods.except(:where).merge(method_scoping)
method_scoping = method_scoping.merge(current_scoped_methods.only(:where))
end
end
self.scoped_methods << method_scoping
@ -1904,20 +1902,22 @@ def subclasses #:nodoc:
# default_scope :order => 'last_name, first_name'
# end
def default_scope(options = {})
self.default_scoping << { :find => options, :create => options[:conditions].is_a?(Hash) ? options[:conditions] : {} }
self.default_scoping << construct_finder_arel(options)
end
# Test whether the given method and optional key are scoped.
def scoped?(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
!key || !scope[key].nil?
case method
when :create
current_scoped_methods.send(:scope_for_create).present? if current_scoped_methods
end
end
# Retrieve the scope for the given method and optional key.
def scope(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
key ? scope[key] : scope
case method
when :create
current_scoped_methods.send(:scope_for_create) if current_scoped_methods
end
end

@ -46,19 +46,19 @@ module ClassMethods
def count(*args)
case args.size
when 0
construct_calculation_arel.count
construct_calculation_arel({}, current_scoped_methods).count
when 1
if args[0].is_a?(Hash)
options = args[0]
distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
construct_calculation_arel(options).count(options[:select], :distinct => distinct)
construct_calculation_arel(options, current_scoped_methods).count(options[:select], :distinct => distinct)
else
construct_calculation_arel.count(args[0])
construct_calculation_arel({}, current_scoped_methods).count(args[0])
end
when 2
column_name, options = args
distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
construct_calculation_arel(options).count(column_name, :distinct => distinct)
construct_calculation_arel(options, current_scoped_methods).count(column_name, :distinct => distinct)
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
end
@ -141,7 +141,7 @@ def sum(column_name, options = {})
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct))
construct_calculation_arel(options, current_scoped_methods).calculate(operation, column_name, options.slice(:distinct))
rescue ThrowResult
0
end
@ -151,49 +151,74 @@ def validate_calculation_options(options = {})
options.assert_valid_keys(CALCULATIONS_OPTIONS)
end
def construct_calculation_arel(options = {})
def construct_calculation_arel(options = {}, merge_with_relation = nil)
validate_calculation_options(options)
options = options.except(:distinct)
scope = scope(:find)
includes = merge_includes(scope ? scope[:include] : [], options[:include])
includes = merge_includes(merge_with_relation ? merge_with_relation.includes_values : [], options[:include])
if includes.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope))
construct_calculation_arel_with_included_associations(options, join_dependency)
merge_with_joins = merge_with_relation ? merge_with_relation.joins_values : []
joins = (merge_with_joins + Array.wrap(options[:joins])).uniq
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(joins, nil))
construct_calculation_arel_with_included_associations(options, join_dependency, merge_with_relation)
else
active_relation.
joins(construct_join(options[:joins], scope)).
from((scope && scope[:from]) || options[:from]).
where(construct_conditions(options[:conditions], scope)).
relation = active_relation.
joins(options[:joins]).
where(options[:conditions]).
order(options[:order]).
limit(options[:limit]).
offset(options[:offset]).
group(options[:group]).
having(options[:having]).
select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins])))
having(options[:having])
if merge_with_relation
relation = merge_with_relation.except(:select, :order, :limit, :offset, :group, :from).merge(relation)
else
relation = relation.where(type_condition) if finder_needs_type_condition?
end
from = merge_with_relation.from_value if merge_with_relation && merge_with_relation.from_value.present?
from = options[:from] if from.blank? && options[:from].present?
relation = relation.from(from)
select = options[:select].presence || (merge_with_relation ? merge_with_relation.select_values.join(", ") : nil)
relation = relation.select(select)
relation
end
end
def construct_calculation_arel_with_included_associations(options, join_dependency)
scope = scope(:find)
def construct_calculation_arel_with_included_associations(options, join_dependency, merge_with_relation = nil)
relation = active_relation
for association in join_dependency.join_associations
relation = association.join_relation(relation)
end
relation = relation.joins(construct_join(options[:joins], scope)).
if merge_with_relation
relation.joins_values = (merge_with_relation.joins_values + relation.joins_values).uniq
relation.where_values = merge_with_relation.where_values
merge_limit = merge_with_relation.taken
else
relation = relation.where(type_condition) if finder_needs_type_condition?
end
relation = relation.joins(options[:joins]).
select(column_aliases(join_dependency)).
group(options[:group]).
having(options[:having]).
order(options[:order]).
where(construct_conditions(options[:conditions], scope)).
from((scope && scope[:from]) || options[:from])
where(options[:conditions]).
from(options[:from])
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
if !using_limitable_reflections?(join_dependency.reflections) && (merge_limit || options[:limit])
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency))
end
relation = relation.limit(options[:limit] || merge_limit) if using_limitable_reflections?(join_dependency.reflections)
relation
end

@ -26,10 +26,12 @@ def scoped(options = {}, &block)
if options.present?
Scope.new(self, options, &block)
else
unless scoped?(:find)
current_scope = current_scoped_methods
unless current_scope
finder_needs_type_condition? ? active_relation.where(type_condition) : active_relation.spawn
else
construct_finder_arel
construct_finder_arel({}, current_scoped_methods)
end
end
end

@ -47,17 +47,20 @@ def to_a
@records = if find_with_associations
begin
@klass.send(:find_with_associations, {
:select => arel.send(:select_clauses).join(', '),
options = {
:select => @select_values.any? ? @select_values.join(", ") : nil,
:joins => arel.joins(arel),
:group => arel.send(:group_clauses).join(', '),
:group => @group_values.any? ? @group_values.join(", ") : nil,
:order => order_clause,
:conditions => where_clause,
:limit => arel.taken,
:offset => arel.skipped,
:from => (arel.send(:from_clauses) if arel.send(:sources).present?)
},
ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_values + @includes_values, nil))
}
including = (@eager_load_values + @includes_values).uniq
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
@klass.send(:find_with_associations, options, join_dependency)
rescue ThrowResult
[]
end
@ -161,7 +164,7 @@ def method_missing(method, *args, &block)
end
def with_create_scope
@klass.send(:with_scope, :create => scope_for_create) { yield }
@klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield }
end
def scope_for_create

@ -40,7 +40,7 @@ def calculate(operation, column_name, options = {})
distinct = options[:distinct] || distinct
column_name = :all if column_name.blank? && operation == "count"
if arel.send(:groupings).any?
if @group_values.any?
return execute_grouped_calculation(operation, column_name)
else
return execute_simple_calculation(operation, column_name, distinct)
@ -63,7 +63,7 @@ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
end
def execute_grouped_calculation(operation, column_name) #:nodoc:
group_attr = arel.send(:groupings).first.value
group_attr = @group_values.first
association = @klass.reflect_on_association(group_attr.to_sym)
associated = association && association.macro == :belongs_to # only count belongs_to associations
group_field = associated ? association.primary_key_name : group_attr
@ -106,7 +106,6 @@ def construct_count_options_from_args(*args)
column_name = :all
# Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true)
# TODO : relation.projections only works when .select() was last in the chain. Fix it!
case args.size
when 0
select = get_projection_name_from_chained_relations

@ -10,8 +10,20 @@ module QueryMethods
def #{query_method}(*args)
spawn.tap do |new_relation|
new_relation.#{query_method}_values ||= []
value = args.size > 1 ? [args] : Array.wrap(args)
new_relation.#{query_method}_values += value
value = Array.wrap(args.flatten).reject {|x| x.blank? }
new_relation.#{query_method}_values += value if value.present?
end
end
CEVAL
end
[:where, :having].each do |query_method|
class_eval <<-CEVAL
def #{query_method}(*args)
spawn.tap do |new_relation|
new_relation.#{query_method}_values ||= []
value = build_where(*args)
new_relation.#{query_method}_values += [*value] if value.present?
end
end
CEVAL
@ -58,51 +70,83 @@ def arel
def build_arel
arel = table
@joins_values.each do |j|
next if j.blank?
joined_associations = []
association_joins = []
joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
# Build association joins first
joins.each do |join|
association_joins << join if [Hash, Array, Symbol].include?(join.class) && !@klass.send(:array_of_strings?, join)
end
if association_joins.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil)
to_join = []
join_dependency.join_associations.each do |association|
if (association_relation = association.relation).is_a?(Array)
to_join << [association_relation.first, association.association_join.first]
to_join << [association_relation.last, association.association_join.last]
else
to_join << [association_relation, association.association_join]
end
end
to_join.each do |tj|
unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] }
joined_associations << tj
arel = arel.join(tj[0]).on(*tj[1])
end
end
end
joins.each do |join|
next if join.blank?
@implicit_readonly = true
case j
case join
when Relation::JoinOperation
arel = arel.join(j.relation, j.join_class).on(j.on)
arel = arel.join(join.relation, join.join_class).on(*join.on)
when Hash, Array, Symbol
if @klass.send(:array_of_strings?, j)
arel = arel.join(j.join(' '))
else
arel = arel.join(@klass.send(:build_association_joins, j))
if @klass.send(:array_of_strings?, join)
join_string = join.join(' ')
arel = arel.join(join_string)
end
else
arel = arel.join(j)
arel = arel.join(join)
end
end
@where_values.each do |where|
if conditions = build_where(where)
arel = conditions.is_a?(String) ? arel.where(conditions) : arel.where(*conditions)
end
@where_values.uniq.each do |w|
arel = w.is_a?(String) ? arel.where(w) : arel.where(*w)
end
@having_values.each do |where|
if conditions = build_where(where)
arel = conditions.is_a?(String) ? arel.having(conditions) : arel.having(*conditions)
end
@having_values.uniq.each do |h|
arel = h.is_a?(String) ? arel.having(h) : arel.having(*h)
end
arel = arel.take(@limit_value) if @limit_value.present?
arel = arel.skip(@offset_value) if @offset_value.present?
@group_values.each do |g|
@group_values.uniq.each do |g|
arel = arel.group(g) if g.present?
end
@order_values.each do |o|
@order_values.uniq.each do |o|
arel = arel.order(o) if o.present?
end
@select_values.each do |s|
@implicit_readonly = false
arel = arel.project(s) if s.present?
selects = @select_values.uniq
if selects.present?
selects.each do |s|
@implicit_readonly = false
arel = arel.project(s) if s.present?
end
elsif joins.present?
arel = arel.project(@klass.quoted_table_name + '.*')
end
arel = arel.from(@from_value) if @from_value.present?
@ -120,7 +164,7 @@ def build_arel
def build_where(*args)
return if args.blank?
builder = PredicateBuilder.new(Arel::Sql::Engine.new(@klass))
builder = PredicateBuilder.new(table.engine)
conditions = if [String, Array].include?(args.first.class)
merged = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first)

@ -19,21 +19,19 @@ def merge(r)
merged_relation = spawn.eager_load(r.eager_load_values).preload(r.preload_values).includes(r.includes_values)
merged_relation.readonly_value = r.readonly_value unless merged_relation.readonly_value
merged_relation.limit_value = r.limit_value unless merged_relation.limit_value
merged_relation.readonly_value = r.readonly_value unless r.readonly_value.nil?
merged_relation.limit_value = r.limit_value if r.limit_value.present?
merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
merged_relation.offset_value = r.offset_value if r.offset_value.present?
merged_relation = merged_relation.
joins(r.joins_values).
group(r.group_values).
offset(r.offset_value).
select(r.select_values).
from(r.from_value).
having(r.having_values)
relation_order = r.order_values
merged_order = relation_order.present? ? relation_order : order_values
merged_relation.order_values = merged_order
merged_relation.order_values = Array.wrap(order_values) + Array.wrap(r.order_values)
merged_relation.create_with_value = @create_with_value
@ -50,7 +48,7 @@ def merge(r)
merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name }
end
merged_wheres << w
merged_wheres += [w]
end
merged_relation.where_values = merged_wheres
@ -74,5 +72,21 @@ def except(*skips)
result
end
def only(*onlies)
result = Relation.new(@klass, table)
onlies.each do |only|
if (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).include?(only)
result.send(:"#{only}_values=", send(:"#{only}_values"))
elsif Relation::SINGLE_VALUE_METHODS.include?(only)
result.send(:"#{only}_value=", send(:"#{only}_value"))
else
raise "Invalid argument : #{only}"
end
end
result
end
end
end

@ -8,39 +8,11 @@
class InnerJoinAssociationTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations
def test_construct_finder_sql_creates_inner_joins
sql = Author.joins(:posts).to_sql
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
end
def test_construct_finder_sql_cascades_inner_joins
sql = Author.joins(:posts => :comments).to_sql
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = posts.id/, sql
end
def test_construct_finder_sql_inner_joins_through_associations
sql = Author.joins(:categorized_posts).to_sql
assert_match /INNER JOIN .?categorizations.?.*INNER JOIN .?posts.?/, sql
end
def test_construct_finder_sql_applies_association_conditions
sql = Author.joins(:categories_like_general).where("TERMINATING_MARKER").to_sql
assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?(.|\n)*TERMINATING_MARKER/, sql
end
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
result = Author.joins(:thinking_posts, :welcome_posts).to_a
assert_equal authors(:david), result.first
end
def test_construct_finder_sql_unpacks_nested_joins
sql = Author.joins(:posts => [[:comments]]).to_sql
assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = .?posts.?.id/, sql
end
def test_construct_finder_sql_ignores_empty_joins_hash
sql = Author.joins({}).to_sql
assert_no_match /JOIN/i, sql

@ -11,7 +11,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_set_conditions
Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do
assert_equal 'just a test...', Developer.send(:current_scoped_methods)[:find][:conditions]
assert_equal '(just a test...)', Developer.scoped.send(:where_clause)
end
end
@ -207,7 +207,7 @@ def test_scoped_create
new_comment = nil
VerySpecialComment.send(:with_scope, :create => { :post_id => 1 }) do
assert_equal({ :post_id => 1 }, VerySpecialComment.send(:current_scoped_methods)[:create])
assert_equal({:post_id => 1}, VerySpecialComment.scoped.send(:scope_for_create))
new_comment = VerySpecialComment.create :body => "Wonderful world"
end
@ -256,8 +256,9 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merge_options
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
Developer.send(:with_scope, :find => { :limit => 10 }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :conditions => 'salary = 80000', :limit => 10 }, merged_option)
devs = Developer.scoped
assert_equal '(salary = 80000)', devs.send(:where_clause)
assert_equal 10, devs.taken
end
end
end
@ -265,26 +266,26 @@ def test_merge_options
def test_merge_inner_scope_has_priority
Developer.send(:with_scope, :find => { :limit => 5 }) do
Developer.send(:with_scope, :find => { :limit => 10 }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :limit => 10 }, merged_option)
assert_equal 10, Developer.scoped.taken
end
end
end
def test_replace_options
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Jamis'" }) do
assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.instance_eval('current_scoped_methods'))
assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.send(:scoped_methods)[-1])
Developer.send(:with_scope, :find => { :conditions => {:name => 'David'} }) do
Developer.send(:with_exclusive_scope, :find => { :conditions => {:name => 'Jamis'} }) do
assert_equal 'Jamis', Developer.scoped.send(:scope_for_create)[:name]
end
assert_equal 'David', Developer.scoped.send(:scope_for_create)[:name]
end
end
def test_append_conditions
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
appended_condition = Developer.instance_eval('current_scoped_methods')[:find][:conditions]
assert_equal("(name = 'David') AND (salary = 80000)", appended_condition)
devs = Developer.scoped
assert_equal "(name = 'David') AND (salary = 80000)", devs.send(:where_clause)
assert_equal(1, Developer.count)
end
Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do
@ -296,8 +297,9 @@ def test_append_conditions
def test_merge_and_append_options
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :conditions => "(salary = 80000) AND (name = 'David')", :limit => 10 }, merged_option)
devs = Developer.scoped
assert_equal "(salary = 80000) AND (name = 'David')", devs.send(:where_clause)
assert_equal 10, devs.taken
end
end
end
@ -325,15 +327,15 @@ def test_nested_scoped_find_merged_include
# :include's remain unique and don't "double up" when merging
Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal('David', Developer.find(:first).name)
assert_equal 1, Developer.scoped.includes_values.uniq.length
assert_equal 'David', Developer.find(:first).name
end
end
# the nested scope doesn't remove the first :include
Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
Developer.send(:with_scope, :find => { :include => [] }) do
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal 1, Developer.scoped.includes_values.uniq.length
assert_equal('David', Developer.find(:first).name)
end
end
@ -341,7 +343,7 @@ def test_nested_scoped_find_merged_include
# mixing array and symbol include's will merge correctly
Developer.send(:with_scope, :find => { :include => [:projects], :conditions => "projects.id = 2" }) do
Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal 1, Developer.scoped.includes_values.uniq.length
assert_equal('David', Developer.find(:first).name)
end
end
@ -350,7 +352,7 @@ def test_nested_scoped_find_merged_include
def test_nested_scoped_find_replace_include
Developer.send(:with_scope, :find => { :include => :projects }) do
Developer.send(:with_exclusive_scope, :find => { :include => [] }) do
assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal 0, Developer.scoped.includes_values.length
end
end
end
@ -416,7 +418,7 @@ def test_nested_scoped_create
comment = nil
Comment.send(:with_scope, :create => { :post_id => 1}) do
Comment.send(:with_scope, :create => { :post_id => 2}) do
assert_equal({ :post_id => 2 }, Comment.send(:current_scoped_methods)[:create])
assert_equal({:post_id => 2}, Comment.scoped.send(:scope_for_create))
comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
end
end
@ -425,9 +427,11 @@ def test_nested_scoped_create
def test_nested_exclusive_scope_for_create
comment = nil
Comment.send(:with_scope, :create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
Comment.send(:with_exclusive_scope, :create => { :post_id => 1 }) do
assert_equal({ :post_id => 1 }, Comment.send(:current_scoped_methods)[:create])
assert_equal({:post_id => 1}, Comment.scoped.send(:scope_for_create))
assert Comment.new.body.blank?
comment = Comment.create :body => "Hey guys"
end
end
@ -603,44 +607,39 @@ def test_default_scope_with_conditions_hash
end
def test_default_scoping_with_threads
scope = [{ :create => {}, :find => { :order => 'salary DESC' } }]
2.times do
Thread.new { assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods) }.join
Thread.new { assert_equal 'salary DESC', DeveloperOrderedBySalary.scoped.send(:order_clause) }.join
end
end
def test_default_scoping_with_inheritance
scope = [{ :create => {}, :find => { :order => 'salary DESC' } }]
# Inherit a class having a default scope and define a new default scope
klass = Class.new(DeveloperOrderedBySalary)
klass.send :default_scope, {}
# Scopes added on children should append to parent scope
expected_klass_scope = [{ :create => {}, :find => { :order => 'salary DESC' }}, { :create => {}, :find => {} }]
assert_equal expected_klass_scope, klass.send(:scoped_methods)
assert klass.scoped.send(:order_clause).blank?
# Parent should still have the original scope
assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods)
assert_equal 'salary DESC', DeveloperOrderedBySalary.scoped.send(:order_clause)
end
def test_method_scope
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
expected = Developer.find(:all, :order => 'name DESC, salary DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary }
assert_equal expected, received
end
def test_nested_scope
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
expected = Developer.find(:all, :order => 'name DESC, salary DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
end
assert_equal expected, received
end
def test_named_scope_overwrites_default
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.name }
def test_named_scope_order_appended_to_default_scope_order
expected = Developer.find(:all, :order => 'name DESC, salary DESC').collect { |dev| dev.name }
received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name }
assert_equal expected, received
end