class Ransack::Adapters::ActiveRecord::Context

Public Instance Methods

alias_tracker() click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 127
def alias_tracker
  @join_dependency.send(:alias_tracker)
end
attribute_method?(str, klass = @klass) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 65
    def attribute_method?(str, klass = @klass)
      exists = false
      if ransackable_attribute?(str, klass)
        exists = true
      elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
        remainder = []
        found_assoc = nil
        while !found_assoc && remainder.unshift(segments.pop) &&
        segments.size > 0 do
          assoc, poly_class = unpolymorphize_association(
            segments.join(Constants::UNDERSCORE)
            )
          if found_assoc = get_association(assoc, klass)
            exists = attribute_method?(
              remainder.join(Constants::UNDERSCORE),
              poly_class || found_assoc.klass
              )
          end
        end
      end
      exists
    end

    def table_for(parent)
      parent.table
    end

    def klassify(obj)
      if Class === obj && ::ActiveRecord::Base > obj
        obj
      elsif obj.respond_to? :klass
        obj.klass
      elsif obj.respond_to? :base_klass
        obj.base_klass
      else
        raise ArgumentError, "Don't know how to klassify #{obj}"
      end
    end

    # All dependent Arel::Join nodes used in the search query.
    #
    # This could otherwise be done as `@object.arel.join_sources`, except
    # that ActiveRecord's build_joins sets up its own JoinDependency.
    # This extracts what we need to access the joins using our existing
    # JoinDependency to track table aliases.
    #
    def join_sources
      base, joins = begin
        alias_tracker = @object.alias_tracker
        constraints   = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)

        [
          Arel::SelectManager.new(@object.table),
          constraints
        ]
      end
      joins.each do |aliased_join|
        base.from(aliased_join)
      end
      base.join_sources
    end

    def alias_tracker
      @join_dependency.send(:alias_tracker)
    end

    def lock_association(association)
      @lock_associations << association
    end

    def remove_association(association)
      return if @lock_associations.include?(association)
      @join_dependency.instance_variable_get(:@join_root).children.delete_if { |stashed|
        stashed.eql?(association)
      }
      @object.joins_values.delete_if { |jd|
        jd.instance_variables.include?(:@join_root) &&
        jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
      }
    end

    # Build an Arel subquery that selects keys for the top query,
    # drawn from the first join association's foreign_key.
    #
    # Example: for an Article that has_and_belongs_to_many Tags
    #
    #   context = Article.search.context
    #   attribute = Attribute.new(context, "tags_name").tap do |a|
    #     context.bind(a, a.name)
    #   end
    #   context.build_correlated_subquery(attribute.parent).to_sql
    #
    #   # SELECT "articles_tags"."article_id" FROM "articles_tags"
    #   # INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
    #   # WHERE "articles_tags"."article_id" = "articles"."id"
    #
    # The WHERE condition on this query makes it invalid by itself,
    # because it is correlated to the primary key on the outer query.
    #
    def build_correlated_subquery(association)
      join_constraints = extract_joins(association)
      join_root = join_constraints.shift
      correlated_key = extract_correlated_key(join_root)
      subquery = Arel::SelectManager.new(association.base_klass)
      subquery.from(join_root.left)
      subquery.project(correlated_key)
      join_constraints.each do |j|
        subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
      end
      subquery.where(correlated_key.eq(primary_key))
    end

    def primary_key
      @object.table[@object.primary_key]
    end

    private

    def extract_correlated_key(join_root)
      case join_root
      when Arel::Nodes::OuterJoin
        # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
        if join_root.right.is_a?(Arel::Nodes::On)
          extract_correlated_key(join_root.right.expr)
        elsif join_root.left.is_a?(Arel::Nodes::On)
          extract_correlated_key(join_root.left.expr)
        else
          raise 'Ransack encountered an unexpected arel structure'
        end
      when Arel::Nodes::Equality
        pk = primary_key
        if join_root.left == pk
          join_root.right
        elsif join_root.right == pk
          join_root.left
        else
          nil
        end
      when Arel::Nodes::And
        extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
      else
        # eg parent was Arel::Nodes::And and the evaluated side was one of
        # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
        nil
      end
    end

    def get_parent_and_attribute_name(str, parent = @base)
      attr_name = nil

      if ransackable_attribute?(str, klassify(parent))
        attr_name = str
      elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
        remainder = []
        found_assoc = nil
        while remainder.unshift(segments.pop) && segments.size > 0 &&
        !found_assoc do
          assoc, klass = unpolymorphize_association(
            segments.join(Constants::UNDERSCORE)
            )
          if found_assoc = get_association(assoc, parent)
            join = build_or_find_association(
              found_assoc.name, parent, klass
              )
            parent, attr_name = get_parent_and_attribute_name(
              remainder.join(Constants::UNDERSCORE), join
              )
          end
        end
      end

      [parent, attr_name]
    end

    def get_association(str, parent = @base)
      klass = klassify parent
      ransackable_association?(str, klass) &&
      klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
    end

    def join_dependency(relation)
      if relation.respond_to?(:join_dependency) # Polyamorous enables this
        relation.join_dependency
      else
        build_joins(relation)
      end
    end

    # Checkout active_record/relation/query_methods.rb +build_joins+ for
    # reference. Lots of duplicated code maybe we can avoid it
    def build_joins(relation)
      buckets = relation.joins_values + relation.left_outer_joins_values

      buckets = buckets.group_by do |join|
        case join
        when String
          :string_join
        when Hash, Symbol, Array
          :association_join
        when Polyamorous::JoinDependency, Polyamorous::JoinAssociation
          :stashed_join
        when Arel::Nodes::Join
          :join_node
        else
          raise 'unknown class: %s' % join.class.name
        end
      end
      buckets.default = []
      association_joins         = buckets[:association_join]
      stashed_association_joins = buckets[:stashed_join]
      join_nodes                = buckets[:join_node].uniq
      string_joins              = buckets[:string_join].map(&:strip)
      string_joins.uniq!

      join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)

      alias_tracker = relation.alias_tracker(join_list)
      join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
      join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
      join_nodes.each do |join|
        join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
      end
      join_dependency
    end

    def convert_join_strings_to_ast(table, joins)
      joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
      joins.compact!
      joins
    end

    def build_or_find_association(name, parent = @base, klass = nil)
      find_association(name, parent, klass) or build_association(name, parent, klass)
    end

    def find_association(name, parent = @base, klass = nil)
      @join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
        assoc.reflection.name == name && assoc.table &&
        (@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
        (!klass || assoc.reflection.klass == klass)
      end
    end

    def build_association(name, parent = @base, klass = nil)
      jd = Polyamorous::JoinDependency.new(
        parent.base_klass,
        parent.table,
        Polyamorous::Join.new(name, @join_type, klass),
        @join_type
      )
      found_association = jd.instance_variable_get(:@join_root).children.last

      @associations_pot[found_association] = parent

      # TODO maybe we dont need to push associations here, we could loop
      # through the @associations_pot instead
      @join_dependency.instance_variable_get(:@join_root).children.push found_association

      # Builds the arel nodes properly for this association
      @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)

      # Leverage the stashed association functionality in AR
      @object = @object.joins(jd)
      found_association
    end

    def extract_joins(association)
      parent = @join_dependency.instance_variable_get(:@join_root)
      reflection = association.reflection
      join_constraints = association.join_constraints_with_tables(
                           parent.table,
                           parent.base_klass,
                           Arel::Nodes::OuterJoin,
                           @join_dependency.instance_variable_get(:@alias_tracker),
                           @tables_pot[association]
                         )
      join_constraints.to_a.flatten
    end
  end
end
build_association(name, parent = @base, klass = nil) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 308
def build_association(name, parent = @base, klass = nil)
  jd = Polyamorous::JoinDependency.new(
    parent.base_klass,
    parent.table,
    Polyamorous::Join.new(name, @join_type, klass),
    @join_type
  )
  found_association = jd.instance_variable_get(:@join_root).children.last

  @associations_pot[found_association] = parent

  # TODO maybe we dont need to push associations here, we could loop
  # through the @associations_pot instead
  @join_dependency.instance_variable_get(:@join_root).children.push found_association

  # Builds the arel nodes properly for this association
  @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)

  # Leverage the stashed association functionality in AR
  @object = @object.joins(jd)
  found_association
end
build_correlated_subquery(association) click to toggle source

Build an Arel subquery that selects keys for the top query, drawn from the first join association’s foreign_key.

Example: for an Article that has_and_belongs_to_many Tags

context = Article.search.context
attribute = Attribute.new(context, "tags_name").tap do |a|
  context.bind(a, a.name)
end
context.build_correlated_subquery(attribute.parent).to_sql

# SELECT "articles_tags"."article_id" FROM "articles_tags"
# INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
# WHERE "articles_tags"."article_id" = "articles"."id"

The WHERE condition on this query makes it invalid by itself, because it is correlated to the primary key on the outer query.

# File lib/ransack/adapters/active_record/context.rb, line 164
def build_correlated_subquery(association)
  join_constraints = extract_joins(association)
  join_root = join_constraints.shift
  correlated_key = extract_correlated_key(join_root)
  subquery = Arel::SelectManager.new(association.base_klass)
  subquery.from(join_root.left)
  subquery.project(correlated_key)
  join_constraints.each do |j|
    subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
  end
  subquery.where(correlated_key.eq(primary_key))
end
build_joins(relation) click to toggle source

Checkout active_record/relation/query_methods.rb build_joins for reference. Lots of duplicated code maybe we can avoid it

# File lib/ransack/adapters/active_record/context.rb, line 255
def build_joins(relation)
  buckets = relation.joins_values + relation.left_outer_joins_values

  buckets = buckets.group_by do |join|
    case join
    when String
      :string_join
    when Hash, Symbol, Array
      :association_join
    when Polyamorous::JoinDependency, Polyamorous::JoinAssociation
      :stashed_join
    when Arel::Nodes::Join
      :join_node
    else
      raise 'unknown class: %s' % join.class.name
    end
  end
  buckets.default = []
  association_joins         = buckets[:association_join]
  stashed_association_joins = buckets[:stashed_join]
  join_nodes                = buckets[:join_node].uniq
  string_joins              = buckets[:string_join].map(&:strip)
  string_joins.uniq!

  join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)

  alias_tracker = relation.alias_tracker(join_list)
  join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
  join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
  join_nodes.each do |join|
    join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
  end
  join_dependency
end
build_or_find_association(name, parent = @base, klass = nil) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 296
def build_or_find_association(name, parent = @base, klass = nil)
  find_association(name, parent, klass) or build_association(name, parent, klass)
end
convert_join_strings_to_ast(table, joins) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 290
def convert_join_strings_to_ast(table, joins)
  joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
  joins.compact!
  joins
end
evaluate(search, opts = {}) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 25
def evaluate(search, opts = {})
  viz = Visitor.new
  relation = @object.where(viz.accept(search.base))

  if search.sorts.any?
    relation = relation.except(:order)
    # Rather than applying all of the search's sorts in one fell swoop,
    # as the original implementation does, we apply one at a time.
    #
    # If the sort (returned by the Visitor above) is a symbol, we know
    # that it represents a scope on the model and we can apply that
    # scope.
    #
    # Otherwise, we fall back to the applying the sort with the "order"
    # method as the original implementation did. Actually the original
    # implementation used "reorder," which was overkill since we already
    # have a clean slate after "relation.except(:order)" above.
    viz.accept(search.sorts).each do |scope_or_sort|
      if scope_or_sort.is_a?(Symbol)
        relation = relation.send(scope_or_sort)
      else
        case Ransack.options[:postgres_fields_sort_option]
        when :nulls_first
          scope_or_sort = scope_or_sort.direction == :asc ? Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST") : Arel.sql("#{scope_or_sort.to_sql} NULLS LAST")
        when :nulls_last
          scope_or_sort = scope_or_sort.direction == :asc ? Arel.sql("#{scope_or_sort.to_sql} NULLS LAST") : Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST")
        when :nulls_always_first
          scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST")
        when :nulls_always_last
          scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS LAST")
        end

        relation = relation.order(scope_or_sort)
      end
    end
  end

  opts[:distinct] ? relation.distinct : relation
end
extract_correlated_key(join_root) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 183
def extract_correlated_key(join_root)
  case join_root
  when Arel::Nodes::OuterJoin
    # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
    if join_root.right.is_a?(Arel::Nodes::On)
      extract_correlated_key(join_root.right.expr)
    elsif join_root.left.is_a?(Arel::Nodes::On)
      extract_correlated_key(join_root.left.expr)
    else
      raise 'Ransack encountered an unexpected arel structure'
    end
  when Arel::Nodes::Equality
    pk = primary_key
    if join_root.left == pk
      join_root.right
    elsif join_root.right == pk
      join_root.left
    else
      nil
    end
  when Arel::Nodes::And
    extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
  else
    # eg parent was Arel::Nodes::And and the evaluated side was one of
    # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
    nil
  end
end
extract_joins(association) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 331
def extract_joins(association)
  parent = @join_dependency.instance_variable_get(:@join_root)
  reflection = association.reflection
  join_constraints = association.join_constraints_with_tables(
                       parent.table,
                       parent.base_klass,
                       Arel::Nodes::OuterJoin,
                       @join_dependency.instance_variable_get(:@alias_tracker),
                       @tables_pot[association]
                     )
  join_constraints.to_a.flatten
end
find_association(name, parent = @base, klass = nil) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 300
def find_association(name, parent = @base, klass = nil)
  @join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
    assoc.reflection.name == name && assoc.table &&
    (@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
    (!klass || assoc.reflection.klass == klass)
  end
end
get_association(str, parent = @base) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 239
def get_association(str, parent = @base)
  klass = klassify parent
  ransackable_association?(str, klass) &&
  klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
end
get_parent_and_attribute_name(str, parent = @base) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 212
  def get_parent_and_attribute_name(str, parent = @base)
    attr_name = nil

    if ransackable_attribute?(str, klassify(parent))
      attr_name = str
    elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
      remainder = []
      found_assoc = nil
      while remainder.unshift(segments.pop) && segments.size > 0 &&
      !found_assoc do
        assoc, klass = unpolymorphize_association(
          segments.join(Constants::UNDERSCORE)
          )
        if found_assoc = get_association(assoc, parent)
          join = build_or_find_association(
            found_assoc.name, parent, klass
            )
          parent, attr_name = get_parent_and_attribute_name(
            remainder.join(Constants::UNDERSCORE), join
            )
        end
      end
    end

    [parent, attr_name]
  end

  def get_association(str, parent = @base)
    klass = klassify parent
    ransackable_association?(str, klass) &&
    klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
  end

  def join_dependency(relation)
    if relation.respond_to?(:join_dependency) # Polyamorous enables this
      relation.join_dependency
    else
      build_joins(relation)
    end
  end

  # Checkout active_record/relation/query_methods.rb +build_joins+ for
  # reference. Lots of duplicated code maybe we can avoid it
  def build_joins(relation)
    buckets = relation.joins_values + relation.left_outer_joins_values

    buckets = buckets.group_by do |join|
      case join
      when String
        :string_join
      when Hash, Symbol, Array
        :association_join
      when Polyamorous::JoinDependency, Polyamorous::JoinAssociation
        :stashed_join
      when Arel::Nodes::Join
        :join_node
      else
        raise 'unknown class: %s' % join.class.name
      end
    end
    buckets.default = []
    association_joins         = buckets[:association_join]
    stashed_association_joins = buckets[:stashed_join]
    join_nodes                = buckets[:join_node].uniq
    string_joins              = buckets[:string_join].map(&:strip)
    string_joins.uniq!

    join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)

    alias_tracker = relation.alias_tracker(join_list)
    join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
    join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
    join_nodes.each do |join|
      join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
    end
    join_dependency
  end

  def convert_join_strings_to_ast(table, joins)
    joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
    joins.compact!
    joins
  end

  def build_or_find_association(name, parent = @base, klass = nil)
    find_association(name, parent, klass) or build_association(name, parent, klass)
  end

  def find_association(name, parent = @base, klass = nil)
    @join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
      assoc.reflection.name == name && assoc.table &&
      (@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
      (!klass || assoc.reflection.klass == klass)
    end
  end

  def build_association(name, parent = @base, klass = nil)
    jd = Polyamorous::JoinDependency.new(
      parent.base_klass,
      parent.table,
      Polyamorous::Join.new(name, @join_type, klass),
      @join_type
    )
    found_association = jd.instance_variable_get(:@join_root).children.last

    @associations_pot[found_association] = parent

    # TODO maybe we dont need to push associations here, we could loop
    # through the @associations_pot instead
    @join_dependency.instance_variable_get(:@join_root).children.push found_association

    # Builds the arel nodes properly for this association
    @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)

    # Leverage the stashed association functionality in AR
    @object = @object.joins(jd)
    found_association
  end

  def extract_joins(association)
    parent = @join_dependency.instance_variable_get(:@join_root)
    reflection = association.reflection
    join_constraints = association.join_constraints_with_tables(
                         parent.table,
                         parent.base_klass,
                         Arel::Nodes::OuterJoin,
                         @join_dependency.instance_variable_get(:@alias_tracker),
                         @tables_pot[association]
                       )
    join_constraints.to_a.flatten
  end
end
join_dependency(relation) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 245
def join_dependency(relation)
  if relation.respond_to?(:join_dependency) # Polyamorous enables this
    relation.join_dependency
  else
    build_joins(relation)
  end
end
join_sources() click to toggle source

All dependent Arel::Join nodes used in the search query.

This could otherwise be done as ‘@object.arel.join_sources`, except that ActiveRecord’s build_joins sets up its own JoinDependency. This extracts what we need to access the joins using our existing JoinDependency to track table aliases.

# File lib/ransack/adapters/active_record/context.rb, line 111
def join_sources
  base, joins = begin
    alias_tracker = @object.alias_tracker
    constraints   = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)

    [
      Arel::SelectManager.new(@object.table),
      constraints
    ]
  end
  joins.each do |aliased_join|
    base.from(aliased_join)
  end
  base.join_sources
end
klassify(obj) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 92
def klassify(obj)
  if Class === obj && ::ActiveRecord::Base > obj
    obj
  elsif obj.respond_to? :klass
    obj.klass
  elsif obj.respond_to? :base_klass
    obj.base_klass
  else
    raise ArgumentError, "Don't know how to klassify #{obj}"
  end
end
lock_association(association) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 131
def lock_association(association)
  @lock_associations << association
end
primary_key() click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 177
def primary_key
  @object.table[@object.primary_key]
end
relation_for(object) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 9
def relation_for(object)
  object.all
end
remove_association(association) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 135
def remove_association(association)
  return if @lock_associations.include?(association)
  @join_dependency.instance_variable_get(:@join_root).children.delete_if { |stashed|
    stashed.eql?(association)
  }
  @object.joins_values.delete_if { |jd|
    jd.instance_variables.include?(:@join_root) &&
    jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
  }
end
table_for(parent) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 88
def table_for(parent)
  parent.table
end
type_for(attr) click to toggle source
# File lib/ransack/adapters/active_record/context.rb, line 13
def type_for(attr)
  return nil unless attr && attr.valid?
  relation     = attr.arel_attribute.relation
  name         = attr.arel_attribute.name.to_s
  table        = relation.respond_to?(:table_name) ? relation.table_name : relation.name
  schema_cache = self.klass.connection.schema_cache
  unless schema_cache.send(:data_source_exists?, table)
    raise "No table named #{table} exists."
  end
  attr.klass.columns.find { |column| column.name == name }.type
end