require 'active_support/deprecation' require 'active_support/core_ext/string/filters' ::ActiveRecord::Associations::JoinDependency::Aliases.class_eval do def initialize(tables) @tables = tables @alias_cache = tables.each_with_object({}) { |table,h| h[table.node] = table.columns.each_with_object({}) { |column,i| i[column.name] = column.alias } } @name_and_alias_cache = tables.each_with_object({}) { |table,h| h[table.node] = table.columns.map { |column| [column.name, column.alias] } @base_class_node_aliases ||= h[table.node] if table.node.is_a?(ActiveRecord::Associations::JoinDependency::JoinBase) } @virtual_attributes_names = [] end # valid formats are: # 1 'table_name.column' or 'table_name.column as column_1' will be parsed! distinct on can be used also # 2 {table_name: column} or { table_name: [column1, column2] } # 3 table_name: 2 def update_aliases_to_select_values( select_values ) return if select_values.blank? select_values.each do |sv| # if sv is symbol that we assume that its a base table column and it will be aliased and added as usual # all we need is some specials joins+select from related tables if sv.is_a?(Hash) flatten_hash_values(sv).each{|sub_sv| @base_class_node_aliases << [sub_sv, sub_sv]; @virtual_attributes_names << sub_sv } elsif sv.is_a?(String) # this is the case of long raw select sv.split( ", " ).each do |sub_sv| if sub_sv[/.+ as .+/i] selected_column = sub_sv[/ as .+/i][4..-1] @base_class_node_aliases << [selected_column, selected_column] @virtual_attributes_names << selected_column elsif sub_sv[/.+\.[^\*]+/] selected_column = sub_sv[/\..+/][1..-1] @base_class_node_aliases << [selected_column, selected_column] @virtual_attributes_names << selected_column end end end end end def slice_selected_attr_types( column_types ) column_types.slice( *@virtual_attributes_names ) end private def flatten_hash_values( some_hash ) some_hash.values.map{ |value| value.is_a?(Hash) ? flatten_hash_values( value ) : value }.flatten end end ::ActiveRecord::Associations::JoinDependency::JoinBase.class_eval do def instantiate(row, aliases, column_types = {}, &block) base_klass.instantiate(extract_record(row, aliases), column_types, &block) end end ::ActiveRecord::Associations::JoinDependency.class_eval do def instantiate(result_set, aliases) 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 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 # DISTINCTION PART > join_root.instantiate(row_hash, column_aliases, aliases.slice_selected_attr_types( result_set.column_types ) ) # PREVIOUS > join_root.instantiate(row_hash, column_aliases ) parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, aliases.slice_selected_attr_types( result_set.column_types ) ) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } end parents.values end end ::ActiveRecord::FinderMethods.class_eval do def find_with_associations # NOTE: the JoinDependency constructed here needs to know about # any joins already present in `self`, so pass them in # # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 # incorrect SQL is generated. In that case, the join dependency for # SpecialCategorizations is constructed without knowledge of the # preexisting join in joins_values to categorizations (by way of # the `has_many :through` for categories). # join_dependency = construct_join_dependency(joins_values) aliases = join_dependency.aliases relation = select aliases.columns relation = apply_join_dependency(relation, join_dependency) if block_given? yield relation else if ActiveRecord::NullRelation === relation [] else arel = relation.arel rows = connection.select_all(arel, 'SQL', relation.bound_attributes) #1 DISTINCTION IS HERE: # now we gently mokey-patching existing column aliases with select values aliases.update_aliases_to_select_values(values[:select]) unless values[:select].blank? join_dependency.instantiate(rows, aliases) end end end end # module ActiveRecord # module Associations # class JoinDependency # :nodoc: # # class Aliases # :nodoc: # # def initialize(tables) # # @tables = tables # # @alias_cache = tables.each_with_object({}) { |table,h| # # h[table.node] = table.columns.each_with_object({}) { |column,i| # # i[column.name] = column.alias # # } # # } # # @name_and_alias_cache = tables.each_with_object({}) { |table,h| # # h[table.node] = table.columns.map { |column| # # [column.name, column.alias] # # } # # @base_class_node_aliases ||= h[table.node] if table.node.is_a?(ActiveRecord::Associations::JoinDependency::JoinBase) # # } # # # # @virtual_attributes_names = [] # # end # # # valid formats are: # # # 1 'table_name.column' or 'table_name.column as column_1' will be parsed! distinct on can be used also # # # 2 {table_name: column} or { table_name: [column1, column2] } # # # 3 table_name: 2 # # def update_aliases_to_select_values( select_values ) # # return if select_values.blank? # # select_values.each do |sv| # # # if sv is symbol that we assume that its a base table column and it will be aliased and added as usual # # # all we need is some specials joins+select from related tables # # if sv.is_a?(Hash) # # flatten_hash_values(sv).each{|sub_sv| @base_class_node_aliases << [sub_sv, sub_sv]; @virtual_attributes_names << sub_sv } # # elsif sv.is_a?(String) # # # this is the case of long raw select # # sv.split( ", " ).each do |sub_sv| # # if sub_sv[/.+ as .+/i] # # selected_column = sub_sv[/ as .+/i][4..-1] # # @base_class_node_aliases << [selected_column, selected_column] # # @virtual_attributes_names << selected_column # # elsif sub_sv[/.+\.[^\*]+/] # # selected_column = sub_sv[/\..+/][1..-1] # # @base_class_node_aliases << [selected_column, selected_column] # # @virtual_attributes_names << selected_column # # end # # end # # end # # end # # end # # # # def slice_selected_attr_types( column_types ) # # column_types.slice( *@virtual_attributes_names ) # # end # # # # private # # def flatten_hash_values( some_hash ) # # some_hash.values.map{ |value| value.is_a?(Hash) ? flatten_hash_values( value ) : value }.flatten # # end # # end # # # class JoinPart # # def instantiate(row, aliases, column_types = {}, &block) # # base_klass.instantiate(extract_record(row, aliases), column_types, &block) # # end # # end # # # def instantiate(result_set, aliases) # # 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 # # # # 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 # # # DISTINCTION PART > join_root.instantiate(row_hash, column_aliases, aliases.slice_selected_attr_types( result_set.column_types ) ) # # # PREVIOUS > join_root.instantiate(row_hash, column_aliases ) # # parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, aliases.slice_selected_attr_types( result_set.column_types ) ) # # construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) # # } # # end # # parents.values # # end # # end # end # end # module ActiveRecord # module FinderMethods # # # def find_with_associations # # # NOTE: the JoinDependency constructed here needs to know about # # # any joins already present in `self`, so pass them in # # # # # # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 # # # incorrect SQL is generated. In that case, the join dependency for # # # SpecialCategorizations is constructed without knowledge of the # # # preexisting join in joins_values to categorizations (by way of # # # the `has_many :through` for categories). # # # # # join_dependency = construct_join_dependency(joins_values) # # # # aliases = join_dependency.aliases # # relation = select aliases.columns # # relation = apply_join_dependency(relation, join_dependency) # # # # if block_given? # # yield relation # # else # # if ActiveRecord::NullRelation === relation # # [] # # else # # arel = relation.arel # # rows = connection.select_all(arel, 'SQL', relation.bound_attributes) # # #1 DISTINCTION IS HERE: # # # now we gently mokey-patching existing column aliases with select values # # aliases.update_aliases_to_select_values(values[:select]) unless values[:select].blank? # # # # join_dependency.instantiate(rows, aliases) # # end # # end # # end # end # end