module Switchman::ActiveRecord::Calculations

Public Instance Methods

calculate_simple_average(column_name, distinct) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 51
def calculate_simple_average(column_name, distinct)
  # See activerecord#execute_simple_calculation
  relation = except(:order)
  column = aggregate_column(column_name)
  relation.select_values = [operation_over_aggregate_column(column, 'average', distinct).as('average'),
                            operation_over_aggregate_column(column, 'count', distinct).as('count')]

  initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
  if initial_results.is_a?(Array)
    initial_results.each do |r|
      r['average'] = type_cast_calculated_value_switchman(r['average'], column_name, 'average')
      r['count'] = type_cast_calculated_value_switchman(r['count'], column_name, 'count')
    end
    result = initial_results.map { |r| r['average'] * r['count'] }.sum / initial_results.map do |r|
                                                                           r['count']
                                                                         end.sum
  else
    result = type_cast_calculated_value_switchman(initial_results.first['average'], column_name, 'average')
  end
  result
end
execute_grouped_calculation(operation, column_name, distinct) click to toggle source

See activerecord#execute_grouped_calculation

# File lib/switchman/active_record/calculations.rb, line 74
def execute_grouped_calculation(operation, column_name, distinct)
  opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)

  relation = build_grouped_calculation_relation(opts)
  target_shard = Shard.current

  rows = relation.activate do |rel, shard|
    calculated_data = klass.connection.select_all(rel)

    if opts[:association]
      key_ids     = calculated_data.collect { |row| row[opts[:group_aliases].first] }
      key_records = opts[:association].klass.base_class.where(id: key_ids)
      key_records = key_records.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }.to_h
    end

    calculated_data.map do |row|
      row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
        row[opts[:aggregate_alias]], column_name, opts[:operation]
      )
      row['count'] = row['count'].to_i if opts[:operation] == 'average'

      opts[:group_columns].each do |aliaz, _type, group_column_name|
        if opts[:associated] && (aliaz == opts[:group_aliases].first)
          row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
        elsif group_column_name && @klass.sharded_column?(group_column_name)
          row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
        end
      end
      row
    end
  end

  compact_grouped_calculation_rows(rows, opts)
end
execute_simple_calculation(operation, column_name, distinct) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 29
def execute_simple_calculation(operation, column_name, distinct)
  operation = operation.to_s.downcase
  if operation == 'average'
    result = calculate_simple_average(column_name, distinct)
  else
    result = activate do |relation|
      relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
    end
    if result.is_a?(Array)
      case operation
      when 'count', 'sum'
        result = result.sum
      when 'minimum'
        result = result.min
      when 'maximum'
        result = result.max
      end
    end
  end
  result
end
pluck(*column_names) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 6
def pluck(*column_names)
  target_shard = Shard.current(klass.connection_classes)
  shard_count = 0
  result = activate do |relation, shard|
    shard_count += 1
    results = relation.call_super(:pluck, Calculations, *column_names)
    if column_names.length > 1
      column_names.each_with_index do |column_name, idx|
        next unless klass.sharded_column?(column_name)

        results.each do |r|
          r[idx] = Shard.relative_id_for(r[idx], shard, target_shard)
        end
      end
    elsif klass.sharded_column?(column_names.first.to_s)
      results = results.map { |r| Shard.relative_id_for(r, shard, target_shard) }
    end
    results
  end
  result.uniq! if distinct_value && shard_count > 1
  result
end

Private Instance Methods

aggregate_alias_for(operation, column_name) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 149
def aggregate_alias_for(operation, column_name)
  if operation == 'count' && column_name == :all
    'count_all'
  elsif operation == 'average'
    'average'
  else
    column_alias_for("#{operation} #{column_name}")
  end
end
build_grouped_calculation_relation(opts) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 159
def build_grouped_calculation_relation(opts)
  group = opts[:group_fields]

  select_values = [
    operation_over_aggregate_column(
      aggregate_column(opts[:column_name]),
      opts[:operation],
      opts[:distinct]
    ).as(opts[:aggregate_alias])
  ]
  if opts[:operation] == 'average'
    # include count in average so we can recalculate the average
    # across all shards if needed
    select_values << operation_over_aggregate_column(
      aggregate_column(opts[:column_name]),
      'count', opts[:distinct]
    ).as('count')
  end

  haves = having_clause.send(:predicates)
  select_values += select_values unless haves.empty?
  select_values.concat(opts[:group_fields].zip(opts[:group_aliases]).map do |field, aliaz|
    if field.respond_to?(:as)
      field.as(aliaz)
    else
      "#{field} AS #{aliaz}"
    end
  end)

  relation = except(:group)
  relation.group_values = group
  relation.select_values = select_values
  relation
end
column_name_for(field) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 120
def column_name_for(field)
  field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
end
compact_grouped_calculation_rows(rows, opts) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 194
def compact_grouped_calculation_rows(rows, opts)
  result = ::ActiveSupport::OrderedHash.new
  rows.each do |row|
    key = opts[:group_columns].map { |aliaz, _column| row[aliaz] }
    key = key.first if key.size == 1
    value = row[opts[:aggregate_alias]]

    if opts[:operation] == 'average'
      if result.key?(key)
        old_value, old_count = result[key]
        new_count = old_count + row['count']
        new_value = ((old_value * old_count) + (value * row['count'])) / new_count
        result[key] = [new_value, new_count]
      else
        result[key] = [value, row['count']]
      end
    elsif result.key?(key)
      case opts[:operation]
      when 'count', 'sum'
        result[key] += value
      when 'minimum'
        result[key] = value if value < result[key]
      when 'maximum'
        result[key] = value if value > result[key]
      end
    else
      result[key] = value
    end
  end

  result.transform_values!(&:first) if opts[:operation] == 'average'

  result
end
grouped_calculation_options(operation, column_name, distinct) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 124
def grouped_calculation_options(operation, column_name, distinct)
  opts = { operation: operation, column_name: column_name, distinct: distinct }

  opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
  group_attrs = group_values
  if group_attrs.first.respond_to?(:to_sym)
    association  = klass.reflect_on_association(group_attrs.first.to_sym)
    associated   = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
    group_fields = Array(associated ? association.foreign_key : group_attrs)
  else
    group_fields = group_attrs
  end

  # to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
  group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
  group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
    [aliaz, type_for(field), column_name_for(field)]
  end
  opts.merge!(association: association, associated: associated,
              group_aliases: group_aliases, group_columns: group_columns,
              group_fields: group_fields)

  opts
end
type_cast_calculated_value_switchman(value, column_name, operation) click to toggle source
# File lib/switchman/active_record/calculations.rb, line 111
def type_cast_calculated_value_switchman(value, column_name, operation)
  type_cast_calculated_value(value, operation) do |val|
    column = aggregate_column(column_name)
    type ||= column.try(:type_caster) ||
             lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
    type.deserialize(val)
  end
end