2006-02-25 18:06:04 -05:00
module ActiveRecord
2006-03-27 22:06:40 -05:00
module Calculations #:nodoc:
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
def self . included ( base )
base . extend ( ClassMethods )
end
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
2008-05-25 07:29:00 -04:00
# Calculates the average value on a given column. The value is returned as a float. See +calculate+ for examples with options.
2006-02-25 18:06:04 -05:00
#
# Person.average('age')
def average ( column_name , options = { } )
calculate ( :avg , column_name , options )
end
2008-05-25 07:29:00 -04:00
# 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.
2006-02-25 18:06:04 -05:00
#
# Person.minimum('age')
def minimum ( column_name , options = { } )
calculate ( :min , column_name , options )
end
2008-05-25 07:29:00 -04:00
# 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.
2006-02-25 18:06:04 -05:00
#
# Person.maximum('age')
def maximum ( column_name , options = { } )
calculate ( :max , column_name , options )
end
2008-05-25 07:29:00 -04:00
# Calculates the sum of values on a given column. The value is returned with the same data type of the column. See +calculate+ for examples with options.
2006-02-25 18:06:04 -05:00
#
2006-03-01 21:53:57 -05:00
# Person.sum('age')
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 )
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
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
2007-07-17 16:16:35 -04: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={})
2007-07-17 16:16:35 -04:00
case args . size
when 1
args [ 0 ] . is_a? ( Hash ) ? options = args [ 0 ] : column_name = args [ 0 ]
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 } "
2007-07-16 16:26:10 -04:00
end if args . size > 0
2007-07-17 16:16:35 -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:
2006-11-04 21:01:31 -05:00
operation = operation . to_s . downcase
options = options . symbolize_keys
2007-11-07 10:07:39 -05:00
scope = scope ( :find )
2006-04-25 01:25:04 -04:00
merged_includes = merge_includes ( scope ? scope [ :include ] : [ ] , options [ :include ] )
aggregate_alias = column_alias_for ( operation , column_name )
2008-03-17 00:02:34 -04:00
column_name = " #{ connection . quote_table_name ( table_name ) } . #{ column_name } " if column_names . include? ( column_name . to_s )
2006-04-25 01:25:04 -04:00
2006-11-04 21:01:31 -05:00
if operation == 'count'
if merged_includes . any?
options [ :distinct ] = true
2007-10-31 01:58:09 -04:00
column_name = options [ :select ] || [ connection . quote_table_name ( table_name ) , primary_key ] * '.'
2006-11-04 21:01:31 -05:00
end
if options [ :distinct ]
use_workaround = ! connection . supports_count_distinct?
end
2006-04-25 01:25:04 -04:00
end
2008-04-04 08:06:22 -04:00
if options [ :distinct ] && column_name . to_s !~ / \ s*DISTINCT \ s+ /i
distinct = 'DISTINCT '
end
sql = " SELECT #{ operation } ( #{ distinct } #{ column_name } ) AS #{ aggregate_alias } "
2006-04-25 01:25:04 -04:00
# 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-11-04 21:01:31 -05:00
2006-03-27 22:06:40 -05:00
sql << " , #{ options [ :group_field ] } AS #{ options [ :group_alias ] } " if options [ :group ]
2008-06-11 19:26:35 -04:00
if options [ :from ]
sql << " FROM #{ options [ :from ] } "
else
sql << " FROM (SELECT #{ distinct } #{ column_name } " if use_workaround
sql << " FROM #{ connection . quote_table_name ( table_name ) } "
end
2008-08-26 12:00:47 -04:00
joins = " "
2008-08-28 12:00:18 -04:00
add_joins! ( joins , options [ :joins ] , scope )
2008-08-26 12:00:47 -04:00
2006-04-25 01:25:04 -04:00
if merged_includes . any?
2008-08-26 12:00:47 -04:00
join_dependency = ActiveRecord :: Associations :: ClassMethods :: JoinDependency . new ( self , merged_includes , joins )
2006-04-25 01:25:04 -04:00
sql << join_dependency . join_associations . collect { | join | join . association_join } . join
end
2008-08-26 12:00:47 -04:00
sql << joins unless joins . blank?
2006-03-27 22:06:40 -05:00
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 ]
2006-11-04 21:01:31 -05:00
group_key = connection . adapter_name == 'FrontBase' ? :group_alias : :group_field
2006-04-27 21:12:18 -04:00
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
2006-11-04 21:01:31 -05:00
if connection . adapter_name == 'FrontBase'
2006-04-27 18:39:45 -04:00
options [ :having ] . downcase!
options [ :having ] . gsub! ( / #{ operation } \ s* \ ( \ s* #{ column_name } \ s* \ ) / , aggregate_alias )
end
2006-11-04 21:01:31 -05:00
2006-04-27 18:39:45 -04:00
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 )
2008-10-03 15:35:01 -04:00
sql << " ) #{ aggregate_alias } _subquery " 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
2008-02-01 23:28:42 -05:00
group_field = associated ? association . primary_key_name : group_attr
2006-03-27 22:06:40 -05:00
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 |
2007-05-31 23:26:51 -04:00
key = type_cast_calculated_value ( row [ group_alias ] , group_column )
key = key_records [ key ] if associated
2006-03-27 22:06:40 -05:00
value = row [ aggregate_alias ]
2007-12-27 06:18:30 -05:00
all [ key ] = type_cast_calculated_value ( value , column , operation )
all
2006-03-27 22:06:40 -05:00
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
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 )
operation = operation . to_s . downcase
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 )
when 'avg' then value && value . to_d
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