Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect negative conditions for collection associations #645

Merged
merged 11 commits into from
Mar 8, 2016
4 changes: 2 additions & 2 deletions lib/ransack/adapters/active_record.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
require 'ransack/adapters/active_record/base'
ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base

require 'ransack/adapters/active_record/context'

case ActiveRecord::VERSION::STRING
when /^3\.0\./
require 'ransack/adapters/active_record/3.0/context'
when /^3\.1\./
require 'ransack/adapters/active_record/3.1/context'
when /^3\.2\./
require 'ransack/adapters/active_record/3.2/context'
else
require 'ransack/adapters/active_record/context'
end
6 changes: 4 additions & 2 deletions lib/ransack/adapters/active_record/3.0/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ module Ransack
module Adapters
module ActiveRecord
class Context < ::Ransack::Context

# Because the AR::Associations namespace is insane
JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
JoinBase = JoinDependency::JoinBase
if defined? ::ActiveRecord::Associations::ClassMethods::JoinDependency
JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
end

# Redefine a few things for ActiveRecord 3.0.

Expand Down
3 changes: 0 additions & 3 deletions lib/ransack/adapters/active_record/3.1/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ module Ransack
module Adapters
module ActiveRecord
class Context < ::Ransack::Context
# Because the AR::Associations namespace is insane
JoinDependency = ::ActiveRecord::Associations::JoinDependency
JoinPart = JoinDependency::JoinPart

# Redefine a few things for ActiveRecord 3.1.

Expand Down
158 changes: 119 additions & 39 deletions lib/ransack/adapters/active_record/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ module ActiveRecord
class Context < ::Ransack::Context

# Because the AR::Associations namespace is insane
JoinDependency = ::ActiveRecord::Associations::JoinDependency
JoinPart = JoinDependency::JoinPart
if defined? ::ActiveRecord::Associations::JoinDependency
JoinDependency = ::ActiveRecord::Associations::JoinDependency
end

def initialize(object, options = {})
super
Expand Down Expand Up @@ -137,6 +138,64 @@ 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?
Expand Down Expand Up @@ -242,65 +301,86 @@ def convert_join_strings_to_ast(table, joins)
.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 build_or_find_association(name, parent = @base, klass = nil)
found_association = @join_dependency.join_root.children
.detect do |assoc|
def find_association(name, parent = @base, klass = nil)
@join_dependency.join_root.children.detect do |assoc|
assoc.reflection.name == name &&
(@associations_pot.nil? || @associations_pot[assoc] == parent) &&
(@associations_pot.empty? || @associations_pot[assoc] == parent) &&
(!klass || assoc.reflection.klass == klass)
end
end

unless found_association
jd = JoinDependency.new(
parent.base_klass,
Polyamorous::Join.new(name, @join_type, klass),
[]
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
)
found_association = jd.join_root.children.last
associations 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)

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

def associations(assoc, parent)
@associations_pot ||= {}
@associations_pot[assoc] = parent
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_or_find_association(name, parent = @base, klass = nil)
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
unless found_association
@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)
end
found_association
end

end
Expand Down
4 changes: 3 additions & 1 deletion lib/ransack/adapters/active_record/ransack/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def initialize(object, options = {})
@join_dependency = join_dependency(@object)
@join_type = options[:join_type] || Polyamorous::OuterJoin
@search_key = options[:search_key] || Ransack.options[:search_key]
@associations_pot = {}
@lock_associations = []

if ::ActiveRecord::VERSION::STRING >= Constants::RAILS_4_1
@base = @join_dependency.join_root
Expand All @@ -40,7 +42,7 @@ def initialize(object, options = {})
@base.table_name, as: @base.aliased_table_name, type_caster: self
)
@bind_pairs = Hash.new do |hash, key|
parent, attr_name = get_parent_and_attribute_name(key.to_s)
parent, attr_name = get_parent_and_attribute_name(key)
if parent && attr_name
hash[key] = [parent, attr_name]
end
Expand Down
48 changes: 20 additions & 28 deletions lib/ransack/adapters/active_record/ransack/nodes/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,33 @@ module Nodes
class Condition

def arel_predicate
arel_predicate_for(attributes_array)
attributes.map { |attribute|
association = attribute.parent
if negative? && attribute.associated_collection?
query = context.build_correlated_subquery(association)
query.where(format_predicate(attribute).not)
context.remove_association(association)
Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
else
format_predicate(attribute)
end
}.reduce(combinator_method)
end

private

def attributes_array
attributes.map do |a|
a.attr.send(
arel_predicate_for_attribute(a), formatted_values_for_attribute(a)
)
end
end

def arel_predicate_for(predicates)
if predicates.size > 1
combinator_for(predicates)
else
format_predicate(predicates.first)
end
end

def combinator_for(predicates)
if combinator === Constants::AND
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(predicates))
elsif combinator === Constants::OR
predicates.inject(&:or)
end
def combinator_method
combinator === Constants::OR ? :or : :and
end

def format_predicate(predicate)
predicate.tap do
if casted_array_with_in_predicate?(predicate)
predicate.right[0] = format_values_for(predicate.right[0])
end
def format_predicate(attribute)
arel_pred = arel_predicate_for_attribute(attribute)
arel_values = formatted_values_for_attribute(attribute)
predicate = attribute.attr.public_send(arel_pred, arel_values)
if casted_array_with_in_predicate?(predicate)
predicate.right[0] = format_values_for(predicate.right[0])
end
predicate
end

def casted_array_with_in_predicate?(predicate)
Expand Down
12 changes: 8 additions & 4 deletions lib/ransack/adapters/mongoid/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ module Adapters
module Mongoid
class Context < ::Ransack::Context

# Because the AR::Associations namespace is insane
# JoinDependency = ::Mongoid::Associations::JoinDependency
# JoinPart = JoinDependency::JoinPart

def initialize(object, options = {})
super
# @arel_visitor = @engine.connection.visitor
Expand Down Expand Up @@ -100,6 +96,14 @@ def klassify(obj)
end
end

def lock_association(association)
warn "lock_association is not implemented for Ransack mongoid adapter" if $DEBUG
end

def remove_association(association)
warn "remove_association is not implemented for Ransack mongoid adapter" if $DEBUG
end

private

def get_parent_and_attribute_name(str, parent = @base)
Expand Down
1 change: 1 addition & 0 deletions lib/ransack/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def scope_arity(scope)
end

def bind(object, str)
return nil unless str
object.parent, object.attr_name = @bind_pairs[str]
end

Expand Down
5 changes: 4 additions & 1 deletion lib/ransack/nodes/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def initialize(context, name = nil, ransacker_args = [])

def name=(name)
@name = name
context.bind(self, name) unless name.blank?
end

def valid?
Expand All @@ -25,6 +24,10 @@ def valid?
.include?(attr_name.split('.').last)
end

def associated_collection?
parent.respond_to?(:reflection) && parent.reflection.collection?
end

def type
if ransacker
return ransacker.type
Expand Down
Loading