# frozen_string_literal: true require "active_record/relation/from_clause" require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_support/core_ext/array/wrap" module ActiveRecord module QueryMethods include ActiveModel::ForbiddenAttributesProtection # +WhereChain+ objects act as placeholder for queries in which +where+ does not have any parameter. # In this case, +where+ can be chained to return a new relation. class WhereChain def initialize(scope) # :nodoc: @scope = scope end # Returns a new relation expressing WHERE + NOT condition according to # the conditions in the arguments. # # #not accepts conditions as a string, array, or hash. See QueryMethods#where for # more details on each format. # # User.where.not("name = 'Jon'") # # SELECT * FROM users WHERE NOT (name = 'Jon') # # User.where.not(["name = ?", "Jon"]) # # SELECT * FROM users WHERE NOT (name = 'Jon') # # User.where.not(name: "Jon") # # SELECT * FROM users WHERE name != 'Jon' # # User.where.not(name: nil) # # SELECT * FROM users WHERE name IS NOT NULL # # User.where.not(name: %w(Ko1 Nobu)) # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu') # # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE NOT (name = 'Jon' AND role = 'admin') # # If there is a non-nil condition on a nullable column in the hash condition, the records that have # nil values on the nullable column won't be returned. # User.create!(nullable_country: nil) # User.where.not(nullable_country: "UK") # # SELECT * FROM users WHERE NOT (nullable_country = 'UK') # # => [] def not(opts, *rest) where_clause = @scope.send(:build_where_clause, opts, rest) @scope.where_clause += where_clause.invert @scope end # Returns a new relation with joins and where clause to identify # associated relations. # # For example, posts that are associated to a related author: # # Post.where.associated(:author) # # SELECT "posts".* FROM "posts" # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # WHERE "authors"."id" IS NOT NULL # # Additionally, multiple relations can be combined. This will return posts # associated to both an author and any comments: # # Post.where.associated(:author, :comments) # # SELECT "posts".* FROM "posts" # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" # # WHERE "authors"."id" IS NOT NULL AND "comments"."id" IS NOT NULL # # You can define join type in the scope and +associated+ will not use `JOIN` by default. # # Post.left_joins(:author).where.associated(:author) # # SELECT "posts".* FROM "posts" # # LEFT OUTER JOIN "authors" "authors"."id" = "posts"."author_id" # # WHERE "authors"."id" IS NOT NULL # # Post.left_joins(:comments).where.associated(:author) # # SELECT "posts".* FROM "posts" # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" # # WHERE "author"."id" IS NOT NULL def associated(*associations) associations.each do |association| reflection = scope_association_reflection(association) unless @scope.joins_values.include?(reflection.name) || @scope.left_outer_joins_values.include?(reflection.name) @scope.joins!(association) end association_conditions = Array(reflection.association_primary_key).index_with(nil) if reflection.options[:class_name] self.not(association => association_conditions) else self.not(reflection.table_name => association_conditions) end end @scope end # Returns a new relation with left outer joins and where clause to identify # missing relations. # # For example, posts that are missing a related author: # # Post.where.missing(:author) # # SELECT "posts".* FROM "posts" # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # WHERE "authors"."id" IS NULL # # Additionally, multiple relations can be combined. This will return posts # that are missing both an author and any comments: # # Post.where.missing(:author, :comments) # # SELECT "posts".* FROM "posts" # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" # # WHERE "authors"."id" IS NULL AND "comments"."id" IS NULL def missing(*associations) associations.each do |association| reflection = scope_association_reflection(association) @scope.left_outer_joins!(association) association_conditions = Array(reflection.association_primary_key).index_with(nil) if reflection.options[:class_name] @scope.where!(association => association_conditions) else @scope.where!(reflection.table_name => association_conditions) end end @scope end private def scope_association_reflection(association) model = @scope.model reflection = model._reflect_on_association(association) unless reflection raise ArgumentError.new("An association named `:#{association}` does not exist on the model `#{model.name}`.") end reflection end end # A wrapper to distinguish CTE joins from other nodes. class CTEJoin # :nodoc: attr_reader :name def initialize(name) @name = name end end FROZEN_EMPTY_ARRAY = [].freeze FROZEN_EMPTY_HASH = {}.freeze Relation::VALUE_METHODS.each do |name| method_name, default = case name when *Relation::MULTI_VALUE_METHODS ["#{name}_values", "FROZEN_EMPTY_ARRAY"] when *Relation::SINGLE_VALUE_METHODS ["#{name}_value", name == :create_with ? "FROZEN_EMPTY_HASH" : "nil"] when *Relation::CLAUSE_METHODS ["#{name}_clause", name == :from ? "Relation::FromClause.empty" : "Relation::WhereClause.empty"] end class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{method_name} # def includes_values @values.fetch(:#{name}, #{default}) # @values.fetch(:includes, FROZEN_EMPTY_ARRAY) end # end def #{method_name}=(value) # def includes_values=(value) assert_modifiable! # assert_modifiable! @values[:#{name}] = value # @values[:includes] = value end # end CODE end alias extensions extending_values # Specify associations +args+ to be eager loaded to prevent N + 1 queries. # A separate query is performed for each association, unless a join is # required by conditions. # # For example: # # users = User.includes(:address).limit(5) # users.each do |user| # user.address.city # end # # # SELECT "users".* FROM "users" LIMIT 5 # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5) # # Instead of loading the 5 addresses with 5 separate queries, all addresses # are loaded with a single query. # # Loading the associations in a separate query will often result in a # performance improvement over a simple join, as a join can result in many # rows that contain redundant data and it performs poorly at scale. # # You can also specify multiple associations. Each association will result # in an additional query: # # User.includes(:address, :friends).to_a # # SELECT "users".* FROM "users" # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5) # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5) # # Loading nested associations is possible using a Hash: # # User.includes(:address, friends: [:address, :followers]) # # === Conditions # # If you want to add string conditions to your included models, you'll have # to explicitly reference them. For example: # # User.includes(:posts).where('posts.name = ?', 'example').to_a # # Will throw an error, but this will work: # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts).to_a # # SELECT "users"."id" AS t0_r0, ... FROM "users" # # LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" # # WHERE "posts"."name" = ? [["name", "example"]] # # As the LEFT OUTER JOIN already contains the posts, the second query for # the posts is no longer performed. # # Note that #includes works with association names while #references needs # the actual table name. # # If you pass the conditions via a Hash, you don't need to call #references # explicitly, as #where references the tables for you. For example, this # will work correctly: # # User.includes(:posts).where(posts: { name: 'example' }) # # NOTE: Conditions affect both sides of an association. For example, the # above code will return only users that have a post named "example", # and will only include posts named "example", even when a # matching user has other additional posts. def includes(*args) check_if_method_has_arguments!(__callee__, args) spawn.includes!(*args) end def includes!(*args) # :nodoc: self.includes_values |= args self end def all # :nodoc: spawn end # Specify associations +args+ to be eager loaded using a LEFT OUTER JOIN. # Performs a single query joining all specified associations. For example: # # users = User.eager_load(:address).limit(5) # users.each do |user| # user.address.city # end # # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users" # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id" # # LIMIT 5 # # Instead of loading the 5 addresses with 5 separate queries, all addresses # are loaded with a single joined query. # # Loading multiple and nested associations is possible using Hashes and Arrays, # similar to #includes: # # User.eager_load(:address, friends: [:address, :followers]) # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users" # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id" # # LEFT OUTER JOIN "friends" ON "friends"."user_id" = "users"."id" # # ... # # NOTE: Loading the associations in a join can result in many rows that # contain redundant data and it performs poorly at scale. def eager_load(*args) check_if_method_has_arguments!(__callee__, args) spawn.eager_load!(*args) end def eager_load!(*args) # :nodoc: self.eager_load_values |= args self end # Specify associations +args+ to be eager loaded using separate queries. # A separate query is performed for each association. # # users = User.preload(:address).limit(5) # users.each do |user| # user.address.city # end # # # SELECT "users".* FROM "users" LIMIT 5 # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5) # # Instead of loading the 5 addresses with 5 separate queries, all addresses # are loaded with a separate query. # # Loading multiple and nested associations is possible using Hashes and Arrays, # similar to #includes: # # User.preload(:address, friends: [:address, :followers]) # # SELECT "users".* FROM "users" # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5) # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5) # # SELECT ... def preload(*args) check_if_method_has_arguments!(__callee__, args) spawn.preload!(*args) end def preload!(*args) # :nodoc: self.preload_values |= args self end # Extracts a named +association+ from the relation. The named association is first preloaded, # then the individual association records are collected from the relation. Like so: # # account.memberships.extract_associated(:user) # # => Returns collection of User records # # This is short-hand for: # # account.memberships.preload(:user).collect(&:user) def extract_associated(association) preload(association).collect(&association) end # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be +JOIN+ed in any query rather than loaded separately. # This method only works in conjunction with #includes. # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") # # Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # Query now knows the string references posts, so adds a JOIN def references(*table_names) check_if_method_has_arguments!(__callee__, table_names) spawn.references!(*table_names) end def references!(*table_names) # :nodoc: self.references_values |= table_names self end # Works in two unique ways. # # First: takes a block so it can be used just like Array#select. # # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, # converting them into an array and iterating through them using # Array#select. # # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: # # Model.select(:field) # # => [#] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query # methods appended to it, such as the other methods in ActiveRecord::QueryMethods. # # The argument to the method can also be an array of fields. # # Model.select(:field, :other_field, :and_one_more) # # => [#] # # The argument also can be a hash of fields and aliases. # # Model.select(models: { field: :alias, other_field: :other_alias }) # # => [#] # # Model.select(models: [:field, :other_field]) # # => [#] # # You can also use one or more strings, which will be used unchanged as SELECT fields. # # Model.select('field AS field_one', 'other_field AS field_two') # # => [#] # # If an alias was specified, it will be accessible from the resulting objects: # # Model.select('field AS field_one').first.field_one # # => "value" # # Accessing attributes of an object that do not have fields retrieved by a select # except +id+ will throw ActiveModel::MissingAttributeError: # # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute 'other_field' for Model def select(*fields) if block_given? if fields.any? raise ArgumentError, "`select' with block doesn't take arguments." end return super() end check_if_method_has_arguments!(__callee__, fields, "Call `select' with at least one field.") fields = process_select_args(fields) spawn._select!(*fields) end def _select!(*fields) # :nodoc: self.select_values |= fields self end # Add a Common Table Expression (CTE) that you can then reference within another SELECT statement. # # Note: CTE's are only supported in MySQL for versions 8.0 and above. You will not be able to # use CTE's with MySQL 5.7. # # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)) # # => ActiveRecord::Relation # # WITH posts_with_tags AS ( # # SELECT * FROM posts WHERE (tags_count > 0) # # ) # # SELECT * FROM posts # # You can also pass an array of sub-queries to be joined in a +UNION ALL+. # # Post.with(posts_with_tags_or_comments: [Post.where("tags_count > ?", 0), Post.where("comments_count > ?", 0)]) # # => ActiveRecord::Relation # # WITH posts_with_tags_or_comments AS ( # # (SELECT * FROM posts WHERE (tags_count > 0)) # # UNION ALL # # (SELECT * FROM posts WHERE (comments_count > 0)) # # ) # # SELECT * FROM posts # # Once you define Common Table Expression you can use custom +FROM+ value or +JOIN+ to reference it. # # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts") # # => ActiveRecord::Relation # # WITH posts_with_tags AS ( # # SELECT * FROM posts WHERE (tags_count > 0) # # ) # # SELECT * FROM posts_with_tags AS posts # # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).joins("JOIN posts_with_tags ON posts_with_tags.id = posts.id") # # => ActiveRecord::Relation # # WITH posts_with_tags AS ( # # SELECT * FROM posts WHERE (tags_count > 0) # # ) # # SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id # # It is recommended to pass a query as ActiveRecord::Relation. If that is not possible # and you have verified it is safe for the database, you can pass it as SQL literal # using +Arel+. # # Post.with(popular_posts: Arel.sql("... complex sql to calculate posts popularity ...")) # # Great caution should be taken to avoid SQL injection vulnerabilities. This method should not # be used with unsafe values that include unsanitized input. # # To add multiple CTEs just pass multiple key-value pairs # # Post.with( # posts_with_comments: Post.where("comments_count > ?", 0), # posts_with_tags: Post.where("tags_count > ?", 0) # ) # # or chain multiple +.with+ calls # # Post # .with(posts_with_comments: Post.where("comments_count > ?", 0)) # .with(posts_with_tags: Post.where("tags_count > ?", 0)) def with(*args) raise ArgumentError, "ActiveRecord::Relation#with does not accept a block" if block_given? check_if_method_has_arguments!(__callee__, args) spawn.with!(*args) end # Like #with, but modifies relation in place. def with!(*args) # :nodoc: args = process_with_args(args) self.with_values |= args self end # Add a recursive Common Table Expression (CTE) that you can then reference within another SELECT statement. # # Post.with_recursive(post_and_replies: [Post.where(id: 42), Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id')]) # # => ActiveRecord::Relation # # WITH RECURSIVE post_and_replies AS ( # # (SELECT * FROM posts WHERE id = 42) # # UNION ALL # # (SELECT * FROM posts JOIN posts_and_replies ON posts.in_reply_to_id = posts_and_replies.id) # # ) # # SELECT * FROM posts # # See `#with` for more information. def with_recursive(*args) check_if_method_has_arguments!(__callee__, args) spawn.with_recursive!(*args) end # Like #with_recursive but modifies the relation in place. def with_recursive!(*args) # :nodoc: args = process_with_args(args) self.with_values |= args @with_is_recursive = true self end # Allows you to change a previously set select statement. # # Post.select(:title, :body) # # SELECT `posts`.`title`, `posts`.`body` FROM `posts` # # Post.select(:title, :body).reselect(:created_at) # # SELECT `posts`.`created_at` FROM `posts` # # This is short-hand for unscope(:select).select(fields). # Note that we're unscoping the entire select statement. def reselect(*args) check_if_method_has_arguments!(__callee__, args) args = process_select_args(args) spawn.reselect!(*args) end # Same as #reselect but operates on relation in-place instead of copying. def reselect!(*args) # :nodoc: self.select_values = args self end # Allows to specify a group attribute: # # User.group(:name) # # SELECT "users".* FROM "users" GROUP BY name # # Returns an array with distinct records based on the +group+ attribute: # # User.select([:id, :name]) # # => [#, #, #] # # User.group(:name) # # => [#, #] # # User.group('name AS grouped_name, age') # # => [#, #, #] # # Passing in an array of attributes to group by is also supported. # # User.select([:id, :first_name]).group(:id, :first_name).first(3) # # => [#, #, #] def group(*args) check_if_method_has_arguments!(__callee__, args) spawn.group!(*args) end def group!(*args) # :nodoc: self.group_values += args self end # Allows you to change a previously set group statement. # # Post.group(:title, :body) # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body` # # Post.group(:title, :body).regroup(:title) # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title` # # This is short-hand for unscope(:group).group(fields). # Note that we're unscoping the entire group statement. def regroup(*args) check_if_method_has_arguments!(__callee__, args) spawn.regroup!(*args) end # Same as #regroup but operates on relation in-place instead of copying. def regroup!(*args) # :nodoc: self.group_values = args self end # Applies an ORDER BY clause to a query. # # #order accepts arguments in one of several formats. # # === symbols # # The symbol represents the name of the column you want to order the results by. # # User.order(:name) # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC # # By default, the order is ascending. If you want descending order, you can # map the column name symbol to +:desc+. # # User.order(email: :desc) # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC # # Multiple columns can be passed this way, and they will be applied in the order specified. # # User.order(:name, email: :desc) # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC # # === strings # # Strings are passed directly to the database, allowing you to specify # simple SQL expressions. # # This could be a source of SQL injection, so only strings composed of plain # column names and simple function(column_name) expressions # with optional +ASC+/+DESC+ modifiers are allowed. # # User.order('name') # # SELECT "users".* FROM "users" ORDER BY name # # User.order('name DESC') # # SELECT "users".* FROM "users" ORDER BY name DESC # # User.order('name DESC, email') # # SELECT "users".* FROM "users" ORDER BY name DESC, email # # === Arel # # If you need to pass in complicated expressions that you have verified # are safe for the database, you can use Arel. # # User.order(Arel.sql('end_date - start_date')) # # SELECT "users".* FROM "users" ORDER BY end_date - start_date # # Custom query syntax, like JSON columns for PostgreSQL, is supported in this way. # # User.order(Arel.sql("payload->>'kind'")) # # SELECT "users".* FROM "users" ORDER BY payload->>'kind' def order(*args) check_if_method_has_arguments!(__callee__, args) do sanitize_order_arguments(args) end spawn.order!(*args) end # Same as #order but operates on relation in-place instead of copying. def order!(*args) # :nodoc: preprocess_order_args(args) unless args.empty? self.order_values |= args self end # Applies an ORDER BY clause based on a given +column+, # ordered and filtered by a specific set of +values+. # # User.in_order_of(:id, [1, 5, 3]) # # SELECT "users".* FROM "users" # # WHERE "users"."id" IN (1, 5, 3) # # ORDER BY CASE # # WHEN "users"."id" = 1 THEN 1 # # WHEN "users"."id" = 5 THEN 2 # # WHEN "users"."id" = 3 THEN 3 # # END ASC # # +column+ can point to an enum column; the actual query generated may be different depending # on the database adapter and the column definition. # # class Conversation < ActiveRecord::Base # enum :status, [ :active, :archived ] # end # # Conversation.in_order_of(:status, [:archived, :active]) # # SELECT "conversations".* FROM "conversations" # # WHERE "conversations"."status" IN (1, 0) # # ORDER BY CASE # # WHEN "conversations"."status" = 1 THEN 1 # # WHEN "conversations"."status" = 0 THEN 2 # # END ASC # # +values+ can also include +nil+. # # Conversation.in_order_of(:status, [nil, :archived, :active]) # # SELECT "conversations".* FROM "conversations" # # WHERE ("conversations"."status" IN (1, 0) OR "conversations"."status" IS NULL) # # ORDER BY CASE # # WHEN "conversations"."status" IS NULL THEN 1 # # WHEN "conversations"."status" = 1 THEN 2 # # WHEN "conversations"."status" = 0 THEN 3 # # END ASC # # +filter+ can be set to +false+ to include all results instead of only the ones specified in +values+. # # Conversation.in_order_of(:status, [:archived, :active], filter: false) # # SELECT "conversations".* FROM "conversations" # # ORDER BY CASE # # WHEN "conversations"."status" = 1 THEN 1 # # WHEN "conversations"."status" = 0 THEN 2 # # ELSE 3 # # END ASC def in_order_of(column, values, filter: true) model.disallow_raw_sql!([column], permit: model.adapter_class.column_name_with_order_matcher) return spawn.none! if values.empty? references = column_references([column]) self.references_values |= references unless references.empty? values = values.map { |value| model.type_caster.type_cast_for_database(column, value) } arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s) scope = spawn.order!(build_case_for_value_position(arel_column, values, filter: filter)) if filter where_clause = if values.include?(nil) arel_column.in(values.compact).or(arel_column.eq(nil)) else arel_column.in(values) end scope = scope.where!(where_clause) end scope end # Replaces any existing order defined on the relation with the specified order. # # User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC' # # Subsequent calls to order on the same relation will be appended. For example: # # User.order('email DESC').reorder('id ASC').order('name ASC') # # generates a query with ORDER BY id ASC, name ASC. def reorder(*args) check_if_method_has_arguments!(__callee__, args) do sanitize_order_arguments(args) end spawn.reorder!(*args) end # Same as #reorder but operates on relation in-place instead of copying. def reorder!(*args) # :nodoc: preprocess_order_args(args) args.uniq! self.reordering_value = true self.order_values = args self end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, :limit, :offset, :joins, :left_outer_joins, :annotate, :includes, :eager_load, :preload, :from, :readonly, :having, :optimizer_hints, :with]) # 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) # # This method is similar to #except, but unlike # #except, it persists across merges: # # 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) } # def unscope(*args) check_if_method_has_arguments!(__callee__, args) spawn.unscope!(*args) end def unscope!(*args) # :nodoc: self.unscope_values += args args.each do |scope| case scope when Symbol scope = :left_outer_joins if scope == :left_joins 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 assert_modifiable! @values.delete(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 target_values = resolve_arel_attributes(Array.wrap(target_value)) self.where_clause = where_clause.except(*target_values) end else raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." end end self end # Performs JOINs on +args+. The given symbol(s) should match the name of # the association(s). # # User.joins(:posts) # # SELECT "users".* # # FROM "users" # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" # # Multiple joins: # # User.joins(:posts, :account) # # SELECT "users".* # # FROM "users" # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" # # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id" # # Nested joins: # # User.joins(posts: [:comments]) # # SELECT "users".* # # FROM "users" # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" # # You can use strings in order to customize your joins: # # 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!(__callee__, args) spawn.joins!(*args) end def joins!(*args) # :nodoc: self.joins_values |= args self end # Performs LEFT OUTER JOINs on +args+: # # User.left_outer_joins(:posts) # # SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" # def left_outer_joins(*args) check_if_method_has_arguments!(__callee__, args) spawn.left_outer_joins!(*args) end alias :left_joins :left_outer_joins def left_outer_joins!(*args) # :nodoc: self.left_outer_joins_values |= args self end # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # # #where accepts conditions in one of several formats. In the examples below, the resulting # SQL is given as an illustration; the actual query generated may be different depending # on the database adapter. # # === \String # # A single string, without additional arguments, is passed to 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'; # # Note that building your own string from user input may expose your application # to injection attacks if not done properly. As an alternative, it is recommended # to use one of the following methods. # # === \Array # # If an array is passed, then the first element of the array is treated as a template, and # the remaining elements are inserted into the template to generate the condition. # Active Record takes care of building the query to avoid injection attacks, and will # convert from the ruby type to the database type where needed. Elements are inserted # into the string in the order in which they appear. # # User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; # # Alternatively, you can use named placeholders in the template, and pass a hash as the # second element of the array. The names in the template are replaced with the corresponding # values from the hash. # # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }]) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; # # This can make for more readable code in complex queries. # # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently # than the previous methods; you are responsible for ensuring that the values in the template # are properly quoted. The values are passed to the connector for quoting, but the caller # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, # the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+. # # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; # # If #where is called with multiple arguments, these are treated as if they were passed as # the elements of a single array. # # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" }) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; # # When using strings to specify conditions, you can use any operator available from # the database. While this provides the most flexibility, you can also unintentionally introduce # dependencies on the underlying database. If your code is intended for general consumption, # test with multiple database backends. # # === \Hash # # #where will also accept a hash condition, in which the keys are fields and the values # are values to be searched for. # # Fields can be symbols or strings. Values can be single values, arrays, or ranges. # # User.where(name: "Joe", email: "joe@example.com") # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com' # # User.where(name: ["Alice", "Bob"]) # # SELECT * FROM users WHERE name IN ('Alice', 'Bob') # # User.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight) # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') # # In the case of a belongs_to relationship, an association key can be used # to specify the model if an ActiveRecord object is used as the value. # # author = Author.find(1) # # # The following queries will be equivalent: # Post.where(author: author) # Post.where(author_id: author) # # This also works with polymorphic belongs_to relationships: # # treasure = Treasure.create(name: 'gold coins') # treasure.price_estimates << PriceEstimate.create(price: 125) # # # The following queries will be equivalent: # PriceEstimate.where(estimate_of: treasure) # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure) # # Hash conditions may also be specified in a tuple-like syntax. Hash keys may be # an array of columns with an array of tuples as values. # # Article.where([:author_id, :id] => [[15, 1], [15, 2]]) # # SELECT * FROM articles WHERE author_id = 15 AND id = 1 OR author_id = 15 AND id = 2 # # === Joins # # If the relation is the result of a join, you may create a condition which uses any of the # tables in the join. For string and array conditions, use the table name in the condition. # # User.joins(:posts).where("posts.created_at < ?", Time.now) # # For hash conditions, you can either use the table name in the key, or use a sub-hash. # # User.joins(:posts).where("posts.published" => true) # User.joins(:posts).where(posts: { published: true }) # # === No Argument # # If no argument is passed, #where returns a new instance of WhereChain, that # can be chained with WhereChain#not, WhereChain#missing, or WhereChain#associated. # # Chaining with WhereChain#not: # # User.where.not(name: "Jon") # # SELECT * FROM users WHERE name != 'Jon' # # Chaining with WhereChain#associated: # # Post.where.associated(:author) # # SELECT "posts".* FROM "posts" # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # WHERE "authors"."id" IS NOT NULL # # Chaining with WhereChain#missing: # # Post.where.missing(:author) # # SELECT "posts".* FROM "posts" # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" # # WHERE "authors"."id" IS NULL # # === Blank Condition # # If the condition is any blank-ish object, then #where is a no-op and returns # the current relation. def where(*args) if args.empty? WhereChain.new(spawn) elsif args.length == 1 && args.first.blank? self else spawn.where!(*args) end end def where!(opts, *rest) # :nodoc: self.where_clause += build_where_clause(opts, rest) self 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) return unscope(:where) if conditions.nil? scope = spawn where_clause = scope.build_where_clause(conditions) scope.unscope!(where: where_clause.extract_attributes) scope.where_clause += where_clause scope end # Allows you to invert an entire where clause instead of manually applying conditions. # # class User # scope :active, -> { where(accepted: true, locked: false) } # end # # User.where(accepted: true) # # WHERE `accepted` = 1 # # User.where(accepted: true).invert_where # # WHERE `accepted` != 1 # # User.active # # WHERE `accepted` = 1 AND `locked` = 0 # # User.active.invert_where # # WHERE NOT (`accepted` = 1 AND `locked` = 0) # # Be careful because this inverts all conditions before +invert_where+ call. # # class User # scope :active, -> { where(accepted: true, locked: false) } # scope :inactive, -> { active.invert_where } # Do not attempt it # end # # # It also inverts `where(role: 'admin')` unexpectedly. # User.where(role: 'admin').inactive # # WHERE NOT (`role` = 'admin' AND `accepted` = 1 AND `locked` = 0) # def invert_where spawn.invert_where! end def invert_where! # :nodoc: self.where_clause = where_clause.invert self end # Checks whether the given relation is structurally compatible with this relation, to determine # if it's possible to use the #and and #or methods without raising an error. Structurally # compatible is defined as: they must be scoping the same model, and they must differ only by # #where (if no #group has been defined) or #having (if a #group is present). # # Post.where("id = 1").structurally_compatible?(Post.where("author_id = 3")) # # => true # # Post.joins(:comments).structurally_compatible?(Post.where("id = 1")) # # => false # def structurally_compatible?(other) structurally_incompatible_values_for(other).empty? end # Returns a new relation, which is the logical intersection of this relation and the one passed # as an argument. # # The two relations must be structurally compatible: they must be scoping the same model, and # they must differ only by #where (if no #group has been defined) or #having (if a #group is # present). # # Post.where(id: [1, 2]).and(Post.where(id: [2, 3])) # # SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (1, 2) AND `posts`.`id` IN (2, 3) # def and(other) if other.is_a?(Relation) spawn.and!(other) else raise ArgumentError, "You have passed #{other.class.name} object to #and. Pass an ActiveRecord::Relation object instead." end end def and!(other) # :nodoc: incompatible_values = structurally_incompatible_values_for(other) unless incompatible_values.empty? raise ArgumentError, "Relation passed to #and must be structurally compatible. Incompatible values: #{incompatible_values}" end self.where_clause |= other.where_clause self.having_clause |= other.having_clause self.references_values |= other.references_values self end # Returns a new relation, which is the logical union of this relation and the one passed as an # argument. # # The two relations must be structurally compatible: they must be scoping the same model, and # they must differ only by #where (if no #group has been defined) or #having (if a #group is # present). # # Post.where("id = 1").or(Post.where("author_id = 3")) # # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3)) # def or(other) if other.is_a?(Relation) if @none other.spawn else spawn.or!(other) end else raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead." end end def or!(other) # :nodoc: incompatible_values = structurally_incompatible_values_for(other) unless incompatible_values.empty? raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}" end self.where_clause = where_clause.or(other.where_clause) self.having_clause = having_clause.or(other.having_clause) self.references_values |= other.references_values self 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) end def having!(opts, *rest) # :nodoc: self.having_clause += build_having_clause(opts, rest) self end # Specifies a limit for the number of records to retrieve. # # User.limit(10) # generated SQL has 'LIMIT 10' # # User.limit(10).limit(20) # generated SQL has 'LIMIT 20' def limit(value) spawn.limit!(value) end def limit!(value) # :nodoc: self.limit_value = value self end # Specifies the number of rows to skip before returning rows. # # User.offset(10) # generated SQL has "OFFSET 10" # # Should be used with order. # # User.offset(10).order("name ASC") def offset(value) spawn.offset!(value) end def offset!(value) # :nodoc: self.offset_value = value self end # Specifies locking settings (default to +true+). For more information # on locking, please see ActiveRecord::Locking. def lock(locks = true) spawn.lock!(locks) end def lock!(locks = true) # :nodoc: case locks when String, TrueClass, NilClass self.lock_value = locks || true else self.lock_value = false end self end # Returns a chainable relation with zero records. # # 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. # # Used in cases where a method or scope could return zero records but the # result needs to be chainable. # # For example: # # @posts = current_user.visible_posts.where(name: params[:name]) # # the visible_posts method is expected to return a chainable Relation # # def visible_posts # case role # when 'Country Manager' # Post.where(country: country) # when 'Reviewer' # Post.published # when 'Bad User' # Post.none # It can't be chained if [] is returned. # end # end # def none spawn.none! end def none! # :nodoc: unless @none where!("1=0") @none = true end self end def null_relation? # :nodoc: @none end # Mark a relation as readonly. Attempting to update a record will result in # an error. # # users = User.readonly # users.first.save # => ActiveRecord::ReadOnlyRecord: User is marked as readonly # # To make a readonly relation writable, pass +false+. # # users.readonly(false) # users.first.save # => true def readonly(value = true) spawn.readonly!(value) end def readonly!(value = true) # :nodoc: self.readonly_value = value self end # Sets the returned relation to strict_loading mode. This will raise an error # if the record tries to lazily load an association. # # user = User.strict_loading.first # user.comments.to_a # => ActiveRecord::StrictLoadingViolationError def strict_loading(value = true) spawn.strict_loading!(value) end def strict_loading!(value = true) # :nodoc: self.strict_loading_value = value self end # Sets attributes to be used when creating new records from a # relation object. # # users = User.where(name: 'Oscar') # users.new.name # => 'Oscar' # # users = users.create_with(name: 'DHH') # users.new.name # => 'DHH' # # You can pass +nil+ to #create_with to reset attributes: # # users = users.create_with(nil) # users.new.name # => 'Oscar' def create_with(value) spawn.create_with!(value) end def create_with!(value) # :nodoc: if value value = sanitize_forbidden_attributes(value) self.create_with_value = create_with_value.merge(value) else self.create_with_value = FROZEN_EMPTY_HASH end self end # Specifies the table from which the records will be fetched. For example: # # Topic.select('title').from('posts') # # SELECT title FROM posts # # Can accept other relation objects. For example: # # Topic.select('title').from(Topic.approved) # # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery # # Passing a second argument (string or symbol), creates the alias for the SQL from clause. Otherwise the alias "subquery" is used: # # Topic.select('a.title').from(Topic.approved, :a) # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a # # It does not add multiple arguments to the SQL from clause. The last +from+ chained is the one used: # # Topic.select('title').from(Topic.approved).from(Topic.inactive) # # SELECT title FROM (SELECT topics.* FROM topics WHERE topics.active = 'f') subquery # # For multiple arguments for the SQL from clause, you can pass a string with the exact elements in the SQL from list: # # color = "red" # Color # .from("colors c, JSONB_ARRAY_ELEMENTS(colored_things) AS colorvalues(colorvalue)") # .where("colorvalue->>'color' = ?", color) # .select("c.*").to_a # # SELECT c.* # # FROM colors c, JSONB_ARRAY_ELEMENTS(colored_things) AS colorvalues(colorvalue) # # WHERE (colorvalue->>'color' = 'red') def from(value, subquery_name = nil) spawn.from!(value, subquery_name) end def from!(value, subquery_name = nil) # :nodoc: self.from_clause = Relation::FromClause.new(value, subquery_name) self end # Specifies whether the records should be unique or not. For example: # # User.select(:name) # # Might return two records with the same name # # User.select(:name).distinct # # Returns 1 record per distinct name # # User.select(:name).distinct.distinct(false) # # You can also remove the uniqueness def distinct(value = true) spawn.distinct!(value) end # Like #distinct, but modifies relation in place. def distinct!(value = true) # :nodoc: self.distinct_value = value self end # Used to extend a scope with additional methods, either through # a module or through a block provided. # # The object returned is a relation, which can be further extended. # # === Using a \Module # # module Pagination # def page(number) # # pagination code goes here # end # end # # scope = Model.all.extending(Pagination) # scope.page(params[:page]) # # You can also pass a list of modules: # # scope = Model.all.extending(Pagination, SomethingElse) # # === Using a Block # # scope = Model.all.extending do # def page(number) # # pagination code goes here # end # end # scope.page(params[:page]) # # You can also use a block and a module list: # # scope = Model.all.extending(Pagination) do # def per_page(number) # # pagination code goes here # end # end def extending(*modules, &block) if modules.any? || block spawn.extending!(*modules, &block) else self end end def extending!(*modules, &block) # :nodoc: modules << Module.new(&block) if block modules.flatten! self.extending_values += modules extend(*extending_values) if extending_values.any? self end # Specify optimizer hints to be used in the SELECT statement. # # Example (for MySQL): # # Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)") # # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics` # # Example (for PostgreSQL with pg_hint_plan): # # Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)") # # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics" def optimizer_hints(*args) check_if_method_has_arguments!(__callee__, args) spawn.optimizer_hints!(*args) end def optimizer_hints!(*args) # :nodoc: self.optimizer_hints_values |= args self end # Reverse the existing order clause on the relation. # # User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC' def reverse_order spawn.reverse_order! end def reverse_order! # :nodoc: orders = order_values.compact_blank self.order_values = reverse_sql_order(orders) self end def skip_query_cache!(value = true) # :nodoc: self.skip_query_cache_value = value self end def skip_preloading! # :nodoc: self.skip_preloading_value = true self end # Adds an SQL comment to queries generated from this relation. For example: # # User.annotate("selecting user names").select(:name) # # SELECT "users"."name" FROM "users" /* selecting user names */ # # User.annotate("selecting", "user", "names").select(:name) # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */ # # The SQL block comment delimiters, "/*" and "*/", will be added automatically. # # Some escaping is performed, however untrusted user input should not be used. def annotate(*args) check_if_method_has_arguments!(__callee__, args) spawn.annotate!(*args) end # Like #annotate, but modifies relation in place. def annotate!(*args) # :nodoc: self.annotate_values += args self end # Deduplicate multiple values. def uniq!(name) if values = @values[name] values.uniq! if values.is_a?(Array) && !values.empty? end self end # Excludes the specified record (or collection of records) from the resulting # relation. For example: # # Post.excluding(post) # # SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1 # # Post.excluding(post_one, post_two) # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2) # # Post.excluding(Post.drafts) # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (3, 4, 5) # # This can also be called on associations. As with the above example, either # a single record of collection thereof may be specified: # # post = Post.find(1) # comment = Comment.find(2) # post.comments.excluding(comment) # # SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2 # # This is short-hand for .where.not(id: post.id) and .where.not(id: [post_one.id, post_two.id]). # # An ArgumentError will be raised if either no records are # specified, or if any of the records in the collection (if a collection # is passed in) are not instances of the same model that the relation is # scoping. def excluding(*records) relations = records.extract! { |element| element.is_a?(Relation) } records.flatten!(1) records.compact! unless records.all?(model) && relations.all? { |relation| relation.model == model } raise ArgumentError, "You must only pass a single or collection of #{model.name} objects to ##{__callee__}." end spawn.excluding!(records + relations.flat_map(&:ids)) end alias :without :excluding def excluding!(records) # :nodoc: predicates = [ predicate_builder[primary_key, records].invert ] self.where_clause += Relation::WhereClause.new(predicates) self end # Returns the Arel object associated with the relation. def arel(aliases = nil) # :nodoc: @arel ||= with_connection { |c| build_arel(c, aliases) } end def construct_join_dependency(associations, join_type) # :nodoc: ActiveRecord::Associations::JoinDependency.new( model, table, associations, join_type ) end protected def build_subquery(subquery_alias, select_value) # :nodoc: subquery = except(:optimizer_hints).arel.as(subquery_alias) Arel::SelectManager.new(subquery).project(select_value).tap do |arel| arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty? end end def build_where_clause(opts, rest = []) # :nodoc: opts = sanitize_forbidden_attributes(opts) if opts.is_a?(Array) opts, *rest = opts end case opts when String if rest.empty? parts = [Arel.sql(opts)] elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts) parts = [build_named_bound_sql_literal(opts, rest.first)] elsif opts.include?("?") parts = [build_bound_sql_literal(opts, rest)] else parts = [model.sanitize_sql(rest.empty? ? opts : [opts, *rest])] end when Hash opts = opts.transform_keys do |key| if key.is_a?(Array) key.map { |k| model.attribute_aliases[k.to_s] || k.to_s } else key = key.to_s model.attribute_aliases[key] || key end end references = PredicateBuilder.references(opts) self.references_values |= references unless references.empty? parts = predicate_builder.build_from_hash(opts) do |table_name| lookup_table_klass_from_join_dependencies(table_name) end when Arel::Nodes::Node parts = [opts] else raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end Relation::WhereClause.new(parts) end alias :build_having_clause :build_where_clause def async! @async = true self end protected def arel_columns(columns) columns.flat_map do |field| case field when Symbol, String arel_column(field) when Proc field.call when Hash arel_columns_from_hash(field) else field end end end private def async spawn.async! end def build_named_bound_sql_literal(statement, values) bound_values = values.transform_values do |value| if ActiveRecord::Relation === value Arel.sql(value.to_sql) elsif value.respond_to?(:map) && !value.acts_like?(:string) values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v } values.empty? ? nil : values else value = value.id_for_database if value.respond_to?(:id_for_database) value end end begin Arel::Nodes::BoundSqlLiteral.new("(#{statement})", nil, bound_values) rescue Arel::BindError => error raise ActiveRecord::PreparedStatementInvalid, error.message end end def build_bound_sql_literal(statement, values) bound_values = values.map do |value| if ActiveRecord::Relation === value Arel.sql(value.to_sql) elsif value.respond_to?(:map) && !value.acts_like?(:string) values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v } values.empty? ? nil : values else value = value.id_for_database if value.respond_to?(:id_for_database) value end end begin Arel::Nodes::BoundSqlLiteral.new("(#{statement})", bound_values, nil) rescue Arel::BindError => error raise ActiveRecord::PreparedStatementInvalid, error.message end end def lookup_table_klass_from_join_dependencies(table_name) each_join_dependencies do |join| return join.base_klass if table_name == join.table_name end nil end def each_join_dependencies(join_dependencies = build_join_dependencies, &block) join_dependencies.each do |join_dependency| join_dependency.each(&block) end end def build_join_dependencies joins = joins_values | left_outer_joins_values joins |= eager_load_values unless eager_load_values.empty? joins |= includes_values unless includes_values.empty? join_dependencies = [] join_dependencies.unshift construct_join_dependency( select_named_joins(joins, join_dependencies), nil ) end def assert_modifiable! raise UnmodifiableRelation if @loaded || @arel end def build_arel(connection, aliases = nil) arel = Arel::SelectManager.new(table) build_joins(arel.join_sources, aliases) arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? arel.take(build_cast_value("LIMIT", connection.sanitize_limit(limit_value))) if limit_value arel.skip(build_cast_value("OFFSET", offset_value.to_i)) if offset_value arel.group(*arel_columns(group_values.uniq)) unless group_values.empty? build_order(arel) build_with(arel) build_select(arel) arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty? arel.distinct(distinct_value) arel.from(build_from) unless from_clause.empty? arel.lock(lock_value) if lock_value unless annotate_values.empty? annotates = annotate_values annotates = annotates.uniq if annotates.size > 1 arel.comment(*annotates) end arel end def build_cast_value(name, value) ActiveModel::Attribute.with_cast_value(name, value, Type.default_value) end def build_from opts = from_clause.value name = from_clause.name case opts when Relation if opts.eager_loading? opts = opts.send(:apply_join_dependency) end name ||= "subquery" opts.arel.as(name.to_s) else opts end end def select_named_joins(join_names, stashed_joins = nil, &block) cte_joins, associations = join_names.partition do |join_name| Symbol === join_name && with_values.any? { _1.key?(join_name) } end cte_joins.each do |cte_name| block&.call(CTEJoin.new(cte_name)) end select_association_list(associations, stashed_joins, &block) end def select_association_list(associations, stashed_joins = nil) result = [] associations.each do |association| case association when Hash, Symbol, Array result << association when ActiveRecord::Associations::JoinDependency stashed_joins&.<< association else yield association if block_given? end end result end def build_join_buckets buckets = Hash.new { |h, k| h[k] = [] } unless left_outer_joins_values.empty? stashed_left_joins = [] left_joins = select_named_joins(left_outer_joins_values, stashed_left_joins) do |left_join| if left_join.is_a?(CTEJoin) buckets[:join_node] << build_with_join_node(left_join.name, Arel::Nodes::OuterJoin) else raise ArgumentError, "only Hash, Symbol and Array are allowed" end end if joins_values.empty? buckets[:named_join] = left_joins buckets[:stashed_join] = stashed_left_joins return buckets, Arel::Nodes::OuterJoin else stashed_left_joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin) end end joins = joins_values.dup if joins.last.is_a?(ActiveRecord::Associations::JoinDependency) stashed_eager_load = joins.pop if joins.last.base_klass == model end joins.each_with_index do |join, i| joins[i] = Arel::Nodes::StringJoin.new(Arel.sql(join.strip)) if join.is_a?(String) end while joins.first.is_a?(Arel::Nodes::Join) join_node = joins.shift if !join_node.is_a?(Arel::Nodes::LeadingJoin) && (stashed_eager_load || stashed_left_joins) buckets[:join_node] << join_node else buckets[:leading_join] << join_node end end buckets[:named_join] = select_named_joins(joins, buckets[:stashed_join]) do |join| if join.is_a?(Arel::Nodes::Join) buckets[:join_node] << join elsif join.is_a?(CTEJoin) buckets[:join_node] << build_with_join_node(join.name) else raise "unknown class: %s" % join.class.name end end buckets[:stashed_join].concat stashed_left_joins if stashed_left_joins buckets[:stashed_join] << stashed_eager_load if stashed_eager_load return buckets, Arel::Nodes::InnerJoin end def build_joins(join_sources, aliases = nil) return join_sources if joins_values.empty? && left_outer_joins_values.empty? buckets, join_type = build_join_buckets named_joins = buckets[:named_join] stashed_joins = buckets[:stashed_join] leading_joins = buckets[:leading_join] join_nodes = buckets[:join_node] join_sources.concat(leading_joins) unless leading_joins.empty? unless named_joins.empty? && stashed_joins.empty? alias_tracker = alias_tracker(leading_joins + join_nodes, aliases) join_dependency = construct_join_dependency(named_joins, join_type) join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker, references_values)) end join_sources.concat(join_nodes) unless join_nodes.empty? join_sources end def build_select(arel) if select_values.any? arel.project(*arel_columns(select_values)) elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements arel.project(*model.column_names.map { |field| table[field] }) else arel.project(table[Arel.star]) end end def build_with(arel) return if with_values.empty? with_statements = with_values.map do |with_value| build_with_value_from_hash(with_value) end @with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements) end def build_with_value_from_hash(hash) hash.map do |name, value| Arel::Nodes::TableAlias.new(build_with_expression_from_value(value), name) end end def build_with_expression_from_value(value, nested = false) case value when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value) when ActiveRecord::Relation if nested value.arel.ast else value.arel end when Arel::SelectManager then value when Array return build_with_expression_from_value(value.first, false) if value.size == 1 parts = value.map do |query| build_with_expression_from_value(query, true) end parts.reduce do |result, value| Arel::Nodes::UnionAll.new(result, value) end else raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}" end end def build_with_join_node(name, kind = Arel::Nodes::InnerJoin) with_table = Arel::Table.new(name) table.join(with_table, kind).on( with_table[model.model_name.to_s.foreign_key].eq(table[model.primary_key]) ).join_sources.first end def arel_columns_from_hash(fields) fields.flat_map do |table_name, columns| table_name = table_name.name if table_name.is_a?(Symbol) case columns when Symbol, String arel_column_with_table(table_name, columns) when Array columns.map do |column| arel_column_with_table(table_name, column) end else raise TypeError, "Expected Symbol, String or Array, got: #{columns.class}" end end end def arel_column_with_table(table_name, column_name) self.references_values |= [Arel.sql(table_name, retryable: true)] if column_name.is_a?(Symbol) || !column_name.match?(/\W/) predicate_builder.resolve_arel_attribute(table_name, column_name) do lookup_table_klass_from_join_dependencies(table_name) end else Arel.sql("#{model.adapter_class.quote_table_name(table_name)}.#{column_name}") end end def arel_column(field) field = field.name if is_symbol = field.is_a?(Symbol) field = model.attribute_aliases[field] || field.to_s from = from_clause.name || from_clause.value if model.columns_hash.key?(field) && (!from || table_name_matches?(from)) table[field] elsif /\A(?(?:\w+\.)?\w+)\.(?\w+)\z/ =~ field arel_column_with_table(table, column) elsif block_given? yield field elsif Arel.arel_node?(field) field else Arel.sql(is_symbol ? model.adapter_class.quote_table_name(field) : field) end end def table_name_matches?(from) table_name = Regexp.escape(table.name) quoted_table_name = Regexp.escape(model.adapter_class.quote_table_name(table.name)) /(?:\A|(? v } } end end STRUCTURAL_VALUE_METHODS = ( Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references, :annotate, :optimizer_hints] ).freeze # :nodoc: def structurally_incompatible_values_for(other) values = other.values STRUCTURAL_VALUE_METHODS.reject do |method| v1, v2 = @values[method], values[method] if v1.is_a?(Array) next true unless v2.is_a?(Array) v1 = v1.uniq v2 = v2.uniq end v1 == v2 end end end end