diff options
Diffstat (limited to 'activerecord/lib/active_record/relation/calculations.rb')
-rw-r--r-- | activerecord/lib/active_record/relation/calculations.rb | 296 |
1 files changed, 148 insertions, 148 deletions
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 4af9f0859a..233a0622d5 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -185,147 +185,147 @@ module ActiveRecord private - def has_include?(column_name) - eager_loading? || (includes_values.present? && column_name && column_name != :all) - end + def has_include?(column_name) + eager_loading? || (includes_values.present? && column_name && column_name != :all) + end - def perform_calculation(operation, column_name) - operation = operation.to_s.downcase + def perform_calculation(operation, column_name) + operation = operation.to_s.downcase - # If #count is used with #distinct (i.e. `relation.distinct.count`) it is - # considered distinct. - distinct = self.distinct_value + # If #count is used with #distinct (i.e. `relation.distinct.count`) it is + # considered distinct. + distinct = self.distinct_value - if operation == "count" - column_name ||= select_for_count + if operation == "count" + column_name ||= select_for_count + + unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? + distinct = true + end - unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? - distinct = true + column_name = primary_key if column_name == :all && distinct + distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i end - column_name = primary_key if column_name == :all && distinct - distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i + if group_values.any? + execute_grouped_calculation(operation, column_name, distinct) + else + execute_simple_calculation(operation, column_name, distinct) + end end - if group_values.any? - execute_grouped_calculation(operation, column_name, distinct) - else - execute_simple_calculation(operation, column_name, distinct) - end - end + def aggregate_column(column_name) + return column_name if Arel::Expressions === column_name - def aggregate_column(column_name) - return column_name if Arel::Expressions === column_name + if @klass.column_names.include?(column_name.to_s) + Arel::Attribute.new(@klass.unscoped.table, column_name) + else + Arel.sql(column_name == :all ? "*" : column_name.to_s) + end + end - if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped.table, column_name) - else - Arel.sql(column_name == :all ? "*" : column_name.to_s) + def operation_over_aggregate_column(column, operation, distinct) + operation == "count" ? column.count(distinct) : column.send(operation) end - end - def operation_over_aggregate_column(column, operation, distinct) - operation == "count" ? column.count(distinct) : column.send(operation) - end + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: + # PostgreSQL doesn't like ORDER BY when there are no GROUP BY + relation = unscope(:order) - def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - # PostgreSQL doesn't like ORDER BY when there are no GROUP BY - relation = unscope(:order) + column_alias = column_name - column_alias = column_name + if operation == "count" && (relation.limit_value || relation.offset_value) + # Shortcut when limit is zero. + return 0 if relation.limit_value == 0 - if operation == "count" && (relation.limit_value || relation.offset_value) - # Shortcut when limit is zero. - return 0 if relation.limit_value == 0 + query_builder = build_count_subquery(relation, column_name, distinct) + else + column = aggregate_column(column_name) - query_builder = build_count_subquery(relation, column_name, distinct) - else - column = aggregate_column(column_name) + select_value = operation_over_aggregate_column(column, operation, distinct) - select_value = operation_over_aggregate_column(column, operation, distinct) + column_alias = select_value.alias + column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) + relation.select_values = [select_value] - column_alias = select_value.alias - column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) - relation.select_values = [select_value] + query_builder = relation.arel + end - query_builder = relation.arel - end + result = @klass.connection.select_all(query_builder, nil, bound_attributes) + row = result.first + value = row && row.values.first + column = result.column_types.fetch(column_alias) do + type_for(column_name) + end - result = @klass.connection.select_all(query_builder, nil, bound_attributes) - row = result.first - value = row && row.values.first - column = result.column_types.fetch(column_alias) do - type_for(column_name) + type_cast_calculated_value(value, column, operation) end - type_cast_calculated_value(value, column, operation) - end + def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: + group_attrs = group_values - def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attrs = group_values - - if group_attrs.first.respond_to?(:to_sym) - association = @klass._reflect_on_association(group_attrs.first) - associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations - group_fields = Array(associated ? association.foreign_key : group_attrs) - else - group_fields = group_attrs - end - group_fields = arel_columns(group_fields) + if group_attrs.first.respond_to?(:to_sym) + association = @klass._reflect_on_association(group_attrs.first) + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attrs) + else + group_fields = group_attrs + end + group_fields = arel_columns(group_fields) - group_aliases = group_fields.map { |field| column_alias_for(field) } - group_columns = group_aliases.zip(group_fields) + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields) - if operation == "count" && column_name == :all - aggregate_alias = "count_all" - else - aggregate_alias = column_alias_for([operation, column_name].join(" ")) - end - - select_values = [ - operation_over_aggregate_column( - aggregate_column(column_name), - operation, - distinct).as(aggregate_alias) - ] - select_values += select_values unless having_clause.empty? - - select_values.concat group_columns.map { |aliaz, field| - if field.respond_to?(:as) - field.as(aliaz) + if operation == "count" && column_name == :all + aggregate_alias = "count_all" else - "#{field} AS #{aliaz}" + aggregate_alias = column_alias_for([operation, column_name].join(" ")) end - } - relation = except(:group) - relation.group_values = group_fields - relation.select_values = select_values + select_values = [ + operation_over_aggregate_column( + aggregate_column(column_name), + operation, + distinct).as(aggregate_alias) + ] + select_values += select_values unless having_clause.empty? + + select_values.concat group_columns.map { |aliaz, field| + if field.respond_to?(:as) + field.as(aliaz) + else + "#{field} AS #{aliaz}" + end + } - calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) + relation = except(:group) + relation.group_values = group_fields + relation.select_values = select_values - if association - key_ids = calculated_data.collect { |row| row[group_aliases.first] } - key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) - key_records = Hash[key_records.map { |r| [r.id, r] }] - end + calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) - Hash[calculated_data.map do |row| - key = group_columns.map { |aliaz, col_name| - column = type_for(col_name) do - calculated_data.column_types.fetch(aliaz) do - Type::Value.new - end - end - type_cast_calculated_value(row[aliaz], column) - } - key = key.first if key.size == 1 - key = key_records[key] if associated + if association + key_ids = calculated_data.collect { |row| row[group_aliases.first] } + key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) + key_records = Hash[key_records.map { |r| [r.id, r] }] + end - column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } - [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] - end] - end + Hash[calculated_data.map do |row| + key = group_columns.map { |aliaz, col_name| + column = type_for(col_name) do + calculated_data.column_types.fetch(aliaz) do + Type::Value.new + end + end + type_cast_calculated_value(row[aliaz], column) + } + key = key.first if key.size == 1 + key = key_records[key] if associated + + column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } + [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] + end] + end # Converts the given keys to the value that the database adapter returns as # a usable column name: @@ -334,54 +334,54 @@ module ActiveRecord # column_alias_for("sum(id)") # => "sum_id" # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" # column_alias_for("count(*)") # => "count_all" - def column_alias_for(keys) - if keys.respond_to? :name - keys = "#{keys.relation.name}.#{keys.name}" - end + def column_alias_for(keys) + if keys.respond_to? :name + keys = "#{keys.relation.name}.#{keys.name}" + end - table_name = keys.to_s.downcase - table_name.gsub!(/\*/, "all") - table_name.gsub!(/\W+/, " ") - table_name.strip! - table_name.gsub!(/ +/, "_") + table_name = keys.to_s.downcase + table_name.gsub!(/\*/, "all") + table_name.gsub!(/\W+/, " ") + table_name.strip! + table_name.gsub!(/ +/, "_") - @klass.connection.table_alias_for(table_name) - end + @klass.connection.table_alias_for(table_name) + end - def type_for(field, &block) - field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last - @klass.type_for_attribute(field_name, &block) - end + def type_for(field, &block) + field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last + @klass.type_for_attribute(field_name, &block) + end - def type_cast_calculated_value(value, type, operation = nil) - case operation - when "count" then value.to_i - when "sum" then type.deserialize(value || 0) - when "average" then value.respond_to?(:to_d) ? value.to_d : value - else type.deserialize(value) + def type_cast_calculated_value(value, type, operation = nil) + case operation + when "count" then value.to_i + when "sum" then type.deserialize(value || 0) + when "average" then value.respond_to?(:to_d) ? value.to_d : value + else type.deserialize(value) + end end - end - def select_for_count - if select_values.present? - return select_values.first if select_values.one? - select_values.join(", ") - else - :all + def select_for_count + if select_values.present? + return select_values.first if select_values.one? + select_values.join(", ") + else + :all + end end - end - def build_count_subquery(relation, column_name, distinct) - column_alias = Arel.sql("count_column") - subquery_alias = Arel.sql("subquery_for_count") + def build_count_subquery(relation, column_name, distinct) + column_alias = Arel.sql("count_column") + subquery_alias = Arel.sql("subquery_for_count") - aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) - relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) + relation.select_values = [aliased_column] + subquery = relation.arel.as(subquery_alias) - sm = Arel::SelectManager.new relation.engine - select_value = operation_over_aggregate_column(column_alias, "count", distinct) - sm.project(select_value).from(subquery) - end + sm = Arel::SelectManager.new relation.engine + select_value = operation_over_aggregate_column(column_alias, "count", distinct) + sm.project(select_value).from(subquery) + end end end |