module Mongoid::Criteria::Queryable::Mergeable

Contains behavior for merging existing selection with new selection.

Attributes

strategy[RW]

@attribute [rw] strategy The name of the current strategy.

Public Instance Methods

intersect() click to toggle source

Instruct the next mergeable call to use intersection.

@example Use intersection on the next call.

mergeable.intersect.in(field: [ 1, 2, 3 ])

@return [ Mergeable ] The intersect flagged mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 22
def intersect
  use(:__intersect__)
end
override() click to toggle source

Instruct the next mergeable call to use override.

@example Use override on the next call.

mergeable.override.in(field: [ 1, 2, 3 ])

@return [ Mergeable ] The override flagged mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 34
def override
  use(:__override__)
end
reset_strategies!() click to toggle source

Clear the current strategy and negating flag, used after cloning.

@example Reset the strategies.

mergeable.reset_strategies!

@return [ Criteria ] self.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 58
def reset_strategies!
  self.strategy = nil
  self.negating = nil
  self
end
union() click to toggle source

Instruct the next mergeable call to use union.

@example Use union on the next call.

mergeable.union.in(field: [ 1, 2, 3 ])

@return [ Mergeable ] The union flagged mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 46
def union
  use(:__union__)
end

Private Instance Methods

__add__(criterion, operator) click to toggle source

Adds the criterion to the existing selection.

@api private

@example Add the criterion.

mergeable.__add__({ name: 1 }, "$in")

@param [ Hash ] criterion The criteria. @param [ String ] operator The MongoDB operator.

@return [ Mergeable ] The new mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 79
def __add__(criterion, operator)
  with_strategy(:__add__, criterion, operator)
end
__expanded__(criterion, outer, inner) click to toggle source

Adds the criterion to the existing selection.

@api private

@example Add the criterion.

mergeable.__expanded__([ 1, 10 ], "$within", "$center")

@param [ Hash ] criterion The criteria. @param [ String ] outer The outer MongoDB operator. @param [ String ] inner The inner MongoDB operator.

@return [ Mergeable ] The new mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 97
def __expanded__(criterion, outer, inner)
  selection(criterion) do |selector, field, value|
    selector.store(field, { outer => { inner => value }})
  end
end
__intersect__(criterion, operator) click to toggle source

Adds the criterion to the existing selection.

@api private

@example Add the criterion.

mergeable.__intersect__([ 1, 2 ], "$in")

@param [ Hash ] criterion The criteria. @param [ String ] operator The MongoDB operator.

@return [ Mergeable ] The new mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 135
def __intersect__(criterion, operator)
  with_strategy(:__intersect__, criterion, operator)
end
__merge__(criterion) click to toggle source

Perform a straight merge of the criterion into the selection and let the symbol overrides do all the work.

@api private

@example Straight merge the expanded criterion.

mergeable.__merge__(location: [ 1, 10 ])

@param [ Hash ] criterion The criteria.

@return [ Mergeable ] The cloned object.

@since 2.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 116
def __merge__(criterion)
  selection(criterion) do |selector, field, value|
    selector.merge!(field.__expr_part__(value))
  end
end
__multi__(criteria, operator) click to toggle source

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 [ 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

# File lib/mongoid/criteria/queryable/mergeable.rb, line 157
def __multi__(criteria, operator)
  clone.tap do |query|
    sel = query.selector
    criteria.flatten.each do |expr|
      next unless expr
      result_criteria = sel[operator] || []
      if expr.is_a?(Selectable)
        expr = expr.selector
      end
      normalized = _mongoid_expand_keys(expr)
      sel.store(operator, result_criteria.push(normalized))
    end
  end
end
__override__(criterion, operator) click to toggle source

Adds the criterion to the existing selection.

@api private

@example Add the criterion.

mergeable.__override__([ 1, 2 ], "$in")

@param [ Hash | Criteria ] criterion The criteria. @param [ String ] operator The MongoDB operator.

@return [ Mergeable ] The new mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 342
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))
    else
      selector.store(field, expression)
    end
  end
end
__union__(criterion, operator) click to toggle source

Adds the criterion to the existing selection.

@api private

@example Add the criterion.

mergeable.__union__([ 1, 2 ], "$in")

@param [ Hash ] criterion The criteria. @param [ String ] operator The MongoDB operator.

@return [ Mergeable ] The new mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 370
def __union__(criterion, operator)
  with_strategy(:__union__, criterion, operator)
end
_mongoid_add_top_level_operation(operator, criteria) click to toggle source

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

# File lib/mongoid/criteria/queryable/mergeable.rb, line 182
        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_expand_keys(criterion.selector)
      else
        expr = _mongoid_expand_keys(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
_mongoid_expand_keys(expr) click to toggle source

Takes a criteria hash and expands Key objects into hashes containing MQL corresponding to said key objects. Also converts the input to BSON::Document to permit indifferent access.

The argument must be a hash containing key-value pairs of the following forms:

  • {field_name: value}

  • {‘field_name’ => value}

  • {key_instance: value}

  • {:$operator => operator_value_expression}

  • {‘$operator’ => operator_value_expression}

Ruby does not permit multiple symbol operators. For example, {:foo.gt => 1, :foo.gt => 2} is collapsed to {:foo.gt => 2} by the language. Therefore this method never has to deal with multiple identical operators.

Similarly, this method should never need to expand a literal value and an operator at the same time.

This method effectively converts symbol keys to string keys in the input expr, such that the downstream code can assume that conditions always contain string keys.

@param [ Hash ] expr Criteria including Key instances.

@return [ BSON::Document ] The expanded criteria.

# File lib/mongoid/criteria/queryable/mergeable.rb, line 256
        def _mongoid_expand_keys(expr)
  unless expr.is_a?(Hash)
    raise ArgumentError, 'Argument must be a Hash'
  end

  result = BSON::Document.new
  expr.each do |field, value|
    field.__expr_part__(value.__expand_complex__, negating?).each do |k, v|
      if existing = result[k]
        if existing.is_a?(Hash)
          # Existing value is an operator.
          # If new value is also an operator, ensure there are no
          # conflicts and add
          if v.is_a?(Hash)
            # The new value is also an operator.
            # If there are no conflicts, combine the hashes, otherwise
            # add new conditions to top level with $and.
            if (v.keys & existing.keys).empty?
              existing.update(v)
            else
              raise NotImplementedError, 'Ruby does not allow same symbol operator with different values'
              result['$and'] ||= []
              result['$and'] << {k => v}
            end
          else
            # The new value is a simple value.
            # Transform the implicit equality to either $eq or $regexp
            # depending on the type of the argument. See
            # https://docs.mongodb.com/manual/reference/operator/query/eq/#std-label-eq-usage-examples
            # for the description of relevant server behavior.
            op = case v
            when Regexp, BSON::Regexp::Raw
              '$regex'
            else
              '$eq'
            end
            # If there isn't an $eq/$regex operator already in the
            # query, transform the new value into an operator
            # expression and add it to the existing hash. Otherwise
            # add the new condition with $and to the top level.
            if existing.key?(op)
              raise NotImplementedError, 'Ruby does not allow same symbol operator with different values'
              result['$and'] ||= []
              result['$and'] << {k => v}
            else
              existing.update(op => v)
            end
          end
        else
          # Existing value is a simple value.
          # See the notes above about transformations to $eq/$regex.
          op = case existing
          when Regexp, BSON::Regexp::Raw
            '$regex'
          else
            '$eq'
          end
          if v.is_a?(Hash) && !v.key?(op)
            result[k] = {op => existing}.update(v)
          else
            raise NotImplementedError, 'Ruby does not allow same symbol operator with different values'
            result['$and'] ||= []
            result['$and'] << {k => v}
          end
        end
      else
        result[k] = v
      end
    end
  end
  result
end
_mongoid_flatten_arrays(array) click to toggle source

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.

# File lib/mongoid/criteria/queryable/mergeable.rb, line 213
        def _mongoid_flatten_arrays(array)
  out = []
  pending = array.dup
  until pending.empty?
    item = pending.shift
    if item.nil?
      # skip
    elsif item.is_a?(Array)
      pending += item
    else
      out << item
    end
  end
  out
end
prepare(field, operator, value) click to toggle source

Prepare the value for merging.

@api private

@example Prepare the value.

mergeable.prepare("field", "$gt", 10)

@param [ String ] field The name of the field. @param [ Object ] value The value.

@return [ Object ] The serialized value.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 428
def prepare(field, operator, value)
  unless operator =~ /exists|type|size/
    value = value.__expand_complex__
    field = field.to_s
    name = aliases[field] || field
    serializer = serializers[name]
    value = serializer ? serializer.evolve(value) : value
  end
  selection = { operator => value }
  negating? ? { "$not" => selection } : selection
end
use(strategy) click to toggle source

Use the named strategy for the next operation.

@api private

@example Use intersection.

mergeable.use(:__intersect__)

@param [ Symbol ] strategy The strategy to use.

@return [ Mergeable ] The existing mergeable.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 386
def use(strategy)
  tap do |mergeable|
    mergeable.strategy = strategy
  end
end
with_strategy(strategy, criterion, operator) click to toggle source

Add criterion to the selection with the named strategy.

@api private

@example Add criterion with a strategy.

mergeable.with_strategy(:__union__, {field_name: [ 1, 2, 3 ]}, "$in")

@param [ Symbol ] strategy The name of the strategy method. @param [ Object ] criterion The criterion to add. @param [ String ] operator The MongoDB operator.

@return [ Mergeable ] The cloned query.

@since 1.0.0

# File lib/mongoid/criteria/queryable/mergeable.rb, line 406
def with_strategy(strategy, criterion, operator)
  selection(criterion) do |selector, field, value|
    selector.store(
      field,
      selector[field].send(strategy, prepare(field, operator, value))
    )
  end
end