khiav223577/left_joins

View on GitHub
lib/left_joins.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
94%
require "left_joins/version"
require 'active_record'
require 'active_record/relation'

module LeftJoins
  IS_RAILS3_FLAG = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
  HAS_BUILT_IN_LEFT_JOINS_METHOD = ActiveRecord::QueryMethods.method_defined?(:left_outer_joins)
  require 'left_joins_for_rails_3' if IS_RAILS3_FLAG

  class << self
    def patch(target, method, as:)
      return yield if target.singleton_methods.include?(method)

      target.define_singleton_method(method, &as)
      result = yield
      target.singleton_class.send(:remove_method, method)

      return result
    end
  end
end

module ActiveRecord::QueryMethods
  if not LeftJoins::HAS_BUILT_IN_LEFT_JOINS_METHOD
    # ----------------------------------------------------------------
    # ● Storing left joins values into @left_outer_joins_values
    # ----------------------------------------------------------------
    attr_writer :left_outer_joins_values
    def left_outer_joins_values
      @left_outer_joins_values ||= []
    end

    def left_outer_joins(*args)
      check_if_method_has_arguments!(:left_outer_joins, args)

      args.compact!
      args.flatten!
      self.distinct_value = false if self.distinct_value == nil

      return (LeftJoins::IS_RAILS3_FLAG ? clone : spawn).left_outer_joins!(*args)
    end

    def left_outer_joins!(*args)
      left_outer_joins_values.concat(args)
      self
    end

    # ----------------------------------------------------------------
    # ● Implement left joins when building arel
    # ----------------------------------------------------------------
    alias_method :left_joins, :left_outer_joins
    alias_method :build_arel_without_outer_joins, :build_arel
    def build_arel(*args)
      arel = build_arel_without_outer_joins(*args)
      build_left_outer_joins(arel, @left_outer_joins_values) if @left_outer_joins_values
      return arel
    end

    alias_method :build_joins_without_join_type, :build_joins
    def build_joins(manager, joins, join_type = nil)
      Thread.current.thread_variable_set(:left_joins_join_type, join_type)
      result = build_joins_without_join_type(manager, joins)
      Thread.current.thread_variable_set(:left_joins_join_type, nil)
      return result
    end

    def build_left_outer_joins(manager, joins)
      result = build_joins(manager, joins, Arel::Nodes::OuterJoin)
      return result
    end

    class << ::ActiveRecord::Base
      def left_joins(*args)
        self.where('').left_joins(*args)
      end
      alias_method :left_outer_joins, :left_joins
    end

    class ::ActiveRecord::Associations::JoinDependency
      if private_method_defined?(:make_constraints)
        alias_method :make_constraints_without_hooking_join_type, :make_constraints
        def make_constraints(*args, join_type)
          join_type = Thread.current.thread_variable_get(:left_joins_join_type) || join_type
          return make_constraints_without_hooking_join_type(*args, join_type)
        end
      else
        alias_method :build_without_hooking_join_type, :build
        def build(associations, parent = nil, join_type = Arel::Nodes::InnerJoin)
          join_type = Thread.current.thread_variable_get(:left_joins_join_type) || join_type
          return build_without_hooking_join_type(associations, parent, join_type)
        end
      end
    end

    module ActiveRecord::Calculations
      # This method is copied from activerecord-4.2.10/lib/active_record/relation/calculations.rb
      # and modified this line `distinct = true` to `distinct = true if distinct == nil`
      def perform_calculation(operation, column_name, options = {})
        # TODO: Remove options argument as soon we remove support to
        # activerecord-deprecated_finders.
        operation = operation.to_s.downcase

        # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
        distinct = options[:distinct] || self.distinct_value

        if operation == "count"
          column_name ||= (select_for_count || :all)

          unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
            distinct = true if distinct == nil
          end

          column_name = primary_key if column_name == :all && distinct
          distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
        end

        if group_values.any?
          execute_grouped_calculation(operation, column_name, distinct)
        else
          execute_simple_calculation(operation, column_name, distinct)
        end
      end
    end
  end
end

# ----------------------------------------------------------------
# ● Implement left joins when merging relations
# ----------------------------------------------------------------
if not LeftJoins::IS_RAILS3_FLAG
  require 'active_record/relation/merger'
  class ActiveRecord::Relation
    class Merger
      alias_method :merge_without_left_joins, :merge
      def merge
        values = other.left_outer_joins_values
        relation.left_outer_joins!(*values) if values.present?
        return merge_without_left_joins
      end
    end
  end

  module ActiveRecord
    module SpawnMethods

      private

      alias_method :relation_with_without_left_joins, :relation_with
      def relation_with(values) # :nodoc:
        result = relation_with_without_left_joins(values)
        result.left_outer_joins_values = self.left_outer_joins_values
        return result
      end
    end
  end
end

# ----------------------------------------------------------------
# ● Implement left joins in update statement
# ----------------------------------------------------------------
module ActiveRecord
  class Relation
    if not LeftJoins::HAS_BUILT_IN_LEFT_JOINS_METHOD
      alias_method :update_all_without_left_joins_values, :update_all

      def update_all(*args)
        local_joins_values = joins_values.clone
        has_left_outer_joins = left_outer_joins_values.any?

        LeftJoins.patch(self, :joins_values, as: ->{ local_joins_values }) do
          LeftJoins.patch(local_joins_values, :any?, as: ->{ super() || has_left_outer_joins }) do
            update_all_without_left_joins_values(*args)
          end
        end
      end
    end
  end
end