lib/mongoid/criteria/queryable/mergeable.rb in mongoid-7.0.13 vs lib/mongoid/criteria/queryable/mergeable.rb in mongoid-7.1.0.rc0
- old
+ new
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
# encoding: utf-8
+
module Mongoid
class Criteria
module Queryable
- # Contains behaviour for merging existing selection with new selection.
+ # Contains behavior for merging existing selection with new selection.
module Mergeable
# @attribute [rw] strategy The name of the current strategy.
attr_accessor :strategy
@@ -48,16 +50,17 @@
# Clear the current strategy and negating flag, used after cloning.
#
# @example Reset the strategies.
# mergeable.reset_strategies!
#
- # @return [ nil ] nil.
+ # @return [ Criteria ] self.
#
# @since 1.0.0
def reset_strategies!
self.strategy = nil
self.negating = nil
+ self
end
private
# Adds the criterion to the existing selection.
@@ -131,38 +134,107 @@
# @since 1.0.0
def __intersect__(criterion, operator)
with_strategy(:__intersect__, criterion, operator)
end
- # Adds the criterion to the existing selection.
+ # Adds $and/$or/$nor criteria to a copy of this selection.
#
+ # Each of the criteria can be a Hash of key/value pairs or MongoDB
+ # operators (keys beginning with $), or a Selectable object
+ # (which typically will be a Criteria instance).
+ #
# @api private
#
# @example Add the criterion.
# mergeable.__multi__([ 1, 2 ], "$in")
#
- # @param [ Hash ] criterion The criteria.
+ # @param [ Array<Hash | Criteria> ] criteria Multiple key/value pair
+ # matches or Criteria objects.
# @param [ String ] operator The MongoDB operator.
#
# @return [ Mergeable ] The new mergeable.
#
# @since 1.0.0
- def __multi__(criterion, operator)
+ def __multi__(criteria, operator)
clone.tap do |query|
sel = query.selector
- criterion.flatten.each do |expr|
+ criteria.flatten.each do |expr|
next unless expr
- criteria = sel[operator] || []
- normalized = expr.inject({}) do |hash, (field, value)|
- hash.merge!(field.__expr_part__(value.__expand_complex__))
- hash
+ result_criteria = sel[operator] || []
+ if expr.is_a?(Selectable)
+ expr = expr.selector
end
- sel.store(operator, criteria.push(normalized))
+ normalized = _mongoid_normalize_expr(expr)
+ sel.store(operator, result_criteria.push(normalized))
end
end
end
+ # Combines criteria into a MongoDB selector.
+ #
+ # Criteria is an array of criterions which will be flattened.
+ #
+ # Each criterion can be:
+ # - A hash
+ # - A Criteria instance
+ # - nil, in which case it is ignored
+ #
+ # @api private
+ private def _mongoid_add_top_level_operation(operator, criteria)
+ # Flatten the criteria. The idea is that predicates in MongoDB
+ # are always hashes and are never arrays. This method additionally
+ # allows Criteria instances as predicates.
+ # The flattening is existing Mongoid behavior but we could possibly
+ # get rid of it as applications can splat their predicates, or
+ # flatten if needed.
+ clone.tap do |query|
+ sel = query.selector
+ _mongoid_flatten_arrays(criteria).each do |criterion|
+ if criterion.is_a?(Selectable)
+ expr = _mongoid_normalize_expr(criterion.selector)
+ else
+ expr = criterion
+ end
+ if sel.empty?
+ sel.store(operator, [expr])
+ elsif sel.keys == [operator]
+ sel.store(operator, sel[operator] + [expr])
+ else
+ operands = [sel.dup] + [expr]
+ sel.clear
+ sel.store(operator, operands)
+ end
+ end
+ end
+ end
+
+ # Calling .flatten on an array which includes a Criteria instance
+ # evaluates the criteria, which we do not want. Hence this method
+ # explicitly only expands Array objects and Array subclasses.
+ private def _mongoid_flatten_arrays(array)
+ out = []
+ pending = array
+ until pending.empty?
+ item = pending.shift
+ if item.nil?
+ # skip
+ elsif item.is_a?(Array)
+ pending += item
+ else
+ out << item
+ end
+ end
+ out
+ end
+
+ # @api private
+ private def _mongoid_normalize_expr(expr)
+ expr.inject({}) do |hash, (field, value)|
+ hash.merge!(field.__expr_part__(value.__expand_complex__))
+ end
+ end
+
# Adds the criterion to the existing selection.
#
# @api private
#
# @example Add the criterion.
@@ -173,9 +245,12 @@
#
# @return [ Mergeable ] The new mergeable.
#
# @since 1.0.0
def __override__(criterion, operator)
+ if criterion.is_a?(Selectable)
+ criterion = criterion.selector
+ end
selection(criterion) do |selector, field, value|
expression = prepare(field, operator, value)
existing = selector[field]
if existing.respond_to?(:merge!)
selector.store(field, existing.merge!(expression))