diff options
Diffstat (limited to 'activerecord/lib/active_record/relation')
6 files changed, 450 insertions, 321 deletions
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index d7494ebb5a..bf5a60f458 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/object/blank' module ActiveRecord - module Batches # :nodoc: + module Batches # Yields each record that was found by the find +options+. The find is # performed by find_in_batches with a batch size of 1000 (or as # specified by the <tt>:batch_size</tt> option). @@ -39,7 +39,7 @@ module ActiveRecord # ascending on the primary key ("id ASC") to make the batch ordering # work. This also mean that this method only works with integer-based # primary keys. You can't set the limit either, that's used to control - # the the batch sizes. + # the batch sizes. # # Example: # @@ -50,7 +50,7 @@ module ActiveRecord def find_in_batches(options = {}) relation = self - if orders.present? || taken.present? + unless arel.orders.blank? && arel.taken.blank? ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end @@ -65,7 +65,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.except(:order).order(batch_order).limit(batch_size) - records = relation.where(primary_key.gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).all while records.any? yield records @@ -73,7 +73,7 @@ module ActiveRecord break if records.size < batch_size if primary_key_offset = records.last.id - records = relation.where(primary_key.gt(primary_key_offset)).to_a + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a else raise "Primary key not included in the custom select clause" end @@ -83,7 +83,7 @@ module ActiveRecord private def batch_order - "#{@klass.table_name}.#{@klass.primary_key} ASC" + "#{table_name}.#{primary_key} ASC" end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index a679c444cf..c1842b1a96 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -6,32 +6,30 @@ module ActiveRecord # Count operates using three different approaches. # # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. - # * Count using column: By passing a column name to count, it will return a count of all the - # rows for the model with supplied column present + # * Count using column: By passing a column name to count, it will return a count of all the + # rows for the model with supplied column present. # * Count using options will find the row count matched by the options used. # # The third approach, count using options, accepts an option hash as the only parameter. The options are: # - # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. + # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. # See conditions in the intro to ActiveRecord::Base. - # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed) - # or named associations in the same form used for the <tt>:include</tt> option, which will - # perform an INNER JOIN on the associated table(s). - # If the value is a string, then the records will be returned read-only since they will have - # attributes that do not correspond to the table's columns. + # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" + # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will + # perform an INNER JOIN on the associated table(s). If the value is a string, then the records + # will be returned read-only since they will have attributes that do not correspond to the table's columns. # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. - # The symbols named refer to already defined associations. When using named associations, count + # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. + # The symbols named refer to already defined associations. When using named associations, count # returns the number of DISTINCT items for the model you're counting. # See eager loading under Associations. # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, - # want to do a join but not - # include the joined columns. - # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as + # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, + # want to do a join but not include the joined columns. + # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as # SELECT COUNT(DISTINCT posts.id) ... - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an # alternate table name (or even the name of a database view). # # Examples for counting all: @@ -44,16 +42,16 @@ module ActiveRecord # Person.count(:conditions => "age > 26") # # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # # # finds the number of rows matching the conditions and joins. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", - # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") + # Person.count(:conditions => "age > 26 AND job.salary > 60000", + # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') # - # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. + # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. # Use Person.count instead. def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) @@ -95,14 +93,14 @@ module ActiveRecord calculate(:sum, column_name, options) end - # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, + # This calculates aggregate values in the given column. Methods for count, sum, average, + # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. # # There are two basic forms of output: - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float + # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float # for AVG, and the given column's type for everything else. - # * Grouped values: This returns an ordered hash of the values and groups them by the + # * Grouped values: This returns an ordered hash of the values and groups them by the # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. # # values = Person.maximum(:age, :group => 'last_name') @@ -119,29 +117,29 @@ module ActiveRecord # end # # Options: - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. # See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, + # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. - # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". + # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". # (Rarely needed). - # The records will be returned read-only since they will have attributes that do not correspond to the + # The records will be returned read-only since they will have attributes that do not correspond to the # table's columns. # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example + # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example # want to do a join, but not include the joined columns. - # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as + # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as # SELECT COUNT(DISTINCT posts.id) ... # # Examples: # Person.calculate(:count, :all) # The same as Person.count # Person.average(:age) # SELECT AVG(age) FROM people... - # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for + # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for # # everyone with a last name other than 'Drake' # # # Selects the minimum age for any family without any minors - # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) + # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) @@ -163,80 +161,112 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase + distinct = nil + if operation == "count" column_name ||= (select_for_count || :all) - joins = arel.joins(arel) - if joins.present? && joins =~ /LEFT OUTER/i + unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true - column_name = @klass.primary_key if column_name == :all + column_name = primary_key if column_name == :all end - distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i - distinct ||= options[:distinct] - else - distinct = nil + distinct = nil if column_name =~ /\s*DISTINCT\s+/i end distinct = options[:distinct] || distinct - column_name = :all if column_name.blank? && operation == "count" if @group_values.any? - return execute_grouped_calculation(operation, column_name) + execute_grouped_calculation(operation, column_name, distinct) else - return execute_simple_calculation(operation, column_name, distinct) + execute_simple_calculation(operation, column_name, distinct) end end - def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - column = if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped, column_name) + def aggregate_column(column_name) + if @klass.column_names.include?(column_name.to_s) + Arel::Attribute.new(@klass.unscoped.table, column_name) else - Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) + Arel.sql(column_name == :all ? "*" : column_name.to_s) end + end - # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = except(:order).select(operation == 'count' ? column.count(distinct) : column.send(operation)) - type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation) + def operation_over_aggregate_column(column, operation, distinct) + operation == 'count' ? column.count(distinct) : column.send(operation) end - def execute_grouped_calculation(operation, column_name) #:nodoc: - 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 - group_alias = column_alias_for(group_field) - group_column = column_for(group_field) + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: + column = aggregate_column(column_name) - group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field + # Postgresql doesn't like ORDER BY when there are no GROUP BY + relation = except(:order) + select_value = operation_over_aggregate_column(column, operation, distinct) + + relation.select_values = [select_value] - aggregate_alias = column_alias_for(operation, column_name) + query_builder = relation.arel - select_statement = if operation == 'count' && column_name == :all - "COUNT(*) AS count_all" + if operation == "count" + limit = relation.limit_value + offset = relation.offset_value + + unless limit && offset + query_builder.limit = nil + query_builder.offset = nil + end + end + + type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation) + end + + def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: + group_attr = @group_values + association = @klass.reflect_on_association(group_attr.first.to_sym) + associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attr) + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields).map { |aliaz,field| + [aliaz, column_for(field)] + } + + group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields + + if operation == 'count' && column_name == :all + aggregate_alias = 'count_all' else - Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql + aggregate_alias = column_alias_for(operation, column_name) end - select_statement << ", #{group_field} AS #{group_alias}" + select_values = [ + operation_over_aggregate_column( + aggregate_column(column_name), + operation, + distinct).as(aggregate_alias) + ] + + select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| + "#{field} AS #{aliaz}" + } - relation = except(:group).select(select_statement).group(group) + relation = except(:group).group(group.join(',')) + relation.select_values = select_values calculated_data = @klass.connection.select_all(relation.to_sql) if association - key_ids = calculated_data.collect { |row| row[group_alias] } + key_ids = calculated_data.collect { |row| row[group_aliases.first] } key_records = association.klass.base_class.find(key_ids) - key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + key_records = Hash[key_records.map { |r| [r.id, r] }] end - calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| - key = type_cast_calculated_value(row[group_alias], group_column) - key = key_records[key] if associated - value = row[aggregate_alias] - all[key] = type_cast_calculated_value(value, column_for(column_name), operation) - all - end + ActiveSupport::OrderedHash[calculated_data.map do |row| + key = group_columns.map { |aliaz, column| + type_cast_calculated_value(row[aliaz], column) + } + key = key.first if key.size == 1 + key = key_records[key] if associated + [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] + end] end # Converts the given keys to the value that the database adapter returns as @@ -264,15 +294,11 @@ module ActiveRecord end def type_cast_calculated_value(value, column, operation = nil) - if value.is_a?(String) || value.nil? - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value.try(:to_d) - else type_cast_using_column(value, column) - end - else - value + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value.respond_to?(:to_d) ? value.to_d : value + else type_cast_using_column(value, column) end end @@ -282,7 +308,7 @@ module ActiveRecord def select_for_count if @select_values.present? - select = @select_values.join(", ") + select = @select_values.join(", ") select if select !~ /(,|\*)/ end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b34c11973b..25e23a9d55 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -19,29 +19,29 @@ module ActiveRecord # # All approaches accept an options hash as their last parameter. # - # ==== Parameters + # ==== Options # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>, # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a + # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a # <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, + # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, # it would skip rows 0 through 4. # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), - # named associations in the same form used for the <tt>:include</tt> option, which will perform an + # named associations in the same form used for the <tt>:include</tt> option, which will perform an # <tt>INNER JOIN</tt> on the associated table(s), # or an array containing a mixture of both strings and named associations. - # If the value is a string, then the records will be returned read-only since they will + # If the value is a string, then the records will be returned read-only since they will # have attributes that do not correspond to the table's columns. # Pass <tt>:readonly => false</tt> to override. # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer # to already defined associations. See eager loading under Associations. - # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, + # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed # to an alternate table name (or even the name of a database view). # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". @@ -54,7 +54,7 @@ module ActiveRecord # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) # Person.find([1]) # returns an array for the object with ID = 1 - # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") + # Person.where("administrator = 1").order("created_on DESC").find(1) # # Note that returned records may not be in the same order as the ids you # provide since database rows are unordered. Give an explicit <tt>:order</tt> @@ -63,23 +63,23 @@ module ActiveRecord # ==== Examples # # # find first - # Person.find(:first) # returns the first object fetched by SELECT * FROM people - # Person.find(:first, :conditions => [ "user_name = ?", user_name]) - # Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }]) - # Person.find(:first, :order => "created_on DESC", :offset => 5) + # Person.first # returns the first object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).first + # Person.where(["user_name = :u", { :u => user_name }]).first + # Person.order("created_on DESC").offset(5).first # # # find last - # Person.find(:last) # returns the last object fetched by SELECT * FROM people - # Person.find(:last, :conditions => [ "user_name = ?", user_name]) - # Person.find(:last, :order => "created_on DESC", :offset => 5) + # Person.last # returns the last object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).last + # Person.order("created_on DESC").offset(5).last # # # find all - # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people - # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) - # Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] } - # Person.find(:all, :offset => 10, :limit => 10) - # Person.find(:all, :include => [ :account, :friends ]) - # Person.find(:all, :group => "category") + # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people + # Person.where(["category IN (?)", categories]).limit(50).all + # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all + # Person.offset(10).limit(10).all + # Person.includes([:account, :friends]).all + # Person.group("category").all # # Example for find with a lock: Imagine two concurrent transactions: # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting @@ -88,7 +88,7 @@ module ActiveRecord # expected <tt>person.visits == 4</tt>. # # Person.transaction do - # person = Person.find(1, :lock => true) + # person = Person.lock(true).find(1) # person.visits += 1 # person.save! # end @@ -123,6 +123,11 @@ module ActiveRecord end end + # Same as #first! but raises RecordNotFound if no record is returned + def first!(*args) + self.first(*args) or raise RecordNotFound + end + # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the # same arguments to this method as you can to <tt>find(:last)</tt>. def last(*args) @@ -137,6 +142,11 @@ module ActiveRecord end end + # Same as #last! but raises RecordNotFound if no record is returned + def last!(*args) + self.last(*args) or raise RecordNotFound + end + # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the # same arguments to this method as you can to <tt>find(:all)</tt>. def all(*args) @@ -171,22 +181,25 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id + relation = select("1").limit(1) + case id when Array, Hash - where(id).exists? + relation = relation.where(id) else - relation = select(primary_key).limit(1) - relation = relation.where(primary_key.eq(id)) if id - relation.first ? true : false + relation = relation.where(table[primary_key].eq(id)) if id end + + relation.first ? true : false end protected def find_with_associations including = (@eager_load_values + @includes_values).uniq - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil) - rows = construct_relation_for_association_find(join_dependency).to_a + join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, []) + relation = construct_relation_for_association_find(join_dependency) + rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] @@ -194,13 +207,13 @@ module ActiveRecord def construct_relation_for_association_calculations including = (@eager_load_values + @includes_values).uniq - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel)) + join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first) relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) end def construct_relation_for_association_find(join_dependency) - relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency)) + relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns) apply_join_dependency(relation, join_dependency) end @@ -222,15 +235,15 @@ module ActiveRecord end def construct_limited_ids_condition(relation) - orders = relation.order_values.join(", ") - values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders) + orders = relation.order_values + values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) - ids_array = relation.select(values).collect {|row| row[@klass.primary_key]} - ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array) + ids_array = relation.select(values).collect {|row| row[primary_key]} + ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end def find_by_attributes(match, attributes, *args) - conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h} + conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] result = where(conditions).send(match.finder) if match.bang? && result.blank? @@ -288,19 +301,24 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - record = where(primary_key.eq(id)).first + column = columns_hash[primary_key] + + substitute = connection.substitute_for(column, @bind_values) + relation = where(table[primary_key].eq(substitute)) + relation.bind_values = [[column, id]] + record = relation.first unless record - conditions = arel.send(:where_clauses).join(', ') - conditions = " [WHERE #{conditions}]" if conditions.present? - raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" + conditions = arel.where_sql + conditions = " [#{conditions}]" if conditions + raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}" end record end def find_some(ids) - result = where(primary_key.in(ids)).all + result = where(table[primary_key].in(ids)).all expected_size = if @limit_value && ids.size > @limit_value @@ -317,7 +335,7 @@ module ActiveRecord if result.size == expected_size result else - conditions = arel.send(:where_clauses).join(', ') + conditions = arel.wheres.map { |x| x.value }.join(', ') conditions = " [WHERE #{conditions}]" if conditions.present? error = "Couldn't find all #{@klass.name.pluralize} with IDs " @@ -342,14 +360,8 @@ module ActiveRecord end end - def column_aliases(join_dependency) - join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| - "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") - end - def using_limitable_reflections?(reflections) - reflections.none?(&:collection?) + reflections.none? { |r| r.collection? } end - end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 5cea2328e8..982b3d7e9f 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,33 +1,38 @@ module ActiveRecord - class PredicateBuilder - - def initialize(engine) - @engine = engine - end - - def build_from_hash(attributes, default_table) + class PredicateBuilder # :nodoc: + def self.build_from_hash(engine, attributes, default_table) predicates = attributes.map do |column, value| table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, :engine => @engine) - build_from_hash(value, table) + table = Arel::Table.new(column, engine) + build_from_hash(engine, value, table) else column = column.to_s if column.include?('.') table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, :engine => @engine) + table = Arel::Table.new(table_name, engine) end - attribute = table[column] || Arel::Attribute.new(table, column) + attribute = table[column.to_sym] case value - when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation - values = value.to_a + when ActiveRecord::Relation + value.select_values = [value.klass.arel_table['id']] if value.select_values.empty? + attribute.in(value.arel.ast) + when Array, ActiveRecord::Associations::CollectionProxy + values = value.to_a.map { |x| + x.is_a?(ActiveRecord::Base) ? x.id : x + } attribute.in(values) when Range, Arel::Relation attribute.in(value) + when ActiveRecord::Base + attribute.eq(value.id) + when Class + # FIXME: I think we need to deprecate this behavior + attribute.eq(value.name) else attribute.eq(value) end @@ -36,6 +41,5 @@ module ActiveRecord predicates.flatten end - end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index e71f1cca72..9470e7c6c5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,247 +6,299 @@ module ActiveRecord extend ActiveSupport::Concern attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, + :select_values, :group_values, :order_values, :joins_values, + :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value def includes(*args) - args.reject! { |a| a.blank? } - clone.tap {|r| r.includes_values += args if args.present? } + args.reject! {|a| a.blank? } + + return self if args.empty? + + relation = clone + relation.includes_values = (relation.includes_values + args).flatten.uniq + relation end def eager_load(*args) - clone.tap {|r| r.eager_load_values += args if args.present? } + return self if args.blank? + + relation = clone + relation.eager_load_values += args + relation end def preload(*args) - clone.tap {|r| r.preload_values += args if args.present? } + return self if args.blank? + + relation = clone + relation.preload_values += args + relation end - def select(*args) + def select(value = Proc.new) if block_given? - to_a.select {|*block_args| yield(*block_args) } + to_a.select {|*block_args| value.call(*block_args) } else - clone.tap {|r| r.select_values += args if args.present? } + relation = clone + relation.select_values += Array.wrap(value) + relation end end def group(*args) - clone.tap {|r| r.group_values += args.flatten if args.present? } + return self if args.blank? + + relation = clone + relation.group_values += args.flatten + relation end def order(*args) - clone.tap {|r| r.order_values += args if args.present? } - end + return self if args.blank? - def reorder(*args) - clone.tap {|r| r.order_values = args if args.present? } + relation = clone + relation.order_values += args.flatten + relation end def joins(*args) + return self if args.compact.blank? + + relation = clone + args.flatten! - clone.tap {|r| r.joins_values += args if args.present? } + relation.joins_values += args + + relation + end + + def bind(value) + relation = clone + relation.bind_values += [value] + relation end def where(opts, *rest) - value = build_where(opts, rest) - value ? clone.tap {|r| r.where_values += Array.wrap(value) } : clone + return self if opts.blank? + + relation = clone + relation.where_values += build_where(opts, rest) + relation end def having(*args) - value = build_where(*args) - clone.tap {|r| r.having_values += Array.wrap(value) if value.present? } + return self if args.blank? + + relation = clone + relation.having_values += build_where(*args) + relation end - def limit(value = true) - clone.tap {|r| r.limit_value = value } + def limit(value) + relation = clone + relation.limit_value = value + relation end - def offset(value = true) - clone.tap {|r| r.offset_value = value } + def offset(value) + relation = clone + relation.offset_value = value + relation end def lock(locks = true) + relation = clone + case locks when String, TrueClass, NilClass - clone.tap {|r| r.lock_value = locks || true } + relation.lock_value = locks || true else - clone.tap {|r| r.lock_value = false } + relation.lock_value = false end + + relation end def readonly(value = true) - clone.tap {|r| r.readonly_value = value } + relation = clone + relation.readonly_value = value + relation end - def create_with(value = true) - clone.tap {|r| r.create_with_value = value } + def create_with(value) + relation = clone + relation.create_with_value = value && (@create_with_value || {}).merge(value) + relation end - def from(value = true) - clone.tap {|r| r.from_value = value } + def from(value) + relation = clone + relation.from_value = value + relation end - def extending(*modules, &block) - modules << Module.new(&block) if block_given? - clone.tap {|r| r.send(:apply_modules, *modules) } + def extending(*modules) + modules << Module.new(&Proc.new) if block_given? + + return self if modules.empty? + + relation = clone + relation.send(:apply_modules, modules.flatten) + relation end def reverse_order - order_clause = arel.send(:order_clauses).join(', ') - relation = except(:order) + order_clause = arel.order_clauses - unless order_clauses.blank? - relation.order(reverse_sql_order(order_clause)) - else - relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC") - end + order = order_clause.empty? ? + "#{table_name}.#{primary_key} DESC" : + reverse_sql_order(order_clause).join(', ') + + except(:order).order(Arel.sql(order)) end def arel @arel ||= build_arel end - def custom_join_sql(*joins) - arel = table - joins.each do |join| - next if join.blank? + def build_arel + arel = table.from table - @implicit_readonly = true + build_joins(arel, @joins_values) unless @joins_values.empty? - case join - when Hash, Array, Symbol - if array_of_strings?(join) - join_string = join.join(' ') - arel = arel.join(join_string) - end - else - arel = arel.join(join) - end - end - arel.joins(arel) - end + collapse_wheres(arel, (@where_values - ['']).uniq) - def build_arel - arel = table + arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? - arel = build_joins(arel, @joins_values) unless @joins_values.empty? + arel.take(connection.sanitize_limit(@limit_value)) if @limit_value + arel.skip(@offset_value) if @offset_value - @where_values.uniq.each do |where| - next if where.blank? + arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? - case where - when Arel::SqlLiteral - arel = arel.where(where) - else - sql = where.is_a?(String) ? where : where.to_sql - arel = arel.where(Arel::SqlLiteral.new("(#{sql})")) - end - end + arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty? - arel = arel.having(*@having_values.uniq.select{|h| h.present?}) unless @having_values.empty? + build_select(arel, @select_values.uniq) - arel = arel.take(@limit_value) if @limit_value - arel = arel.skip(@offset_value) if @offset_value + arel.from(@from_value) if @from_value + arel.lock(@lock_value) if @lock_value - arel = arel.group(*@group_values.uniq.select{|g| g.present?}) unless @group_values.empty? + arel + end - arel = arel.order(*@order_values.uniq.select{|o| o.present?}) unless @order_values.empty? + private - arel = build_select(arel, @select_values.uniq) + def custom_join_ast(table, joins) + joins = joins.reject { |join| join.blank? } - arel = arel.from(@from_value) if @from_value - arel = arel.lock(@lock_value) if @lock_value + return [] if joins.empty? - arel + @implicit_readonly = true + + joins.map do |join| + case join + when Array + join = Arel.sql(join.join(' ')) if array_of_strings?(join) + when String + join = Arel.sql(join) + end + table.create_string_join(join) + end + 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| + where = Arel.sql(where) if String === where + arel.where(Arel::Nodes::Grouping.new(where)) + end end def build_where(opts, other = []) case opts when String, Array - @klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other)) + [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - PredicateBuilder.new(table.engine).build_from_hash(attributes, table) + PredicateBuilder.build_from_hash(table.engine, attributes, table) else - opts + [opts] end end - private - - def build_joins(relation, joins) - joined_associations = [] - association_joins = [] - - joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq - - joins.each do |join| - association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) + def build_joins(manager, joins) + buckets = joins.group_by do |join| + case join + when String + 'string_join' + when Hash, Symbol, Array + 'association_join' + when ActiveRecord::Associations::JoinDependency::JoinAssociation + 'stashed_join' + when Arel::Nodes::Join + 'join_node' + else + raise 'unknown class: %s' % join.class.name + end end - stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)} + association_joins = buckets['association_join'] || [] + stashed_association_joins = buckets['stashed_join'] || [] + join_nodes = buckets['join_node'] || [] + string_joins = (buckets['string_join'] || []).map { |x| + x.strip + }.uniq - non_association_joins = (joins - association_joins - stashed_association_joins) - custom_joins = custom_join_sql(*non_association_joins) + join_list = custom_join_ast(manager, string_joins) - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins) + join_dependency = ActiveRecord::Associations::JoinDependency.new( + @klass, + association_joins, + join_list + ) + + join_nodes.each do |join| + join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase) + end join_dependency.graft(*stashed_association_joins) @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - to_join = [] - + # FIXME: refactor this to build an AST join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.join_class, association.association_join.first] - to_join << [association_relation.last, association.join_class, association.association_join.last] - else - to_join << [association_relation, association.join_class, association.association_join] - end + association.join_to(manager) end - to_join.each do |tj| - unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] } - joined_associations << tj - relation = relation.join(tj[0], tj[1]).on(*tj[2]) - end - end + manager.join_sources.concat join_nodes.uniq + manager.join_sources.concat join_list - relation.join(custom_joins) + manager end def build_select(arel, selects) - if selects.present? + unless selects.empty? @implicit_readonly = false - # TODO: fix this ugly hack, we should refactor the callers to get an ARel compatible array. - # Before this change we were passing to ARel the last element only, and ARel is capable of handling an array - if selects.all? {|s| s.is_a?(String) || !s.is_a?(Arel::Expression) } && !(selects.last =~ /^COUNT\(/) - arel.project(*selects) - else - arel.project(selects.last) - end + arel.project(*selects) else - arel.project(@klass.quoted_table_name + '.*') + arel.project(@klass.arel_table[Arel.star]) end end def apply_modules(modules) - values = Array.wrap(modules) - @extensions += values if values.present? - values.each {|extension| extend(extension) } + unless modules.empty? + @extensions += modules + modules.each {|extension| extend(extension) } + end end def reverse_sql_order(order_query) - order_query.split(',').each { |s| - if s.match(/\s(asc|ASC)$/) - s.gsub!(/\s(asc|ASC)$/, ' DESC') - elsif s.match(/\s(desc|DESC)$/) - s.gsub!(/\s(desc|DESC)$/, ' ASC') - else - s.concat(' DESC') - end - }.join(',') + order_query.join(', ').split(',').collect do |s| + s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') + end end def array_of_strings?(o) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 7712ad2569..128e0fbd86 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -3,71 +3,109 @@ require 'active_support/core_ext/object/blank' module ActiveRecord module SpawnMethods def merge(r) + return self unless r + return to_a & r if r.is_a?(Array) + merged_relation = clone - return merged_relation unless r - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - [:joins, :where]).each do |method| + Relation::ASSOCIATION_METHODS.each do |method| value = r.send(:"#{method}_values") - merged_relation.send(:"#{method}_values=", value) if value.present? - end - - merged_relation = merged_relation.joins(r.joins_values) - merged_wheres = @where_values - - r.where_values.each do |w| - if w.is_a?(Arel::Predicates::Equality) - merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name } + unless value.empty? + if method == :includes + merged_relation = merged_relation.includes(value) + else + merged_relation.send(:"#{method}_values=", value) + end end + end - merged_wheres += [w] + (Relation::MULTI_VALUE_METHODS - [:joins, :where]).each do |method| + value = r.send(:"#{method}_values") + merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present? + end + + merged_relation.joins_values += r.joins_values + + merged_wheres = @where_values + r.where_values + + unless @where_values.empty? + # Remove duplicates, last one wins. + seen = Hash.new { |h,table| h[table] = {} } + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false + if w.respond_to?(:operator) && w.operator == :== + name = w.left.name + table = w.left.relation.name + nuke = seen[table][name] + seen[table][name] = true + end + nuke + }.reverse end merged_relation.where_values = merged_wheres - Relation::SINGLE_VALUE_METHODS.reject {|m| m == :lock}.each do |method| - unless (value = r.send(:"#{method}_value")).nil? - merged_relation.send(:"#{method}_value=", value) - end + (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method| + value = r.send(:"#{method}_value") + merged_relation.send(:"#{method}_value=", value) unless value.nil? end merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value + # Apply scope extension modules merged_relation.send :apply_modules, r.extensions merged_relation end - alias :& :merge - + # Removes from the query the condition(s) specified in +skips+. + # + # Example: + # + # Post.order('id asc').except(:order) # discards the order condition + # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order + # def except(*skips) result = self.class.new(@klass, table) - (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) unless skips.include?(method) + ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| + result.send(:"#{method}_values=", send(:"#{method}_values")) end - Relation::SINGLE_VALUE_METHODS.each do |method| - result.send(:"#{method}_value=", send(:"#{method}_value")) unless skips.include?(method) + (Relation::SINGLE_VALUE_METHODS - skips).each do |method| + result.send(:"#{method}_value=", send(:"#{method}_value")) end + # Apply scope extension modules + result.send(:apply_modules, extensions) + result end + # Removes any condition from the query other than the one(s) specified in +onlies+. + # + # Example: + # + # Post.order('id asc').only(:where) # discards the order condition + # Post.order('id asc').only(:where, :order) # uses the specified order + # def only(*onlies) result = self.class.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 + ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method| + result.send(:"#{method}_values=", send(:"#{method}_values")) + end + + (Relation::SINGLE_VALUE_METHODS & onlies).each do |method| + result.send(:"#{method}_value=", send(:"#{method}_value")) end + # Apply scope extension modules + result.send(:apply_modules, extensions) + result end @@ -79,19 +117,16 @@ module ActiveRecord return relation unless options options.assert_valid_keys(VALID_FIND_OPTIONS) + finders = options.dup + finders.delete_if { |key, value| value.nil? && key != :limit } - [:joins, :select, :group, :having, :limit, :offset, :from, :lock, :readonly].each do |finder| - relation = relation.send(finder, options[finder]) if options.has_key?(finder) - end - - # Give precedence to newly-applied orders and groups to play nicely with with_scope - [:group, :order].each do |finder| - relation.send("#{finder}_values=", Array.wrap(options[finder]) + relation.send("#{finder}_values")) if options.has_key?(finder) + ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| + relation = relation.send(finder, finders[finder]) end - relation = relation.where(options[:conditions]) if options.has_key?(:conditions) - relation = relation.includes(options[:include]) if options.has_key?(:include) - relation = relation.extending(options[:extend]) if options.has_key?(:extend) + relation = relation.where(finders[:conditions]) if options.has_key?(:conditions) + relation = relation.includes(finders[:include]) if options.has_key?(:include) + relation = relation.extending(finders[:extend]) if options.has_key?(:extend) relation end |