# frozen_string_literal: true # encoding: utf-8 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].key?(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) else 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 ] 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. selector.store(operator, op_expr) 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