class ActiveRecord::Associations::JoinDependency

FIXME: Hopefully we can get this into Rails core so this is no longer required in our codebase, but the rule that are broken here are mostly due to the style of the Rails codebase conflicting with our own. Ignoring them to avoid noise in RuboCop, but allow us to keep the same syntax from the original codebase.

rubocop:disable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax rubocop:disable Layout/AlignHash

Public Instance Methods

instantiate(result_set, *_, &block) click to toggle source
# File lib/active_record/virtual_attributes/virtual_fields.rb, line 253
def instantiate(result_set, *_, &block)
  primary_key = aliases.column_alias(join_root, join_root.primary_key)

  seen = Hash.new { |i, object_id|
    i[object_id] = Hash.new { |j, child_class|
      j[child_class] = {}
    }
  }

  model_cache = Hash.new { |h,klass| h[klass] = {} }
  parents = model_cache[join_root]
  column_aliases = aliases.column_aliases(join_root)

  # New Code
  column_aliases += select_values_from_references(column_aliases, result_set) if result_set.present?
  # End of New Code

  message_bus = ActiveSupport::Notifications.instrumenter

  payload = {
    record_count: result_set.length,
    class_name: join_root.base_klass.name
  }

  message_bus.instrument('instantiation.active_record', payload) do
    result_set.each { |row_hash|
      parent_key = primary_key ? row_hash[primary_key] : row_hash
      parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
      if ActiveRecord.version.to_s < "6.0"
        construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
      else
        construct(parent, join_root, row_hash, seen, model_cache)
      end
    }
  end

  parents.values
end
select_values_from_references(column_aliases, result_set) click to toggle source

This monkey patches the ActiveRecord::Associations::JoinDependency to include columns into the main record that might have been added through a `select` clause.

This can be seen with the following:

Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
  .includes(:tags => {}).references(:tags)

Which will produce a SQL SELECT statement kind of like this:

SELECT "vms".*,
       (<virtual_attribute_arel>) AS some_vm_virtual_col,
       "vms"."id"      AS t0_r0
       "vms"."vendor"  AS t0_r1
       "vms"."format"  AS t0_r1
       "vms"."version" AS t0_r1
       ...
       "tags"."id"     AS t1_r0
       "tags"."name"   AS t1_r1

This is because rails is trying to reduce the number of queries needed to fetch all of the records in the include, so it grabs the columns for both of the tables together to do it. Unfortunately (or fortunately… depending on how you look at it), it does not remove any `.select` columns from the query that is run in the process, so that is brought along for the ride, but never used when this method instanciates the objects.

The “New Code” here simply also instanciates any extra rows that might have been included in the select (virtual_columns) as well and brought back with the result set.

# File lib/active_record/virtual_attributes/virtual_fields.rb, line 327
def select_values_from_references(column_aliases, result_set)
  join_dep_keys         = aliases.columns.map(&:right)
  join_root_aliases     = column_aliases.map(&:first)
  additional_attributes = result_set.first.keys
                                    .reject { |k| join_dep_keys.include?(k) }
                                    .reject { |k| join_root_aliases.include?(k) }
  if ActiveRecord.version.to_s >= "6.0"
    additional_attributes.map { |k| Aliases::Column.new(k, k) }
  else
    additional_attributes.map { |k| [k, k] }
  end
end