brick icon indicating copy to clipboard operation
brick copied to clipboard

Question about .select with .includes

Open chuan29812 opened this issue 1 year ago • 3 comments

Dear maintainer/creator, I am very interested in your .select with .includes work here, as I am trying to accomplish something very similar for my company. I also saw your post in Rails forum. Did you ever consider making this a part of Rails?

Also, im trying to find how you are achieving this in this repo. I was able to grep for _brick_eager_load, but that wasnt too helpful. If you dont mind could you share a list of files Im supposed to look for to study how you accomplished this?

Sincerely, Chuan

chuan29812 avatar Sep 14 '23 16:09 chuan29812

If you just want the .select() and .includes() stuff then that's pretty simple -- just add this anywhere, say in application.rb is fine:

# An intelligent .eager_load() and .includes() that creates t0_r0 style aliases only for the columns
# used in .select().  To enable this behaviour, include the flag :_brick_eager_load as the first
# entry in your .select().
# More information:  https://discuss.rubyonrails.org/t/includes-and-select-for-joined-data/81640
class ActiveRecord::Associations::JoinDependency
  def apply_column_aliases(relation)
    if !(@join_root_alias = relation.select_values.empty?) &&
        relation.select_values.first.to_s == '_brick_eager_load'
      relation.select_values.shift
      used_cols = {}
      # Find and expand out all column names being used in select(...)
      new_select_values = relation.select_values.map(&:to_s).each_with_object([]) do |col, s|
        if col.include?(' ') # Some expression? (No chance for a simple column reference)
          s << col # Just pass it through
        else
          col = if (col_parts = col.split('.')).length == 1
                  [col]
                else
                  [col_parts[0..-2].join('.'), col_parts.last]
                end
          used_cols[col] = nil
        end
      end
      if new_select_values.present?
        relation.select_values = new_select_values
      else
        relation.select_values.clear
      end

      @aliases ||= Aliases.new(join_root.each_with_index.map do |join_part, i|
        join_alias = join_part.table&.table_alias || join_part.table_name
        keys = [join_part.base_klass.primary_key] # Always include the primary key

        # # %%% Optional to include all foreign keys:
        # keys.concat(join_part.base_klass.reflect_on_all_associations.select { |a| a.belongs_to? }.map(&:foreign_key))

        # Add foreign keys out to referenced tables that we belongs_to
        join_part.children.each { |child| keys << child.reflection.foreign_key if child.reflection.belongs_to? }

        # Add the foreign key that got us here -- "the train we rode in on" -- if we arrived from
        # a has_many or has_one:
        if join_part.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation) &&
            !join_part.reflection.belongs_to?
          keys << join_part.reflection.foreign_key
        end
        keys = keys.compact # In case we're using composite_primary_keys
        j = 0
        columns = join_part.column_names.each_with_object([]) do |column_name, s|
          # Include columns chosen in select(...) as well as the PK and any relevant FKs
          if used_cols.keys.find { |c| (c.length == 1 || c.first == join_alias) && c.last == column_name } ||
              keys.find { |c| c == column_name }
            s << Aliases::Column.new(column_name, "t#{i}_r#{j}")
          end
          j += 1
        end
        Aliases::Table.new(join_part, columns)
      end)
    end

    relation._select!(-> { aliases.columns })
  end
end

If you want to do the more interesting things with .brick_where() or .brick_select(), it gets quite a bit more involved because then you have to understand how "brick_links" works -- this is a patch to AREL which finds how every ActiveRecord chain of association names relates back to an exact table correlation name chosen by AREL when the AST tree is being walked.

lorint avatar Sep 14 '23 18:09 lorint

Thank you very much for the answer, now I completely understand how it is done. It is quite clever. Do you have any consideration of contributing this to Rails? It seems like a very useful addition. Another goal of mine is to enable/implement the same behavior for preload, it seems like i'd have to override the preload_association method in some ways. Did you ever given thought to preload?

Thanks again!

chuan29812 avatar Sep 19 '23 08:09 chuan29812

I am hopeful that this might become a part of Rails 8.

https://github.com/lorint/brick/assets/5301131/ee95f696-504b-4a01-b39e-46af1d499ec5

lorint avatar Sep 22 '23 14:09 lorint