From 79a61f3dec93404221e28e89e77a71e971f44312 Mon Sep 17 00:00:00 2001 From: Eric Lindvall Date: Mon, 5 May 2008 17:20:53 -0700 Subject: [PATCH] Allow ActiveRecord calculations to accept multiple grouping columns Updating patch from http://dev.rubyonrails.org/ticket/10771 to latest. --- activerecord/lib/active_record/calculations.rb | 97 ++++++++++++++++++------ activerecord/test/cases/calculations_test.rb | 6 ++ 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 3c5caef..7ebdd31 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -177,7 +177,12 @@ module ActiveRecord # 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 - sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] + if options[:groups] + options[:groups].each do |group| + sql << ", #{group[:field]} AS #{group[:alias]}" + end + end + sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround sql << " FROM #{connection.quote_table_name(table_name)} " if merged_includes.any? @@ -188,9 +193,9 @@ module ActiveRecord add_conditions!(sql, options[:conditions], scope) add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) - if options[:group] - group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field - sql << " GROUP BY #{options[group_key]} " + if options[:groups] + group_key = connection.adapter_name == 'FrontBase' ? lambda {|g| g[:alias]} : lambda {|g| g[:field]} + sql << " GROUP BY #{options[:groups].map(&group_key).join(', ')} " end if options[:group] && options[:having] @@ -214,33 +219,79 @@ module ActiveRecord type_cast_calculated_value(value, column, operation) end + def execute_grouped_calculation(operation, column_name, column, options) #:nodoc: - 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 - sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias)) - calculated_data = connection.select_all(sql) - aggregate_alias = column_alias_for(operation, column_name) + groups = [] + aggregates = [] + options[:group] = options[:group].split(',') if options[:group].is_a?(String) + options[:group] = [options[:group]] unless options[:group].is_a?(Array) - 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) } + options[:group].each do |group_option| + group_attr = group_option.to_s.strip + 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) + groups << { + :column => group_column, + :field => group_field, + :alias => group_alias, + :association => association, + :associated => associated + } + aggregates << {:alias => column_alias_for(operation, column_name)} 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, operation) - all + sql = construct_calculation_sql(operation, column_name, options.merge(:groups => groups)) + calculated_data = connection.select_all(sql) + + groups.each do |group| + if group[:association] + key_ids = calculated_data.collect { |row| row[group[:alias]] } + key_records = group[:association].klass.base_class.find(key_ids) + group[:key_records] = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + end end + + ActiveSupport::OrderedHash.new(order_results(calculated_data, groups, 0, aggregates)) end private + # recursively transforms a calculated results array into an ordered hash + # for example: [{:year => 2007, :month => 1, :count_id => 10}, + # {:year => 2007, :month => 2, :count_id => 34}, + # {:year => 2008, :month => 2, :count_id => 3}] + # when grouped by year, month, becomes: + # [[2007, [[1, 10], [2, 34]]], [2008, [[2, 3]]]] + def order_results(set, groups, group_index, aggregates) + group = groups[group_index] + set_by_groups = set.group_by do |row| + row[group[:alias]] + end + + hash = [] + if (group_index+1) < groups.size + set_by_groups.each do |key, inner_set| + key = type_cast_calculated_value(key, group[:column]) + key = group[:key_records][key] if group[:key_records] + hash << ActiveSupport::OrderedHash.new([key, order_results(inner_set, groups, (group_index+1), aggregates)]) + end + else + set_by_groups.each do |key, inner_set| + key = type_cast_calculated_value(key, group[:column]) + key = group[:key_records][key] if group[:key_records] + value = inner_set.map do |row| + row[aggregates.first[:alias]] + end + + hash << ActiveSupport::OrderedHash.new([key, value.first.to_i]) + end + end + + hash + end + def validate_calculation_options(operation, options = {}) options.assert_valid_keys(CALCULATIONS_OPTIONS) end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index a87fc26..5b6eb71 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -47,6 +47,12 @@ class CalculationsTest < ActiveRecord::TestCase c = Account.sum(:credit_limit, :group => :firm_id) [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } end + + def test_should_group_by_multiple_fields + c = Account.count(1, :group => [:credit_limit, :firm_id]) + [50, 53, 55, 60].each { |limit| assert c.keys.include?(limit) } + [[nil,1], [1,1], [6,1]].each {|inner| assert c.first.last.include?(inner) } + end def test_should_group_by_summed_field c = Account.sum(:credit_limit, :group => :firm_id) -- 1.5.3.3