diff options
Diffstat (limited to 'activerecord/lib/active_record/relation/query_methods.rb')
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 118 |
1 files changed, 107 insertions, 11 deletions
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4b8c40592e..257221174b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -5,7 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern # WhereChain objects act as placeholder for queries in which #where does not have any parameter. - # In this case, #where must be chained with either #not, #like, or #not_like to return a new relation. + # In this case, #where must be chained with #not to return a new relation. class WhereChain def initialize(scope) @scope = scope @@ -317,6 +317,67 @@ module ActiveRecord self end + VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, + :limit, :offset, :joins, :includes, :from, + :readonly, :having]) + + # Removes an unwanted relation that is already defined on a chain of relations. + # This is useful when passing around chains of relations and would like to + # modify the relations without reconstructing the entire chain. + # + # User.order('email DESC').unscope(:order) == User.all + # + # The method arguments are symbols which correspond to the names of the methods + # which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES. + # The method can also be called with multiple arguments. For example: + # + # User.order('email DESC').select('id').where(name: "John") + # .unscope(:order, :select, :where) == User.all + # + # One can additionally pass a hash as an argument to unscope specific :where values. + # This is done by passing a hash with a single key-value pair. The key should be + # :where and the value should be the where value to unscope. For example: + # + # User.where(name: "John", active: true).unscope(where: :name) + # == User.where(active: true) + # + # Note that this method is more generalized than ActiveRecord::SpawnMethods#except + # because #except will only affect a particular relation's values. It won't wipe + # the order, grouping, etc. when that relation is merged. For example: + # + # Post.comments.except(:order) + # + # will still have an order if it comes from the default_scope on Comment. + def unscope(*args) + check_if_method_has_arguments!("unscope", args) + spawn.unscope!(*args) + end + + def unscope!(*args) + args.flatten! + + args.each do |scope| + case scope + when Symbol + symbol_unscoping(scope) + when Hash + scope.each do |key, target_value| + if key != :where + raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key." + end + + Array(target_value).each do |val| + where_unscoping(val) + end + end + else + raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." + end + end + + self + end + # Performs a joins on +args+: # # User.joins(:posts) @@ -553,7 +614,7 @@ module ActiveRecord # # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the # Null Object pattern. It is an object with defined null behavior and always returns an empty - # array of records without quering the database. + # array of records without querying the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -649,20 +710,22 @@ module ActiveRecord # User.select(:name) # # => Might return two records with the same name # - # User.select(:name).uniq - # # => Returns 1 record per unique name + # User.select(:name).distinct + # # => Returns 1 record per distinct name # - # User.select(:name).uniq.uniq(false) + # User.select(:name).distinct.distinct(false) # # => You can also remove the uniqueness - def uniq(value = true) - spawn.uniq!(value) + def distinct(value = true) + spawn.distinct!(value) end + alias uniq distinct - # Like #uniq, but modifies relation in place. - def uniq!(value = true) # :nodoc: - self.uniq_value = value + # Like #distinct, but modifies relation in place. + def distinct!(value = true) # :nodoc: + self.distinct_value = value self end + alias uniq! distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -753,7 +816,7 @@ module ActiveRecord build_select(arel, select_values.uniq) - arel.distinct(uniq_value) + arel.distinct(distinct_value) arel.from(build_from) if from_value arel.lock(lock_value) if lock_value @@ -762,6 +825,39 @@ module ActiveRecord private + def symbol_unscoping(scope) + if !VALID_UNSCOPING_VALUES.include?(scope) + raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." + end + + single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) + unscope_code = :"#{scope}_value#{'s' unless single_val_method}=" + + case scope + when :order + self.send(:reverse_order_value=, false) + result = [] + else + result = [] unless single_val_method + end + + self.send(unscope_code, result) + end + + def where_unscoping(target_value) + target_value_sym = target_value.to_sym + + where_values.reject! do |rel| + case rel + when Arel::Nodes::In, Arel::Nodes::Equality + subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) + subrelation.name.to_sym == target_value_sym + else + raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented." + end + end + end + def custom_join_ast(table, joins) joins = joins.reject { |join| join.blank? } |