2006-02-25 18:06:04 -05:00
module ActiveRecord
2006-03-27 22:06:40 -05:00
module Calculations #:nodoc:
2006-04-25 01:25:04 -04:00
CALCULATIONS_OPTIONS = [ :conditions , :joins , :order , :select , :group , :having , :distinct , :limit , :offset , :include ]
2006-02-25 18:06:04 -05:00
def self . included ( base )
base . extend ( ClassMethods )
end
module ClassMethods
# 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.
2006-09-26 13:02:45 -04:00
# * Count by conditions or joins: This API has been deprecated and will be removed in Rails 2.0
2006-02-25 18:06:04 -05:00
# * Count using options will find the row count matched by the options used.
#
# The last 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 ]. See conditions in the intro.
# * <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 table's columns.
# * <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 DISTINCT items for the model you're counting.
# See eager loading under Associations.
2006-03-01 11:25:14 -05:00
# * <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 SELECT COUNT(DISTINCT posts.id) ...
2006-02-25 18:06:04 -05:00
#
# Examples for counting all:
# Person.count # returns the total count of all people
#
2006-09-26 13:02:45 -04:00
# Examples for count by +conditions+ and +joins+ (this has been deprecated):
2006-02-25 18:06:04 -05:00
# Person.count("age > 26") # returns the number of people older than 26
# Person.find("age > 26 AND job.salary > 60000", "LEFT JOIN jobs on jobs.person_id = person.id") # returns the total number of rows matching the conditions and joins fetched by SELECT COUNT(*).
#
# Examples for count with options:
# Person.count(:conditions => "age > 26")
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
# 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: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead.
def count ( * args )
2006-04-25 01:25:04 -04:00
calculate ( :count , * construct_count_options_from_legacy_args ( * args ) )
2006-02-25 18:06:04 -05:00
end
# Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options.
#
# Person.average('age')
def average ( column_name , options = { } )
calculate ( :avg , column_name , options )
end
# Calculates the minimum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options.
#
# Person.minimum('age')
def minimum ( column_name , options = { } )
calculate ( :min , column_name , options )
end
# Calculates the maximum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options.
#
# Person.maximum('age')
def maximum ( column_name , options = { } )
calculate ( :max , column_name , options )
end
# Calculates the sum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options.
#
2006-03-01 21:53:57 -05:00
# Person.sum('age')
2006-02-25 18:06:04 -05:00
def sum ( column_name , options = { } )
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 :conditions, :order, :group, :having, and :joins 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 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 :group option. It takes either a column name, or the name
# of a belongs_to association.
#
# values = Person.maximum(:age, :group => 'last_name')
# puts values["Drake"]
# => 43
#
# drake = Family.find_by_last_name('Drake')
# values = Person.maximum(:age, :group => :family) # Person belongs_to :family
# puts values[drake]
# => 43
#
# values.each do |family, max_age|
# ...
# end
#
2006-03-01 11:25:14 -05:00
# Options:
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <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 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 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) ...
#
2006-02-25 18:06:04 -05:00
# 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 everyone with a last name other than 'Drake'
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
def calculate ( operation , column_name , options = { } )
2006-03-01 11:25:14 -05:00
validate_calculation_options ( operation , options )
column_name = options [ :select ] if options [ :select ]
2006-02-25 18:41:51 -05:00
column_name = '*' if column_name == :all
2006-03-01 11:25:14 -05:00
column = column_for column_name
2006-04-25 01:25:04 -04:00
catch :invalid_query do
if options [ :group ]
return execute_grouped_calculation ( operation , column_name , column , options )
else
return execute_simple_calculation ( operation , column_name , column , options )
end
2006-02-25 18:06:04 -05:00
end
2006-04-25 01:25:04 -04:00
0
2006-02-25 18:06:04 -05:00
end
protected
2006-04-19 17:37:54 -04:00
def construct_count_options_from_legacy_args ( * args )
options = { }
column_name = :all
2006-09-26 13:02:45 -04:00
# We need to handle
# count()
# count(options={})
# count(column_name=:all, options={})
# count(conditions=nil, joins=nil) # deprecated
if args . size > 2
raise ArgumentError , " Unexpected parameters passed to count(options={}): #{ args . inspect } "
elsif args . size > 0
if args [ 0 ] . is_a? ( Hash )
options = args [ 0 ]
2006-04-19 17:37:54 -04:00
elsif args [ 1 ] . is_a? ( Hash )
2006-09-26 13:02:45 -04:00
column_name , options = args
2006-04-19 17:37:54 -04:00
else
2006-09-26 13:02:45 -04:00
# Deprecated count(conditions, joins=nil)
ActiveSupport :: Deprecation . warn (
2006-09-29 03:48:16 -04:00
" You called count( #{ args [ 0 ] . inspect } , #{ args [ 1 ] . inspect } ), which is a deprecated API call. " +
" Instead you should use count(column_name, options). Passing the conditions and joins as " +
" string parameters will be removed in Rails 2.0. " , caller ( 2 )
2006-09-26 13:02:45 -04:00
)
options . merge! ( :conditions = > args [ 0 ] )
options . merge! ( :joins = > args [ 1 ] ) if args [ 1 ]
2006-04-19 17:37:54 -04:00
end
end
2006-09-26 13:02:45 -04:00
2006-04-19 17:37:54 -04:00
[ column_name , options ]
end
2006-09-26 13:02:45 -04:00
2006-04-25 01:25:04 -04:00
def construct_calculation_sql ( operation , column_name , options ) #:nodoc:
scope = scope ( :find )
merged_includes = merge_includes ( scope ? scope [ :include ] : [ ] , options [ :include ] )
aggregate_alias = column_alias_for ( operation , column_name )
use_workaround = ! Base . connection . supports_count_distinct? && options [ :distinct ] && operation . to_s . downcase == 'count'
join_dependency = nil
if merged_includes . any? && operation . to_s . downcase == 'count'
options [ :distinct ] = true
2006-06-03 17:19:36 -04:00
column_name = options [ :select ] || [ table_name , primary_key ] * '.'
2006-04-25 01:25:04 -04:00
end
sql = " SELECT #{ operation } ( #{ 'DISTINCT ' if options [ :distinct ] } #{ column_name } ) AS #{ aggregate_alias } "
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
sql = " SELECT COUNT(*) AS #{ aggregate_alias } " if use_workaround
2006-03-27 22:06:40 -05:00
sql << " , #{ options [ :group_field ] } AS #{ options [ :group_alias ] } " if options [ :group ]
2006-04-25 01:25:04 -04:00
sql << " FROM (SELECT DISTINCT #{ column_name } " if use_workaround
2006-03-27 22:06:40 -05:00
sql << " FROM #{ table_name } "
2006-04-25 01:25:04 -04:00
if merged_includes . any?
join_dependency = ActiveRecord :: Associations :: ClassMethods :: JoinDependency . new ( self , merged_includes , options [ :joins ] )
sql << join_dependency . join_associations . collect { | join | join . association_join } . join
end
2006-03-27 22:06:40 -05:00
add_joins! ( sql , options , scope )
add_conditions! ( sql , options [ :conditions ] , scope )
2006-04-25 01:25:04 -04:00
add_limited_ids_condition! ( sql , options , join_dependency ) if join_dependency && ! using_limitable_reflections? ( join_dependency . reflections ) && ( ( scope && scope [ :limit ] ) || options [ :limit ] )
2006-04-27 21:12:18 -04:00
if options [ :group ]
group_key = Base . connection . adapter_name == 'FrontBase' ? :group_alias : :group_field
sql << " GROUP BY #{ options [ group_key ] } "
end
2006-04-27 18:39:45 -04:00
if options [ :group ] && options [ :having ]
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if Base . connection . adapter_name == 'FrontBase'
options [ :having ] . downcase!
options [ :having ] . gsub! ( / #{ operation } \ s* \ ( \ s* #{ column_name } \ s* \ ) / , aggregate_alias )
end
sql << " HAVING #{ options [ :having ] } "
end
2006-04-25 14:53:19 -04:00
sql << " ORDER BY #{ options [ :order ] } " if options [ :order ]
2006-04-25 01:25:04 -04:00
add_limit! ( sql , options , scope )
sql << ')' if use_workaround
2006-04-06 16:10:44 -04:00
sql
2006-03-27 22:06:40 -05:00
end
2006-02-25 18:06:04 -05:00
2006-04-25 01:25:04 -04:00
def execute_simple_calculation ( operation , column_name , column , options ) #:nodoc:
value = connection . select_value ( construct_calculation_sql ( operation , column_name , options ) )
2006-03-27 22:06:40 -05:00
type_cast_calculated_value ( value , column , operation )
2006-02-25 18:06:04 -05:00
end
2006-04-25 01:25:04 -04:00
def execute_grouped_calculation ( operation , column_name , column , options ) #:nodoc:
2006-03-27 22:06:40 -05:00
group_attr = options [ :group ] . to_s
association = reflect_on_association ( group_attr . to_sym )
associated = association && association . macro == :belongs_to # only count belongs_to associations
group_field = ( associated ? " #{ options [ :group ] } _id " : options [ :group ] ) . to_s
group_alias = column_alias_for ( group_field )
group_column = column_for group_field
2006-04-25 01:25:04 -04:00
sql = construct_calculation_sql ( operation , column_name , options . merge ( :group_field = > group_field , :group_alias = > group_alias ) )
2006-03-27 22:06:40 -05:00
calculated_data = connection . select_all ( sql )
2006-04-25 01:25:04 -04:00
aggregate_alias = column_alias_for ( operation , column_name )
2006-03-27 22:06:40 -05:00
if association
key_ids = calculated_data . collect { | row | row [ group_alias ] }
key_records = association . klass . base_class . find ( key_ids )
key_records = key_records . inject ( { } ) { | hsh , r | hsh . merge ( r . id = > r ) }
end
2006-04-30 16:36:37 -04:00
calculated_data . inject ( ActiveSupport :: OrderedHash . new ) do | all , row |
2006-03-27 22:06:40 -05:00
key = associated ? key_records [ row [ group_alias ] . to_i ] : type_cast_calculated_value ( row [ group_alias ] , group_column )
value = row [ aggregate_alias ]
all << [ key , type_cast_calculated_value ( value , column , operation ) ]
end
2006-02-25 18:06:04 -05:00
end
private
2006-03-27 22:06:40 -05:00
def validate_calculation_options ( operation , options = { } )
2006-04-25 01:25:04 -04:00
options . assert_valid_keys ( CALCULATIONS_OPTIONS )
2006-03-27 22:06:40 -05:00
end
2006-02-25 18:41:51 -05:00
2006-03-27 22:06:40 -05:00
# converts a given key to the value that the database adapter returns as
#
# users.id #=> users_id
# sum(id) #=> sum_id
# count(distinct users.id) #=> count_distinct_users_id
# count(*) #=> count_all
def column_alias_for ( * keys )
2006-04-25 14:53:19 -04:00
connection . table_alias_for ( keys . join ( ' ' ) . downcase . gsub ( / \ * / , 'all' ) . gsub ( / \ W+ / , ' ' ) . strip . gsub ( / + / , '_' ) )
2006-03-27 22:06:40 -05:00
end
2006-02-25 18:06:04 -05:00
2006-03-27 22:06:40 -05:00
def column_for ( field )
field_name = field . to_s . split ( '.' ) . last
columns . detect { | c | c . name . to_s == field_name }
end
2006-03-01 11:25:14 -05:00
2006-03-27 22:06:40 -05:00
def type_cast_calculated_value ( value , column , operation = nil )
operation = operation . to_s . downcase
case operation
when 'count' then value . to_i
when 'avg' then value . to_f
else column ? column . type_cast ( value ) : value
end
2006-02-25 18:06:04 -05:00
end
end
end
2006-03-27 01:19:31 -05:00
end