Permalink
Browse files

Make scopes use relations under the hood

  • Loading branch information...
1 parent 1c30ec2 commit bed9179aa1496f6d28891cf515af0d7e515ebbab @lifo lifo committed Jan 14, 2010
View
219 activerecord/lib/active_record/associations.rb
@@ -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])
+
+ 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
- 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) && ((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
-
- 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)
- ]
+ 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
+
+ [
+ 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
+
+ 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
+
+ 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
- jt_foreign_key = through_reflection.primary_key_name
+ first_key = through_reflection.klass.base_class.to_s.foreign_key
+ second_key = options[:foreign_key] || primary_key
end
- 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
+ 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
-
- ["(%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
- ]
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
+
+ [
+ [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
View
9 activerecord/lib/active_record/associations/association_collection.rb
@@ -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) }
View
2 activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -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
View
3 activerecord/lib/active_record/associations/through_association_scope.rb
@@ -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,
View
144 activerecord/lib/active_record/base.rb
@@ -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]))
-
- lock = (scope && scope[:lock]) || options[:lock]
- relation = relation.lock if lock.present?
+ includes(options[:include])
- 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,52 +1831,49 @@ 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
+ 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 ])
+ method_scoping.assert_valid_keys([ :find, :create ])
- if f = method_scoping[:find]
- f.assert_valid_keys(VALID_FIND_OPTIONS)
- set_readonly_option! f
- end
+ 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
+ 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
- hash
+
+ 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
@@ -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
View
73 activerecord/lib/active_record/calculations.rb
@@ -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])
+
+
+ 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.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)
+ relation = relation.limit(options[:limit] || merge_limit) if using_limitable_reflections?(join_dependency.reflections)
relation
end
View
6 activerecord/lib/active_record/named_scope.rb
@@ -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
View
15 activerecord/lib/active_record/relation.rb
@@ -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
View
5 activerecord/lib/active_record/relation/calculation_methods.rb
@@ -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
View
94 activerecord/lib/active_record/relation/query_methods.rb
@@ -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)
View
28 activerecord/lib/active_record/relation/spawn_methods.rb
@@ -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
View
28 activerecord/test/cases/associations/inner_join_association_test.rb
@@ -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
View
65 activerecord/test/cases/method_scoping_test.rb
@@ -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,35 +256,36 @@ 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
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,23 +327,23 @@ 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
# 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

0 comments on commit bed9179

Please sign in to comment.