# AggregateAssociations module AggregateAssociations def self.included(base) super base.extend(ClassMethods) end class AggregateHasManyAssociation < ActiveRecord::Associations::AssociationProxy attr_reader :finder_sql def initialize(owner, reflection) super construct_sql construct_joins end def count(*args) raise "Not implemented" end def find(*args) options = ActiveRecord::Base.send(:extract_options_from_args!, args) if @reflection.options[:finder_sql] expects_array = args.first.kind_of?(Array) ids = args.flatten.compact.uniq if ids.size == 1 id = ids.first record = load_target.detect { |record| id == record.id } expects_array ? [ record ] : record else load_target.select { |record| ids.include?(record.id) } end else conditions = "#{@finder_sql}" #if sanitized_conditions = sanitize_sql(options[:conditions]) # conditions << " AND (#{sanitized_conditions})" #end options[:conditions] = conditions if options[:order] && @reflection.options[:order] options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" elsif @reflection.options[:order] options[:order] = @reflection.options[:order] end if options[:joins] && @join_sql options[:joins] << " " << @join_sql else options[:joins] = @join_sql end merge_options_from_reflection!(options) args << options @reflection.klass.find(*args) end end protected def construct_sql case when @reflection.options[:finder_sql] @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) else finder_set = @reflection.options[:associations].collect do |assoc| # interpolate_sql(assoc.finder_sql) interpolate_sql(@owner.send(assoc).finder_sql) end @finder_sql = finder_set.join(" AND ") end if @reflection.options[:counter_sql] @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) elsif @reflection.options[:finder_sql] @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) else @counter_sql = @finder_sql end end def construct_joins associations = @reflection.options[:associations].collect { |assoc| @owner.send(assoc) } @join_sql = associations.grep(ActiveRecord::Associations::HasManyThroughAssociation).collect { |hmt| hmt.join_sql }.join(" ") end def load_target if !@owner.new_record? || foreign_key_present begin if !loaded? if @target.is_a?(Array) && @target.any? @target = (find_target + @target).uniq else @target = find_target end end rescue ActiveRecord::RecordNotFound reset end end loaded if target target end def find_target records = if @reflection.options[:finder_sql] @reflection.klass.find_by_sql(@finder_sql) else find(:all) end @reflection.options[:uniq] ? uniq(records) : records end end module ClassMethods def aggregate_has_many(association_id, options = {}, &extension) reflection = create_aggregate_has_many_reflection(association_id, options, &extension) if options[:associations] collection_reader_method(reflection, AggregateAssociations::AggregateHasManyAssociation) end end protected def create_aggregate_has_many_reflection(association_id, options, &extension) options.assert_valid_keys( :class_name, :table_name, :foreign_key, :exclusively_dependent, :dependent, :select, :conditions, :include, :order, :group, :limit, :offset, :as, :through, :source, :uniq, :extend, :associations ) options[:extend] = create_extension_module(association_id, extension) if block_given? # Create reflection reflection = ActiveRecord::Reflection::AssociationReflection.new(:aggregate_has_many, association_id, options, self) write_inheritable_hash :reflections, name => reflection reflection end end end module ActiveRecord module Associations # # Provide means to retrieve SQL conditions from associations. # I'm not sure if this is the best way to do it... class HasManyThroughAssociation < AssociationProxy attr_reader :finder_sql def join_sql construct_joins end end class HasManyAssociation < AssociationCollection attr_reader :finder_sql end class BelongsToAssociation < AssociationProxy # Fake finder SQL TODO # Somewhere the records ID should be retrievable, right? # Then we could construct some matching SQL, but usually that # won't be necessary def finder_sql raise "Not implemented right now" end end class HasOneAssociation < BelongsToAssociation # This seems to have a @finder_sql again attr_reader :finder_sql end end class Base include AggregateAssociations end end