2006-02-25 18:06:04 -05:00
module ActiveRecord
2006-03-27 22:06:40 -05:00
module Calculations #:nodoc:
2009-05-28 12:35:36 -04:00
extend ActiveSupport :: Concern
2009-05-11 22:23:47 -04:00
2008-06-11 19:26:35 -04:00
CALCULATIONS_OPTIONS = [ :conditions , :joins , :order , :select , :group , :having , :distinct , :limit , :offset , :include , :from ]
2006-02-25 18:06:04 -05:00
module ClassMethods
2007-07-17 16:16:35 -04:00
# Count operates using three different approaches.
2006-02-25 18:06:04 -05:00
#
# * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
2008-05-02 09:45:23 -04:00
# * 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
2006-02-25 18:06:04 -05:00
# * Count using options will find the row count matched by the options used.
#
2007-07-17 16:16:35 -04:00
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
2006-02-25 18:06:04 -05:00
#
2008-09-03 12:58:47 -04:00
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
2007-12-13 14:51:44 -05:00
# * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
2008-05-02 09:45:23 -04:00
# 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).
2007-11-07 22:37:16 -05:00
# 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.
2008-05-02 09:45:23 -04:00
# Pass <tt>:readonly => false</tt> to override.
2006-02-25 18:06:04 -05:00
# * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
2007-11-07 22:37:16 -05:00
# to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting.
2006-02-25 18:06:04 -05:00
# 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.
2007-11-07 22:37:16 -05:00
# * <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
2006-03-01 11:25:14 -05:00
# include the joined columns.
# * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
2008-06-11 19:26:35 -04:00
# * <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).
2006-02-25 18:06:04 -05:00
#
# Examples for counting all:
# Person.count # returns the total count of all people
#
2007-07-17 16:16:35 -04:00
# Examples for counting by column:
# Person.count(:age) # returns the total count of all people whose age is present in database
#
2006-02-25 18:06:04 -05:00
# 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.
2006-11-04 21:01:31 -05:00
# 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.
2006-02-25 18:06:04 -05:00
# Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
# Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
#
2008-05-02 09:45:23 -04:00
# Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
2006-02-25 18:06:04 -05:00
def count ( * args )
2007-07-16 16:26:10 -04:00
calculate ( :count , * construct_count_options_from_args ( * args ) )
2006-02-25 18:06:04 -05:00
end
2009-01-18 13:10:58 -05:00
# Calculates the average value on a given column. The value is returned as
# a float, or +nil+ if there's no row. See +calculate+ for examples with
# options.
2006-02-25 18:06:04 -05:00
#
2009-01-18 13:10:58 -05:00
# Person.average('age') # => 35.8
2006-02-25 18:06:04 -05:00
def average ( column_name , options = { } )
2009-04-29 18:39:53 -04:00
calculate ( :average , column_name , options )
2006-02-25 18:06:04 -05:00
end
2009-01-18 13:10:58 -05:00
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
2006-02-25 18:06:04 -05:00
#
2009-01-18 13:10:58 -05:00
# Person.minimum('age') # => 7
2006-02-25 18:06:04 -05:00
def minimum ( column_name , options = { } )
2009-04-29 18:39:53 -04:00
calculate ( :minimum , column_name , options )
2006-02-25 18:06:04 -05:00
end
2009-01-18 13:10:58 -05:00
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
2006-02-25 18:06:04 -05:00
#
2009-01-18 13:10:58 -05:00
# Person.maximum('age') # => 93
2006-02-25 18:06:04 -05:00
def maximum ( column_name , options = { } )
2009-04-29 18:39:53 -04:00
calculate ( :maximum , column_name , options )
2006-02-25 18:06:04 -05:00
end
2009-01-18 13:10:58 -05:00
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
2006-02-25 18:06:04 -05:00
#
2009-01-18 13:10:58 -05:00
# Person.sum('age') # => 4562
2006-02-25 18:06:04 -05:00
def sum ( column_name , options = { } )
2008-06-01 23:00:15 -04:00
calculate ( :sum , column_name , options )
2006-02-25 18:06:04 -05:00
end
2007-11-07 22:37:16 -05:00
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
2008-05-02 09:45:23 -04:00
# 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.
2006-02-25 18:06:04 -05:00
#
# 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.
2008-05-02 09:45:23 -04:00
# * 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
2006-02-25 18:06:04 -05:00
# 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:
2008-09-03 12:58:47 -04:00
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
2007-12-05 15:51:03 -05:00
# * <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.
2007-11-06 18:33:40 -05:00
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
2006-03-01 11:25:14 -05:00
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
2007-11-06 18:33:40 -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
2006-03-01 11:25:14 -05:00
# include the joined columns.
2007-11-06 18:33:40 -05:00
# * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
2006-03-01 11:25:14 -05:00
#
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
2008-03-17 00:02:34 -04:00
# Person.sum("2 * age")
2006-02-25 18:06:04 -05:00
def calculate ( operation , column_name , options = { } )
2006-03-01 11:25:14 -05:00
validate_calculation_options ( operation , options )
2009-05-02 03:02:09 -04:00
operation = operation . to_s . downcase
2009-04-29 18:39:53 -04:00
scope = scope ( :find )
merged_includes = merge_includes ( scope ? scope [ :include ] : [ ] , options [ :include ] )
2009-05-02 03:02:09 -04:00
if operation == " count "
2009-04-29 18:39:53 -04:00
if merged_includes . any?
distinct = true
column_name = options [ :select ] || primary_key
end
distinct = nil if column_name . to_s =~ / \ s*DISTINCT \ s+ /i
distinct || = options [ :distinct ]
else
distinct = nil
end
2006-04-25 01:25:04 -04:00
catch :invalid_query do
2009-08-14 11:33:05 -04:00
relation = if merged_includes . any?
join_dependency = ActiveRecord :: Associations :: ClassMethods :: JoinDependency . new ( self , merged_includes , construct_join ( options [ :joins ] , scope ) )
construct_finder_arel_with_included_associations ( options , join_dependency )
else
2009-08-18 06:50:11 -04:00
relation = arel_table ( options [ :from ] ) .
2009-08-18 13:10:03 -04:00
joins ( construct_join ( options [ :joins ] , scope ) ) .
conditions ( construct_conditions ( options [ :conditions ] , scope ) ) .
order ( options [ :order ] ) .
limit ( options [ :limit ] ) .
offset ( options [ :offset ] )
2009-08-14 11:33:05 -04:00
end
2006-04-25 01:25:04 -04:00
if options [ :group ]
2009-08-07 12:16:34 -04:00
return execute_grouped_calculation ( operation , column_name , options , relation )
2006-04-25 01:25:04 -04:00
else
2009-08-07 12:16:34 -04:00
return execute_simple_calculation ( operation , column_name , options . merge ( :distinct = > distinct ) , relation )
2006-04-25 01:25:04 -04:00
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
2009-08-07 12:16:34 -04:00
def execute_simple_calculation ( operation , column_name , options , relation ) #:nodoc:
2009-06-02 10:40:01 -04:00
column = if column_names . include? ( column_name . to_s )
Arel :: Attribute . new ( arel_table ( options [ :from ] || table_name ) ,
options [ :select ] || column_name )
2009-04-29 18:39:53 -04:00
else
2009-06-02 10:40:01 -04:00
Arel :: SqlLiteral . new ( options [ :select ] ||
( column_name == :all ? " * " : column_name . to_s ) )
2009-04-29 18:39:53 -04:00
end
2009-08-18 13:10:03 -04:00
relation = relation . select ( operation == 'count' ? column . count ( options [ :distinct ] ) : column . send ( operation ) )
2009-06-02 10:40:01 -04:00
2009-08-07 12:16:34 -04:00
type_cast_calculated_value ( connection . select_value ( relation . to_sql ) , column_for ( column_name ) , operation )
2009-04-29 18:39:53 -04:00
end
2009-08-07 12:16:34 -04:00
def execute_grouped_calculation ( operation , column_name , options , relation ) #:nodoc:
2009-04-29 18:39:53 -04: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 ? association . primary_key_name : group_attr
group_alias = column_alias_for ( group_field )
group_column = column_for group_field
options [ :group ] = connection . adapter_name == 'FrontBase' ? group_alias : group_field
aggregate_alias = column_alias_for ( operation , column_name )
2009-05-02 03:02:09 -04:00
2009-05-06 13:16:03 -04:00
options [ :select ] = ( operation == 'count' && column_name == :all ) ?
" COUNT(*) AS count_all " :
Arel :: Attribute . new ( arel_table , column_name ) . send ( operation ) . as ( aggregate_alias ) . to_sql
options [ :select ] << " , #{ group_field } AS #{ group_alias } "
2009-04-29 18:39:53 -04:00
2009-08-18 13:10:03 -04:00
relation = relation . select ( options [ :select ] ) . group ( construct_group ( options [ :group ] , options [ :having ] , nil ) )
2009-08-07 12:16:34 -04:00
calculated_data = connection . select_all ( relation . to_sql )
2009-04-29 18:39:53 -04: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
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
end
2009-05-04 20:53:29 -04:00
protected
2007-07-16 16:26:10 -04:00
def construct_count_options_from_args ( * args )
2006-04-19 17:37:54 -04:00
options = { }
column_name = :all
2009-03-07 10:26:56 -05:00
2006-09-26 13:02:45 -04:00
# We need to handle
# count()
2007-07-17 16:16:35 -04:00
# count(:column_name=:all)
2006-09-26 13:02:45 -04:00
# count(options={})
# count(column_name=:all, options={})
2009-03-07 10:26:56 -05:00
# selects specified by scopes
2007-07-17 16:16:35 -04:00
case args . size
2009-03-07 10:26:56 -05:00
when 0
column_name = scope ( :find ) [ :select ] if scope ( :find )
2007-07-17 16:16:35 -04:00
when 1
2009-03-07 10:26:56 -05:00
if args [ 0 ] . is_a? ( Hash )
column_name = scope ( :find ) [ :select ] if scope ( :find )
options = args [ 0 ]
else
column_name = args [ 0 ]
end
2007-07-17 16:16:35 -04:00
when 2
2007-07-16 16:26:10 -04:00
column_name , options = args
else
2007-07-17 16:16:35 -04:00
raise ArgumentError , " Unexpected parameters passed to count(): #{ args . inspect } "
2009-03-07 10:26:56 -05:00
end
[ column_name || :all , options ]
2006-04-19 17:37:54 -04:00
end
2006-09-26 13:02:45 -04:00
2006-02-25 18:06:04 -05:00
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
2008-05-25 07:29:00 -04:00
# Converts the given keys to the value that the database adapter returns as
# a usable column name:
#
# column_alias_for("users.id") # => "users_id"
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
2006-03-27 22:06:40 -05:00
def column_alias_for ( * keys )
2008-08-26 15:24:52 -04:00
table_name = keys . join ( ' ' )
table_name . downcase!
table_name . gsub! ( / \ * / , 'all' )
table_name . gsub! ( / \ W+ / , ' ' )
table_name . strip!
table_name . gsub! ( / + / , '_' )
connection . table_alias_for ( table_name )
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 )
case operation
2008-06-02 15:40:25 -04:00
when 'count' then value . to_i
2008-10-04 15:03:42 -04:00
when 'sum' then type_cast_using_column ( value || '0' , column )
2009-04-29 18:39:53 -04:00
when 'average' then value && ( value . is_a? ( Fixnum ) ? value . to_f : value ) . to_d
2008-10-04 15:03:42 -04:00
else type_cast_using_column ( value , column )
2006-03-27 22:06:40 -05:00
end
2006-02-25 18:06:04 -05:00
end
2008-10-04 15:03:42 -04:00
def type_cast_using_column ( value , column )
column ? column . type_cast ( value ) : value
end
2006-02-25 18:06:04 -05:00
end
end
2006-03-27 01:19:31 -05:00
end