diff options
Diffstat (limited to 'activerecord/lib/active_record/relation/query_methods.rb')
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 225 |
1 files changed, 128 insertions, 97 deletions
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 257221174b..78da6a83ec 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -34,10 +34,11 @@ module ActiveRecord # # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' - # def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel + when NilClass + raise ArgumentError, 'Invalid argument for .where.not(), got nil.' when Arel::Nodes::In Arel::Nodes::NotIn.new(rel.left, rel.right) when Arel::Nodes::Equality @@ -101,6 +102,14 @@ module ActiveRecord # firing an additional query. This will often result in a # performance improvement over a simple +join+. # + # You can also specify multiple relationships, like this: + # + # users = User.includes(:address, :friends) + # + # Loading nested relationships is possible using a Hash: + # + # users = User.includes(:address, friends: [:address, :followers]) + # # === conditions # # If you want to add conditions to your included models you'll have @@ -112,14 +121,15 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - check_if_method_has_arguments!("includes", args) + check_if_method_has_arguments!(:includes, args) spawn.includes!(*args) end def includes!(*args) # :nodoc: - args.reject! {|a| a.blank? } + args.reject!(&:blank?) + args.flatten! - self.includes_values = (includes_values + args).flatten.uniq + self.includes_values |= args self end @@ -130,7 +140,7 @@ module ActiveRecord # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = # "users"."id" def eager_load(*args) - check_if_method_has_arguments!("eager_load", args) + check_if_method_has_arguments!(:eager_load, args) spawn.eager_load!(*args) end @@ -144,7 +154,7 @@ module ActiveRecord # User.preload(:posts) # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - check_if_method_has_arguments!("preload", args) + check_if_method_has_arguments!(:preload, args) spawn.preload!(*args) end @@ -162,14 +172,15 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - check_if_method_has_arguments!("references", args) + check_if_method_has_arguments!(:references, args) spawn.references!(*args) end def references!(*args) # :nodoc: args.flatten! + args.map!(&:to_s) - self.references_values = (references_values + args.map!(&:to_s)).uniq + self.references_values |= args self end @@ -222,7 +233,9 @@ module ActiveRecord end def select!(*fields) # :nodoc: - self.select_values += fields.flatten + fields.flatten! + + self.select_values += fields self end @@ -242,7 +255,7 @@ module ActiveRecord # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] def group(*args) - check_if_method_has_arguments!("group", args) + check_if_method_has_arguments!(:group, args) spawn.group!(*args) end @@ -273,24 +286,14 @@ module ActiveRecord # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC def order(*args) - check_if_method_has_arguments!("order", args) + check_if_method_has_arguments!(:order, args) spawn.order!(*args) end def order!(*args) # :nodoc: - args.flatten! - validate_order_args args - - references = args.reject { |arg| Arel::Node === arg } - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! - references!(references) if references.any? + preprocess_order_args(args) - # if a symbol is given we prepend the quoted table name - args = args.map { |arg| - arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg - } - - self.order_values = args + self.order_values + self.order_values += args self end @@ -302,15 +305,14 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY name ASC, id ASC'. + # generates a query with 'ORDER BY id ASC, name ASC'. def reorder(*args) - check_if_method_has_arguments!("reorder", args) + check_if_method_has_arguments!(:reorder, args) spawn.reorder!(*args) end def reorder!(*args) # :nodoc: - args.flatten! - validate_order_args args + preprocess_order_args(args) self.reordering_value = true self.order_values = args @@ -341,20 +343,27 @@ module ActiveRecord # 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: + # This method is similar to <tt>except</tt>, but unlike + # <tt>except</tt>, it persists across merges: # - # Post.comments.except(:order) + # User.order('email').merge(User.except(:order)) + # == User.order('email') + # + # User.order('email').merge(User.unscope(:order)) + # == User.all + # + # This means it can be used in association definitions: + # + # has_many :comments, -> { unscope where: :trashed } # - # will still have an order if it comes from the default_scope on Comment. def unscope(*args) - check_if_method_has_arguments!("unscope", args) + check_if_method_has_arguments!(:unscope, args) spawn.unscope!(*args) end - def unscope!(*args) + def unscope!(*args) # :nodoc: args.flatten! + self.unscope_values += args args.each do |scope| case scope @@ -388,8 +397,12 @@ module ActiveRecord # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) - check_if_method_has_arguments!("joins", args) - spawn.joins!(*args.compact.flatten) + check_if_method_has_arguments!(:joins, args) + + args.compact! + args.flatten! + + spawn.joins!(*args) end def joins!(*args) # :nodoc: @@ -416,7 +429,7 @@ module ActiveRecord # === string # # A single string, without additional arguments, is passed to the query - # constructor as a SQL fragment, and used in the where clause of the query. + # constructor as an SQL fragment, and used in the where clause of the query. # # Client.where("orders_count = '2'") # # SELECT * from clients where orders_count = '2'; @@ -546,13 +559,24 @@ module ActiveRecord end end + # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. + # + # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0 + # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0 + # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0 + # + # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping + # the named conditions -- not the entire where statement. + def rewhere(conditions) + unscope(where: conditions.keys).where(conditions) + end + # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) opts.blank? ? self : spawn.having!(opts, *rest) - spawn.having!(opts, *rest) end def having!(opts, *rest) # :nodoc: @@ -609,12 +633,11 @@ module ActiveRecord self end - # Returns a chainable relation with zero records, specifically an - # instance of the <tt>ActiveRecord::NullRelation</tt> class. + # Returns a chainable relation with zero records. # - # 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 querying the database. + # The returned relation implements the Null Object pattern. It is an + # object with defined null behavior and always returns an empty 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. @@ -634,7 +657,7 @@ module ActiveRecord # when 'Reviewer' # Post.published # when 'Bad User' - # Post.none # => returning [] instead breaks the previous code + # Post.none # It can't be chained if [] is returned. # end # end # @@ -686,7 +709,7 @@ module ActiveRecord # Specifies table from which the records will be fetched. For example: # # Topic.select('title').from('posts') - # #=> SELECT title FROM posts + # # => SELECT title FROM posts # # Can accept other relation objects. For example: # @@ -772,9 +795,10 @@ module ActiveRecord end def extending!(*modules, &block) # :nodoc: - modules << Module.new(&block) if block_given? + modules << Module.new(&block) if block + modules.flatten! - self.extending_values += modules.flatten + self.extending_values += modules extend(*extending_values) if extending_values.any? self @@ -794,23 +818,23 @@ module ActiveRecord # Returns the Arel object associated with the relation. def arel - @arel ||= with_default_scope.build_arel + @arel ||= build_arel end # Like #arel, but ignores the default scope of the model. def build_arel arel = Arel::SelectManager.new(table.engine, table) - build_joins(arel, joins_values) unless joins_values.empty? + build_joins(arel, joins_values.flatten) unless joins_values.empty? collapse_wheres(arel, (where_values - ['']).uniq) - arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty? + arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? arel.take(connection.sanitize_limit(limit_value)) if limit_value arel.skip(offset_value.to_i) if offset_value - arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? + arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty? build_order(arel) @@ -849,7 +873,7 @@ module ActiveRecord where_values.reject! do |rel| case rel - when Arel::Nodes::In, Arel::Nodes::Equality + when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) subrelation.name.to_sym == target_value_sym else @@ -859,13 +883,11 @@ module ActiveRecord end def custom_join_ast(table, joins) - joins = joins.reject { |join| join.blank? } + joins = joins.reject(&:blank?) return [] if joins.empty? - @implicit_readonly = true - - joins.map do |join| + joins.map! do |join| case join when Array join = Arel.sql(join.join(' ')) if array_of_strings?(join) @@ -877,21 +899,28 @@ module ActiveRecord end def collapse_wheres(arel, wheres) - equalities = wheres.grep(Arel::Nodes::Equality) - - arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty? - - (wheres - equalities).each do |where| + predicates = wheres.map do |where| + next where if ::Arel::Nodes::Equality === where where = Arel.sql(where) if String === where - arel.where(Arel::Nodes::Grouping.new(where)) + Arel::Nodes::Grouping.new(where) end + + arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? end def build_where(opts, other = []) case opts when String, Array + #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 + values = Hash === other.first ? other.first.values : other + + values.grep(ActiveRecord::Relation) do |rel| + self.bind_values += rel.bind_values + end + [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash + opts = PredicateBuilder.resolve_column_aliases(klass, opts) attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) attributes.values.grep(ActiveRecord::Relation) do |rel| @@ -909,6 +938,7 @@ module ActiveRecord case opts when Relation name ||= 'subquery' + self.bind_values = opts.bind_values + self.bind_values opts.arel.as(name.to_s) else opts @@ -922,7 +952,7 @@ module ActiveRecord :string_join when Hash, Symbol, Array :association_join - when ActiveRecord::Associations::JoinDependency::JoinAssociation + when ActiveRecord::Associations::JoinDependency :stashed_join when Arel::Nodes::Join :join_node @@ -934,9 +964,7 @@ module ActiveRecord association_joins = buckets[:association_join] || [] stashed_association_joins = buckets[:stashed_join] || [] join_nodes = (buckets[:join_node] || []).uniq - string_joins = (buckets[:string_join] || []).map { |x| - x.strip - }.uniq + string_joins = (buckets[:string_join] || []).map(&:strip).uniq join_list = join_nodes + custom_join_ast(manager, string_joins) @@ -946,24 +974,20 @@ module ActiveRecord join_list ) - join_dependency.graft(*stashed_association_joins) - - @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? + joins = join_dependency.join_constraints stashed_association_joins - # FIXME: refactor this to build an AST - join_dependency.join_associations.each do |association| - association.join_to(manager) - end + joins.each { |join| manager.from(join) } - manager.join_sources.concat join_list + manager.join_sources.concat(join_list) manager end def build_select(arel, selects) - unless selects.empty? - @implicit_readonly = false + if !selects.empty? arel.project(*selects) + elsif from_value + arel.project(Arel.star) else arel.project(@klass.arel_table[Arel.star]) end @@ -977,16 +1001,10 @@ module ActiveRecord when Arel::Nodes::Ordering o.reverse when String - o.to_s.split(',').collect do |s| + o.to_s.split(',').map! do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end - when Symbol - { o => :desc } - when Hash - o.each_with_object({}) do |(field, dir), memo| - memo[field] = (dir == :asc ? :desc : :asc ) - end else o end @@ -994,35 +1012,48 @@ module ActiveRecord end def array_of_strings?(o) - o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} + o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) } end def build_order(arel) - orders = order_values + orders = order_values.uniq + orders.reject!(&:blank?) orders = reverse_sql_order(orders) if reverse_order_value - orders = orders.uniq.reject(&:blank?).flat_map do |order| - case order - when Symbol - table[order].asc - when Hash - order.map { |field, dir| table[field].send(dir) } - else - order - end - end - arel.order(*orders) unless orders.empty? end def validate_order_args(args) - args.select { |a| Hash === a }.each do |h| + args.grep(Hash) do |h| unless (h.values - [:asc, :desc]).empty? raise ArgumentError, 'Direction should be :asc or :desc' end end end + def preprocess_order_args(order_args) + order_args.flatten! + validate_order_args(order_args) + + references = order_args.grep(String) + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? + + # if a symbol is given we prepend the quoted table name + order_args.map! do |arg| + case arg + when Symbol + table[arg].asc + when Hash + arg.map { |field, dir| + table[field].send(dir) + } + else + arg + end + end.flatten! + end + # Checks to make sure that the arguments are not blank. Note that if some # blank-like object were initially passed into the query method, then this # method will not raise an error. |