aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relational_calculations.rb
diff options
context:
space:
mode:
authorPratik Naik <pratiknaik@gmail.com>2009-12-29 04:28:43 +0530
committerPratik Naik <pratiknaik@gmail.com>2009-12-29 04:30:04 +0530
commit08633bae5e4f05e913ec5d5d2483bfd6c07c7375 (patch)
tree9ceafedbf6825b2b756dabc34a4e297df4971741 /activerecord/lib/active_record/relational_calculations.rb
parent949c8c0d0e92e6e7855dc4decc10eb4c658e0ede (diff)
downloadrails-08633bae5e4f05e913ec5d5d2483bfd6c07c7375.tar.gz
rails-08633bae5e4f05e913ec5d5d2483bfd6c07c7375.tar.bz2
rails-08633bae5e4f05e913ec5d5d2483bfd6c07c7375.zip
Migrate all the calculation methods to Relation
Diffstat (limited to 'activerecord/lib/active_record/relational_calculations.rb')
-rw-r--r--activerecord/lib/active_record/relational_calculations.rb147
1 files changed, 132 insertions, 15 deletions
diff --git a/activerecord/lib/active_record/relational_calculations.rb b/activerecord/lib/active_record/relational_calculations.rb
index 10eb992167..d77624c7bf 100644
--- a/activerecord/lib/active_record/relational_calculations.rb
+++ b/activerecord/lib/active_record/relational_calculations.rb
@@ -2,39 +2,119 @@ module ActiveRecord
module RelationalCalculations
def count(*args)
- column_name, options = construct_count_options_from_args(*args)
- distinct = options[:distinct] ? true : false
+ calculate(:count, *construct_count_options_from_args(*args))
+ end
+
+ def average(column_name)
+ calculate(:average, column_name)
+ end
+
+ def minimum(column_name)
+ calculate(:minimum, column_name)
+ end
+
+ def maximum(column_name)
+ calculate(:maximum, column_name)
+ end
+
+ def sum(column_name)
+ calculate(:sum, column_name)
+ end
+
+ def calculate(operation, column_name, options = {})
+ operation = operation.to_s.downcase
+
+ if operation == "count"
+ joins = @relation.joins(relation)
+ if joins.present? && joins =~ /LEFT OUTER/i
+ distinct = true
+ column_name = @klass.primary_key if column_name == :all
+ end
+ distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
+ distinct ||= options[:distinct]
+ else
+ distinct = nil
+ end
+
+ distinct = options[:distinct] || distinct
+ column_name = :all if column_name.blank? && operation == "count"
+
+ if @relation.send(:groupings).any?
+ return execute_grouped_calculation(operation, column_name)
+ else
+ return execute_simple_calculation(operation, column_name, distinct)
+ end
+ rescue ThrowResult
+ 0
+ end
+
+ private
+
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
column = if @klass.column_names.include?(column_name.to_s)
- Arel::Attribute.new(@relation.table, column_name)
+ Arel::Attribute.new(@klass.arel_table, column_name)
else
Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
end
- relation = select(column.count(distinct))
- @klass.connection.select_value(relation.to_sql).to_i
+ relation = 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)
end
- private
+ def execute_grouped_calculation(operation, column_name) #:nodoc:
+ group_attr = @relation.send(:groupings).first.value
+ 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)
+
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
+
+ aggregate_alias = column_alias_for(operation, column_name)
+
+ select_statement = if operation == 'count' && column_name == :all
+ "COUNT(*) AS count_all"
+ else
+ Arel::Attribute.new(@klass.arel_table, column_name).send(operation).as(aggregate_alias).to_sql
+ end
+
+ select_statement << ", #{group_field} AS #{group_alias}"
+
+ relation = select(select_statement).group(group)
+
+ calculated_data = @klass.connection.select_all(relation.to_sql)
+
+ 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
def construct_count_options_from_args(*args)
options = {}
column_name = :all
- # We need to handle
- # count()
- # count(:column_name=:all)
- # count(options={})
- # count(column_name=:all, options={})
- # selects specified by scopes
-
+ # Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true)
# TODO : relation.projections only works when .select() was last in the chain. Fix it!
case args.size
when 0
- column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present?
+ select = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present?
+ column_name = select if select !~ /(,|\*)/
when 1
if args[0].is_a?(Hash)
- column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present?
+ select = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present?
+ column_name = select if select !~ /(,|\*)/
options = args[0]
else
column_name = args[0]
@@ -48,5 +128,42 @@ module ActiveRecord
[column_name || :all, options]
end
+ # 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"
+ def column_alias_for(*keys)
+ table_name = keys.join(' ')
+ table_name.downcase!
+ table_name.gsub!(/\*/, 'all')
+ table_name.gsub!(/\W+/, ' ')
+ table_name.strip!
+ table_name.gsub!(/ +/, '_')
+
+ @klass.connection.table_alias_for(table_name)
+ end
+
+ def column_for(field)
+ field_name = field.to_s.split('.').last
+ @klass.columns.detect { |c| c.name.to_s == field_name }
+ end
+
+ def type_cast_calculated_value(value, column, operation = nil)
+ case operation
+ when 'count' then value.to_i
+ when 'sum' then type_cast_using_column(value || '0', column)
+ when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
+ else type_cast_using_column(value, column)
+ end
+ end
+
+ def type_cast_using_column(value, column)
+ column ? column.type_cast(value) : value
+ end
+
end
end