lib/mongoid/criteria/queryable/storable.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
class Criteria
module Queryable
# This module encapsulates methods that write query expressions into
# the Criteria's selector.
#
# The query expressions must have already been expanded as necessary.
# The methods of this module do not perform processing on expression
# values.
#
# Methods in this module do not handle negation - if negation is needed,
# it must have already been handled upstream of these methods.
#
# @api private
module Storable
# Adds a field expression to the query.
#
# +field+ must be a field name, and it must be a string. The upstream
# code must have converted other field/key types to the simple string
# form by the time this method is invoked.
#
# +value+ can be of any type, it is written into the selector unchanged.
#
# This method performs no processing on the provided field value.
#
# Mutates the receiver.
#
# @param [ String ] field The field name.
# @param [ Object ] value The field value.
#
# @return [ Storable ] self.
def add_field_expression(field, value)
unless field.is_a?(String)
raise ArgumentError, "Field must be a string: #{field}"
end
if field.start_with?('$')
raise ArgumentError, "Field cannot be an operator (i.e. begin with $): #{field}"
end
if selector[field]
# We already have a restriction by the field we are trying
# to restrict, combine the restrictions.
if value.is_a?(Hash) && selector[field].is_a?(Hash) &&
value.keys.all? { |key|
key_s = key.to_s
key_s.start_with?('$') && !selector[field].keys.map(&:to_s).include?(key_s)
}
then
# Multiple operators can be combined on the same field by
# adding them to the existing hash.
new_value = selector[field].merge(value)
selector.store(field, new_value)
elsif selector[field] != value
add_operator_expression('$and', [{field => value}])
end
else
selector.store(field, value)
end
self
end
# Adds a logical operator expression to the selector.
#
# This method only handles logical operators ($and, $nor and $or).
# It raises ArgumentError if called with another operator. Note that
# in MQL, $not is a field-level operator and not a query-level one,
# and therefore $not is not handled by this method.
#
# This method takes the operator and the operator value expression
# separately for callers' convenience. It can be considered to
# handle storing the hash +{operator => op_expr}+.
#
# If the selector consists of a single condition which is the specified
# operator (on the top level), the new condition given in op_expr is
# added to the existing conditions for the specified operator.
# For example, if the selector is currently:
#
# {'$or' => [{'hello' => 'world'}]}
#
# ... and operator is '$or' and op_expr is +[{'test' => 123'}]+,
# the resulting selector will be:
#
# {'$or' => [{'hello' => 'world'}, {'test' => 123}]}
#
# This method always adds the new conditions as additional requirements;
# in other words, it does not implement the ActiveRecord or/nor behavior
# where the receiver becomes one of the operands. It is expected that
# code upstream of this method implements such behavior.
#
# This method does not simplify values (i.e. if the selector is
# currently empty and operator is $and, op_expr is written to the
# selector with $and even if the $and can in principle be elided).
# Such simplification is also expected to have already been performed
# by the upstream code.
#
# This method mutates the receiver.
#
# @param [ String ] operator The operator to add.
# @param [ Array<Hash> ] op_expr Operator value to add.
#
# @return [ Storable ] self.
def add_logical_operator_expression(operator, op_expr)
unless operator.is_a?(String)
raise ArgumentError, "Operator must be a string: #{operator}"
end
unless %w($and $nor $or).include?(operator)
raise ArgumentError, "This method only handles logical operators ($and, $nor, $or). Operator given: #{operator}"
end
unless op_expr.is_a?(Array)
raise Errors::InvalidQuery, "#{operator} argument must be an array: #{Errors::InvalidQuery.truncate_expr(op_expr)}"
end
if selector.length == 1 && selector.keys.first == operator
new_value = selector.values.first + op_expr
selector.store(operator, new_value)
elsif operator == '$and' || selector.empty?
# $and can always be added to top level and it will be combined
# with whatever other conditions exist.
if current_value = selector[operator]
new_value = current_value + op_expr
selector.store(operator, new_value)
else
selector.store(operator, op_expr)
end
else
# Other operators need to be added separately
if selector[operator]
add_logical_operator_expression('$and', [operator => op_expr])
else
selector.store(operator, op_expr)
end
end
self
end
# Adds an operator expression to the selector.
#
# This method takes the operator and the operator value expression
# separately for callers' convenience. It can be considered to
# handle storing the hash +{operator => op_expr}+.
#
# The operator value can be of any type.
#
# If the selector already has the specified operator in it (on the
# top level), the new condition given in op_expr is added to the
# existing conditions for the specified operator. This is
# straightforward for $and; for other logical operators, the behavior
# of this method is to add the new conditions to the existing operator.
# For example, if the selector is currently:
#
# {'foo' => 'bar', '$or' => [{'hello' => 'world'}]}
#
# ... and operator is '$or' and op_expr is +{'test' => 123'}+,
# the resulting selector will be:
#
# {'foo' => 'bar', '$or' => [{'hello' => 'world'}, {'test' => 123}]}
#
# This does not implement an OR between the existing selector and the
# new operator expression - handling this is the job of upstream
# methods. This method simply stores op_expr into the selector on the
# assumption that the existing selector is the correct left hand side
# of the operation already.
#
# For non-logical query-level operators like $where and $text, if
# there already is a top-level operator with the same name, the
# op_expr is added to the selector via a top-level $and operator,
# thus producing a selector having both operator values.
#
# This method does not simplify values (i.e. if the selector is
# currently empty and operator is $and, op_expr is written to the
# selector with $and even if the $and can in principle be elided).
#
# This method mutates the receiver.
#
# @param [ String ] operator The operator to add.
# @param [ Object ] op_expr Operator value to add.
#
# @return [ Storable ] self.
def add_operator_expression(operator, op_expr)
unless operator.is_a?(String)
raise ArgumentError, "Operator must be a string: #{operator}"
end
unless operator.start_with?('$')
raise ArgumentError, "Operator must begin with $: #{operator}"
end
if %w($and $nor $or).include?(operator)
return add_logical_operator_expression(operator, op_expr)
end
# For other operators, if the operator already exists in the
# query, add the new condition with $and, otherwise add the
# new condition to the top level.
if selector[operator]
add_logical_operator_expression('$and', [{operator => op_expr}])
else
selector.store(operator, op_expr)
end
self
end
# Adds an arbitrary expression to the query.
#
# Field can either be a field name or an operator.
#
# Mutates the receiver.
#
# @param [ String ] field Field name or operator name.
# @param [ Object ] value Field value or operator expression.
#
# @return [ Storable ] self.
def add_one_expression(field, value)
unless field.is_a?(String)
raise ArgumentError, "Field must be a string: #{field}"
end
if field.start_with?('$')
add_operator_expression(field, value)
else
add_field_expression(field, value)
end
end
end
end
end
end