require 'ransack/context'
require 'ransack/adapters/active_record/compat'
require 'polyamorous'
module Ransack
module Adapters
module ActiveRecord
class Context < ::Ransack::Context
# Because the AR::Associations namespace is insane
if defined? ::ActiveRecord::Associations::JoinDependency
JoinDependency = ::ActiveRecord::Associations::JoinDependency
end
def initialize(object, options = {})
super
@arel_visitor = @engine.connection.visitor
end
def relation_for(object)
object.all
end
def type_for(attr)
return nil unless attr && attr.valid?
name = attr.arel_attribute.name.to_s
table = attr.arel_attribute.relation.table_name
schema_cache = @engine.connection.schema_cache
unless schema_cache.send(database_table_exists?, table)
raise "No table named #{table} exists."
end
schema_cache.columns_hash(table)[name].type
end
def evaluate(search, opts = {})
viz = Visitor.new
relation = @object.where(viz.accept(search.base))
if search.sorts.any?
relation = relation.except(:order).reorder(viz.accept(search.sorts))
end
opts[:distinct] ? relation.distinct : relation
end
def attribute_method?(str, klass = @klass)
exists = false
if ransackable_attribute?(str, klass)
exists = true
elsif (segments = str.split(/_/)).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
if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
def join_associations
raise NotImplementedError,
"ActiveRecord 4.1 and later does not use join_associations. Use join_sources."
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 =
if ::ActiveRecord::VERSION::MAJOR >= 5
[
Arel::SelectManager.new(@object.table),
@join_dependency.join_constraints(@object.joins_values, @join_type)
]
else
[
Arel::SelectManager.new(@object.engine, @object.table),
@join_dependency.join_constraints(@object.joins_values)
]
end
joins.each do |aliased_join|
base.from(aliased_join)
end
base.join_sources
end
else
# All dependent JoinAssociation items used in the search query.
#
# Deprecated: this goes away in ActiveRecord 4.1. Use join_sources.
#
def join_associations
@join_dependency.join_associations
end
def join_sources
base = Arel::SelectManager.new(@object.engine, @object.table)
joins = @object.joins_values
joins.each do |assoc|
assoc.join_to(base)
end
base.join_sources
end
end
def alias_tracker
@join_dependency.alias_tracker
end
def lock_association(association)
@lock_associations << association
end
if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
def remove_association(association)
return if @lock_associations.include?(association)
@join_dependency.join_root.children.delete_if { |stashed|
stashed.eql?(association)
}
@object.joins_values.delete_if { |jd|
jd.join_root.children.map(&:object_id) == [association.object_id]
}
end
else
def remove_association(association)
return if @lock_associations.include?(association)
@join_dependency.join_parts.delete(association)
@object.joins_values.delete(association)
end
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
join_table = join_root.left
correlated_key = join_root.right.expr.left
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 database_table_exists?
if ::ActiveRecord::VERSION::MAJOR >= 5
:data_source_exists?
else
:table_exists?
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.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).uniq
join_list =
if ::ActiveRecord::VERSION::MAJOR >= 5
join_nodes +
convert_join_strings_to_ast(relation.table, string_joins)
else
relation.send :custom_join_ast,
relation.table.from(relation.table), string_joins
end
join_dependency = JoinDependency.new(
relation.klass, association_joins, join_list
)
join_nodes.each do |join|
join_dependency.alias_tracker.aliases[join.left.name.downcase] = 1
end
if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
join_dependency
else
join_dependency.graft(*stashed_association_joins)
end
end
def convert_join_strings_to_ast(table, joins)
joins
.flatten
.reject(&:blank?)
.map { |join| table.create_string_join(Arel.sql(join)) }
end
def build_or_find_association(name, parent = @base, klass = nil)
find_association(name, parent, klass) or build_association(name, parent, klass)
end
if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
def find_association(name, parent = @base, klass = nil)
@join_dependency.join_root.children.detect do |assoc|
assoc.reflection.name == name &&
(@associations_pot.empty? || @associations_pot[assoc] == parent) &&
(!klass || assoc.reflection.klass == klass)
end
end
def build_association(name, parent = @base, klass = nil)
jd = JoinDependency.new(
parent.base_klass,
Polyamorous::Join.new(name, @join_type, klass),
[]
)
found_association = jd.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.join_root.children.push found_association
# Builds the arel nodes properly for this association
@join_dependency.send(
:construct_tables!, jd.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.join_root
reflection = association.reflection
join_constraints = association.join_constraints(
parent.table,
parent.base_klass,
association,
Arel::Nodes::OuterJoin,
association.tables,
reflection.scope_chain,
reflection.chain
)
join_constraints.to_a.flatten
end
else
def build_association(name, parent = @base, klass = nil)
@join_dependency.send(
:build,
Polyamorous::Join.new(name, @join_type, klass),
parent
)
found_association = @join_dependency.join_associations.last
# Leverage the stashed association functionality in AR
@object = @object.joins(found_association)
found_association
end
def extract_joins(association)
query = Arel::SelectManager.new(association.base_klass, association.table)
association.join_to(query).join_sources
end
def find_association(name, parent = @base, klass = nil)
found_association = @join_dependency.join_associations
.detect do |assoc|
assoc.reflection.name == name &&
assoc.parent == parent &&
(!klass || assoc.reflection.klass == klass)
end
end
end
end
end
end
end