Skip to content

Commit

Permalink
convert CONTAINS to use a visitor
Browse files Browse the repository at this point in the history
  • Loading branch information
kbrock committed Apr 14, 2020
1 parent 35ef423 commit b932ef2
Showing 1 changed file with 87 additions and 38 deletions.
125 changes: 87 additions & 38 deletions lib/miq_expression/query_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,87 @@
class MiqExpression
class QueryHelper
class QueryHelper < Arel::Visitors::Visitor
# rubocop:disable Naming/MethodName (the visitor pattern is defined with odd naming conventions)
if ActiveRecord.version.to_s < "5.2"
# Rails 5.2 Arel::Visitors::Visitor
def accept(object, collector = nil)
visit(object, collector)
end

# Rails 5.2 Arel::Visitors::Visitor
# rubocop:disable Style/BlockDelimiters
def visit(object, collector)
if collector
send(dispatch[object.class], object, collector)
else
send(dispatch[object.class], object)
end
rescue NoMethodError => e
raise e if respond_to?(dispatch[object.class], true)

superklass = object.class.ancestors.find { |klass|
respond_to?(dispatch[klass], true)
}
raise(TypeError, "Cannot visit #{object.class}") unless superklass

dispatch[object.class] = dispatch[superklass]
retry
end
# rubocop:enable Style/BlockDelimiters, Naming/UncommunicativeMethodParamName
end

def visit_NilClass(object, _collector)
object
end

def visit_Arel_Nodes_Equality(object, collector)
field, binds = collector
if field && (
(object.right == field.right && object.left == field.left) ||
(object.left == field.right && object.right == field.left)
)
# remove this node from the query
nil
elsif object.right.kind_of?(Arel::Nodes::BindParam) && binds.respond_to?(:shift)
# replace this node with "field = $(next value from the binds array)"
object.left.eq(binds.shift.value_for_database)
else
# leave this node in the query unchanged
object
end
end

def visit_Arel_Nodes_And(object, collector)
children = object.children.compact.flatten.map { |q| visit(q, collector) }
children.compact!

if children.blank?
nil
elsif children.size == 1
children.first
elsif children == object.children
object
else
Arel::Nodes::And.new(children)
end
end

def visit_Arel_Nodes_Grouping(object, collector)
expr = visit(object.expr, collector)
if expr == object.expr
object
elsif expr.nil?
nil
else
Arel::Nodes::Grouping.new(expr)
end
end

def punt(object, _collector)
object
end

# alias :visit_Arel_Attributes_Or :punt

# This method does 2 things:
# - remove equality node from the query
# - replace bind variables in the query
Expand All @@ -16,43 +98,10 @@ class QueryHelper
# @param bind [Array[ActiveModel::Attribute]] values to populate in the BindParam slots.
# @return query without the equality node
def self.remove_equality(query, node, binds = [])
traverse_and_replace(query) do |q|
if (q.left == node.left && q.right == node.right) ||
(q.right == node.left && q.left == node.right)
# remove this node from the query
nil
elsif q.right.kind_of?(Arel::Nodes::BindParam)
# replace this node with "field = $(next value from the binds array)"
q.left.eq(binds.shift.value_for_database)
else
# leave this node in the query unchanged
q
end
end
end

def self.traverse_and_replace(query, &bind)
return if query.nil?

if query.kind_of?(Arel::Nodes::And)
children = query.children.compact.flatten.map { |q| traverse_and_replace(q, &bind) }
children.compact!

if children.empty?
nil
elsif children.size == 1
children.first
else
query.class.new(children)
end
elsif query.kind_of?(Arel::Nodes::Grouping)
q2 = traverse_and_replace(query.expr, &bind)
q2 ? query.class.new(q2) : q2
elsif query.kind_of?(Arel::Nodes::Equality)
yield(query)
else
query
end
# visitor will return a value (not doing it right)
# We're using the standard collector as input (again, not doing it right)
new.accept(query, [node, binds]) unless query.nil?
end
end
# rubocop:enable Naming/MethodName
end

0 comments on commit b932ef2

Please sign in to comment.