<commit_before>module Searchlogic
  module NamedScopes
    # Handles dynamically creating named scopes for associations.
    module Associations
      def condition?(name) # :nodoc:
        super || association_condition?(name) || association_alias_condition?(name)
      end
      
      def primary_condition_name(name) # :nodoc:
        if result = super
          result
        elsif association_condition?(name)
          name.to_sym
        elsif details = association_alias_condition_details(name)
          "#{details[:association]}_#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
        else
          nil
        end
      end
      
      # Is the name of the method a valid name for an association condition?
      def association_condition?(name)
        !association_condition_details(name).nil?
      end
      
      # Is the ane of the method a valie name for an association alias condition?
      # An alias being "gt" for "greater_than", etc.
      def association_alias_condition?(name)
        !association_alias_condition_details(name).nil?
      end
      
      private
        def method_missing(name, *args, &block)
          if details = association_condition_details(name)
            create_association_condition(details[:association], details[:column], details[:condition], args)
            send(name, *args)
          elsif details = association_alias_condition_details(name)
            create_association_alias_condition(details[:association], details[:column], details[:condition], args)
            send(name, *args)
          elsif details = association_ordering_condition_details(name)
            create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
            send(name, *args)
          else
            super
          end
        end
        
        def association_condition_details(name)
          associations = reflect_on_all_associations.collect { |assoc| assoc.name }
          if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
            {:association => $1, :column => $2, :condition => $3}
          end
        end
        
        def create_association_condition(association_name, column, condition, args)
          named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
        end
        
        def association_alias_condition_details(name)
          associations = reflect_on_all_associations.collect { |assoc| assoc.name }
          if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
            {:association => $1, :column => $2, :condition => $3}
          end
        end
        
        def create_association_alias_condition(association, column, condition, args)
          primary_condition = primary_condition(condition)
          alias_name = "#{association}_#{column}_#{condition}"
          primary_name = "#{association}_#{column}_#{primary_condition}"
          send(primary_name, *args) # go back to method_missing and make sure we create the method
          (class << self; self; end).class_eval { alias_method alias_name, primary_name }
        end
        
        def association_ordering_condition_details(name)
          associations = reflect_on_all_associations.collect { |assoc| assoc.name }
          if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
            {:order_as => $1, :association => $2, :column => $3}
          end
        end
        
        def create_association_ordering_condition(association_name, order_as, column, args)
          named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
        end
        
        def association_condition_options(association_name, association_condition, args)
          association = reflect_on_association(association_name.to_sym)
          scope = association.klass.send(association_condition, *args)
          scope_options = association.klass.named_scope_options(association_condition)
          arity = association.klass.named_scope_arity(association_condition)
          
          if !arity || arity == 0
            # The underlying condition doesn't require any parameters, so let's just create a simple
            # named scope that is based on a hash.
            options = scope.proxy_options
            add_left_outer_joins(options, association)
            options
          else
            # The underlying condition requires parameters, let's match the parameters it requires
            # and pass those onto the named scope. We can't use proxy_options because that returns the
            # result after a value has been passed.
            proc_args = []
            if arity > 0
              arity.times { |i| proc_args << "arg#{i}"}
            else
              positive_arity = arity * -1
              positive_arity.times do |i|
                if i == (positive_arity - 1)
                  proc_args << "*arg#{i}"
                else
                  proc_args << "arg#{i}"
                end
              end
            end
            eval <<-"end_eval"
              searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
                options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
                add_left_outer_joins(options, association)
                options
              }
            end_eval
          end
        end
        
        # In a named scope you have 2 options for adding joins: :include and :joins.
        #
        # :include will execute multiple queries for each association and instantiate objects for each association.
        # This is not what we want when we are searching. The only other option left is :joins. We can pass the
        # name of the association directly, but AR creates an INNER JOIN. If we are ordering by an association's
        # attribute, and that association is optional, the records without an association will be omitted. Again,
        # not what we want.
        #
        # So the only option left is to use :joins with manually written SQL. We can still have AR generate this SQL
        # for us by leveraging it's join dependency classes. Instead of using the InnerJoinDependency we use the regular
        # JoinDependency which creates a LEFT OUTER JOIN, which is what we want.
        #
        # The code below was extracted out of AR's add_joins! method and then modified.
        def add_left_outer_joins(options, association)
          joins = left_outer_joins(association.name)
          options[:joins] ||= []
          options[:joins] = joins + options[:joins]
        end
        
        # Leverages ActiveRecord's JoinDependency class to create a left outer join. Searchlogic uses left outer joins so that
        # records with no associations are included in the result when the association is optional. You can use this method
        # internally when creating your own named scopes that need joins. You need to do this because then ActiveRecord will
        # remove any duplicate joins for you when you chain named scopes that require the same join. If you are using a
        # LEFT OUTER JOIN and an INNER JOIN, ActiveRecord will add both to the query, causing SQL errors.
        #
        # Bottom line, this is convenience method that you can use when creating your own named scopes. Ex:
        #
        #   named_scope :orders_line_items_price_expensive, :joins => left_out_joins(:orders => :line_items), :conditions => "line_items.price > 100"
        #
        # Now your joins are consistent with Searchlogic allowing you to avoid SQL errors with duplicate joins.
        def left_outer_joins(association_name)
          ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect do |assoc|
            sql = assoc.association_join.strip
            sql.split(/LEFT OUTER JOIN/).delete_if { |join| join.strip.blank? }.collect { |join| "LEFT OUTER JOIN #{join.strip}"}
          end.flatten
        end
    end
  end
end<commit_msg>Make left_outer_joins public<commit_after>module Searchlogic
  module NamedScopes
    # Handles dynamically creating named scopes for associations.
    module Associations
      def condition?(name) # :nodoc:
        super || association_condition?(name) || association_alias_condition?(name)
      end
      
      def primary_condition_name(name) # :nodoc:
        if result = super
          result
        elsif association_condition?(name)
          name.to_sym
        elsif details = association_alias_condition_details(name)
          "#{details[:association]}_#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
        else
          nil
        end
      end
      
      # Is the name of the method a valid name for an association condition?
      def association_condition?(name)
        !association_condition_details(name).nil?
      end
      
      # Is the ane of the method a valie name for an association alias condition?
      # An alias being "gt" for "greater_than", etc.
      def association_alias_condition?(name)
        !association_alias_condition_details(name).nil?
      end
      
      # Leverages ActiveRecord's JoinDependency class to create a left outer join. Searchlogic uses left outer joins so that
      # records with no associations are included in the result when the association is optional. You can use this method
      # internally when creating your own named scopes that need joins. You need to do this because then ActiveRecord will
      # remove any duplicate joins for you when you chain named scopes that require the same join. If you are using a
      # LEFT OUTER JOIN and an INNER JOIN, ActiveRecord will add both to the query, causing SQL errors.
      #
      # Bottom line, this is convenience method that you can use when creating your own named scopes. Ex:
      #
      #   named_scope :orders_line_items_price_expensive, :joins => left_out_joins(:orders => :line_items), :conditions => "line_items.price > 100"
      #
      # Now your joins are consistent with Searchlogic allowing you to avoid SQL errors with duplicate joins.
      def left_outer_joins(association_name)
        ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect do |assoc|
          sql = assoc.association_join.strip
          sql.split(/LEFT OUTER JOIN/).delete_if { |join| join.strip.blank? }.collect { |join| "LEFT OUTER JOIN #{join.strip}"}
        end.flatten
      end
      
      private
        def method_missing(name, *args, &block)
          if details = association_condition_details(name)
            create_association_condition(details[:association], details[:column], details[:condition], args)
            send(name, *args)
          elsif details = association_alias_condition_details(name)
            create_association_alias_condition(details[:association], details[:column], details[:condition], args)
            send(name, *args)
          elsif details = association_ordering_condition_details(name)
            create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
            send(name, *args)
          else
            super
          end
        end
        
        def association_condition_details(name)
          associations = reflect_on_all_associations.collect { |assoc| assoc.name }
          if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
            {:association => $1, :column => $2, :condition => $3}
          end
        end
        
        def create_association_condition(association_name, column, condition, args)
          named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
        end
        
        def association_alias_condition_details(name)
          associations = reflect_on_all_associations.collect { |assoc| assoc.name }
          if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
            {:association => $1, :column => $2, :condition => $3}
          end
        end
        
        def create_association_alias_condition(association, column, condition, args)
          primary_condition = primary_condition(condition)
          alias_name = "#{association}_#{column}_#{condition}"
          primary_name = "#{association}_#{column}_#{primary_condition}"
          send(primary_name, *args) # go back to method_missing and make sure we create the method
          (class << self; self; end).class_eval { alias_method alias_name, primary_name }
        end
        
        def association_ordering_condition_details(name)
          associations = reflect_on_all_associations.collect { |assoc| assoc.name }
          if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
            {:order_as => $1, :association => $2, :column => $3}
          end
        end
        
        def create_association_ordering_condition(association_name, order_as, column, args)
          named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
        end
        
        def association_condition_options(association_name, association_condition, args)
          association = reflect_on_association(association_name.to_sym)
          scope = association.klass.send(association_condition, *args)
          scope_options = association.klass.named_scope_options(association_condition)
          arity = association.klass.named_scope_arity(association_condition)
          
          if !arity || arity == 0
            # The underlying condition doesn't require any parameters, so let's just create a simple
            # named scope that is based on a hash.
            options = scope.proxy_options
            add_left_outer_joins(options, association)
            options
          else
            # The underlying condition requires parameters, let's match the parameters it requires
            # and pass those onto the named scope. We can't use proxy_options because that returns the
            # result after a value has been passed.
            proc_args = []
            if arity > 0
              arity.times { |i| proc_args << "arg#{i}"}
            else
              positive_arity = arity * -1
              positive_arity.times do |i|
                if i == (positive_arity - 1)
                  proc_args << "*arg#{i}"
                else
                  proc_args << "arg#{i}"
                end
              end
            end
            eval <<-"end_eval"
              searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
                options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
                add_left_outer_joins(options, association)
                options
              }
            end_eval
          end
        end
        
        # In a named scope you have 2 options for adding joins: :include and :joins.
        #
        # :include will execute multiple queries for each association and instantiate objects for each association.
        # This is not what we want when we are searching. The only other option left is :joins. We can pass the
        # name of the association directly, but AR creates an INNER JOIN. If we are ordering by an association's
        # attribute, and that association is optional, the records without an association will be omitted. Again,
        # not what we want.
        #
        # So the only option left is to use :joins with manually written SQL. We can still have AR generate this SQL
        # for us by leveraging it's join dependency classes. Instead of using the InnerJoinDependency we use the regular
        # JoinDependency which creates a LEFT OUTER JOIN, which is what we want.
        #
        # The code below was extracted out of AR's add_joins! method and then modified.
        def add_left_outer_joins(options, association)
          joins = left_outer_joins(association.name)
          options[:joins] ||= []
          options[:joins] = joins + options[:joins]
        end
    end
  end
end
{'fraction_non_alphanumeric': 0.08622452047381396, 'fraction_numerical': 0.002044374962419578, 'mean_word_length': 2.540983606557377, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5553719', 'n_tokens_mistral': 4392, 'n_tokens_neox': 4337, 'n_words': 1307}