class Ransack::Adapters::ActiveRecord::Context
def alias_tracker
def alias_tracker @join_dependency.send(:alias_tracker) end
def attribute_method?(str, klass = @klass)
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 build_association(name, parent = @base, klass = nil)
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 build_correlated_subquery(association)
because it is correlated to the primary key on the outer query.
The WHERE condition on this query makes it invalid by itself,
# WHERE "articles_tags"."article_id" = "articles"."id"
# INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
# SELECT "articles_tags"."article_id" FROM "articles_tags"
context.build_correlated_subquery(attribute.parent).to_sql
end
context.bind(a, a.name)
attribute = Attribute.new(context, "tags_name").tap do |a|
context = Article.search.context
Example: for an Article that has_and_belongs_to_many Tags
drawn from the first join association's foreign_key.
Build an Arel subquery that selects keys for the top 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 build_joins(relation)
Checkout active_record/relation/query_methods.rb +build_joins+ for
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 = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, 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 build_or_find_association(name, parent = @base, klass = nil)
def build_or_find_association(name, parent = @base, klass = nil) find_association(name, parent, klass) or build_association(name, parent, klass) end
def convert_join_strings_to_ast(table, joins)
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 evaluate(search, opts = {})
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
def extract_correlated_key(join_root)
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 extract_joins(association)
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
def find_association(name, parent = @base, klass = nil)
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 get_association(str, parent = @base)
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 get_parent_and_attribute_name(str, parent = @base)
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 join_dependency(relation)
def join_dependency(relation) if relation.respond_to?(:join_dependency) # Polyamorous enables this relation.join_dependency else build_joins(relation) end end
def join_sources
JoinDependency to track table aliases.
This extracts what we need to access the joins using our existing
that ActiveRecord's build_joins sets up its own JoinDependency.
This could otherwise be done as `@object.arel.join_sources`, except
All dependent Arel::Join nodes used in the search query.
def join_sources base, joins = begin alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, @object.table.name, []) 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 klassify(obj)
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
def lock_association(association)
def lock_association(association) @lock_associations << association end
def primary_key
def primary_key @object.table[@object.primary_key] end
def relation_for(object)
def relation_for(object) object.all end
def remove_association(association)
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
def table_for(parent)
def table_for(parent) parent.table end
def type_for(attr)
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 = 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