Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MONGOID-4862 Make any_of behave like or but not disjunct the receiver #4741

Merged
merged 2 commits into from Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 44 additions & 13 deletions docs/tutorials/mongoid-queries.txt
Expand Up @@ -365,9 +365,11 @@ The following calls all produce the same query conditions:
Operator Combinations
`````````````````````

As of Mongoid 7.1, logical operators have been changed to have the
the same semantics as `those of ActiveRecord
As of Mongoid 7.1, logical operators (``and``, ``or``, ``nor`` and ``not``)
have been changed to have the the same semantics as `those of ActiveRecord
<https://guides.rubyonrails.org/active_record_querying.html>`_.
To obtain the semantics of ``or`` as it behaved in Mongoid 7.0 and earlier,
use ``any_of`` which is described below.

When conditions are specified on the same field multiple times, all
conditions are added to the criteria:
Expand All @@ -380,11 +382,12 @@ conditions are added to the criteria:
Band.where(name: 1).or(name: 2).selector
# => {"$or"=>[{"name"=>"1"}, {"name"=>"2"}]}

``nor`` and ``not`` behave similarly, with ``not`` producing different query
shapes as described later.
``any_of``, ``nor`` and ``not`` behave similarly, with ``not`` producing
different query shapes as described below.

When a logical operator is used, it operates on the criteria built up to
that point and its argument. ``where`` has the same meaning as ``and``:
When ``and``, ``or`` and ``nor`` logical operators are used, they
operate on the criteria built up to that point and its argument.
``where`` has the same meaning as ``and``:

.. code-block:: ruby

Expand Down Expand Up @@ -434,21 +437,49 @@ the same field, depending on which form of ``and`` was used.
``or``/``nor`` Behavior
```````````````````````

``or`` and ``nor`` always combine conditions with ``$or`` and ``$nor``
MongoDB operators, respectively. If the only condition on the receiving
criteria is another ``or``/``nor``, new conditions are added to the
existing list, otherwise a new top-level ``$or``/``$nor`` operator is created.
``or`` and ``nor`` produce ``$or`` and ``$nor`` MongoDB operators, respectively,
using the receiver and all of the arguments as operands. For example:

.. code-block:: ruby

Band.where(name: /Best/).or(name: 'Astral Projection').
or(Band.where(label: /Records/)).selector
# => {"$or"=>[{"name"=>/Best/}, {"name"=>"Astral Projection"}, {"label"=>/Records/}]}
Band.where(name: /Best/).or(name: 'Astral Projection')
# => {"$or"=>[{"name"=>/Best/}, {"name"=>"Astral Projection"}]}

Band.where(name: /Best/).and(name: 'Astral Projection').
or(Band.where(label: /Records/)).and(label: 'Trust').selector
# => {"$or"=>[{"name"=>/Best/, "$and"=>[{"name"=>"Astral Projection"}]}, {"label"=>/Records/}], "label"=>"Trust"}

If the only condition on the receiver is another ``or``/``nor``, the new
conditions are added to the existing list:

.. code-block:: ruby

Band.where(name: /Best/).or(name: 'Astral Projection').
or(Band.where(label: /Records/)).selector
# => {"$or"=>[{"name"=>/Best/}, {"name"=>"Astral Projection"}, {"label"=>/Records/}]}

Use ``any_of`` to add a disjunction to a Criteria object while maintaining
all of the conditions built up so far as they are.


``any_of`` Behavior
```````````````````

``any_of`` adds a disjunction built from its arguments to the existing
conditions in the criteria. For example:

.. code-block:: ruby

Band.where(label: /Trust/).any_of({name: 'Astral Projection'}, {name: /Best/})
# => {"label"=>/Trust/, "$or"=>[{"name"=>"Astral Projection"}, {"name"=>/Best/}]}

The conditions are hoisted to the top level if possible:

.. code-block:: ruby

Band.where(label: /Trust/).any_of({name: 'Astral Projection'})
# => {"label"=>/Trust/, "name"=>"Astral Projection"}


``not`` Behavior
````````````````
Expand Down
97 changes: 92 additions & 5 deletions lib/mongoid/criteria/queryable/selectable.rb
Expand Up @@ -586,21 +586,108 @@ def not(*criteria)
end
key :not, :override, "$not"

# Adds $or selection to the selectable.
# Creates a disjunction using $or from the existing criteria in the
# receiver and the provided arguments.
#
# @example Add the $or selection.
# This behavior (receiver becoming one of the disjunction operands)
# matches ActiveRecord's +or+ behavior.
#
# Use +any_of+ to add a disjunction of the arguments as an additional
# constraint to the criteria already existing in the receiver.
#
# Each argument can be a Hash, a Criteria object, an array of
# Hash or Criteria objects, or a nested array. Nested arrays will be
# flattened and can be of any depth. Passing arrays is deprecated.
#
# @example Add the $or selection where both fields must have the specified values.
# selectable.or(field: 1, field: 2)
#
# @param [ Array<Hash | Criteria> ] criteria Multiple key/value pair
# matches or Criteria objects.
# @example Add the $or selection where either value match is sufficient.
# selectable.or({field: 1}, {field: 2})
#
# @example Same as previous example but using the deprecated array wrap.
# selectable.or([{field: 1}, {field: 2}])
#
# @example Same as previous example, also deprecated.
# selectable.or([{field: 1}], [{field: 2}])
#
# @param [ Hash | Criteria | Array<Hash | Criteria>, ... ] criteria
# Multiple key/value pair matches or Criteria objects, or arrays
# thereof. Passing arrays is deprecated.
#
# @return [ Selectable ] The new selectable.
#
# @since 1.0.0
def or(*criteria)
_mongoid_add_top_level_operation('$or', criteria)
end
alias :any_of :or

# Adds a disjunction of the arguments as an additional constraint
# to the criteria already existing in the receiver.
#
# Use +or+ to make the receiver one of the disjunction operands.
#
# Each argument can be a Hash, a Criteria object, an array of
# Hash or Criteria objects, or a nested array. Nested arrays will be
# flattened and can be of any depth. Passing arrays is deprecated.
#
# @example Add the $or selection where both fields must have the specified values.
# selectable.any_of(field: 1, field: 2)
#
# @example Add the $or selection where either value match is sufficient.
# selectable.any_of({field: 1}, {field: 2})
#
# @example Same as previous example but using the deprecated array wrap.
# selectable.any_of([{field: 1}, {field: 2}])
#
# @example Same as previous example, also deprecated.
# selectable.any_of([{field: 1}], [{field: 2}])
#
# @param [ Hash | Criteria | Array<Hash | Criteria>, ... ] criteria
# Multiple key/value pair matches or Criteria objects, or arrays
# thereof. Passing arrays is deprecated.
#
# @return [ Selectable ] The new selectable.
#
# @since 1.0.0
def any_of(*criteria)
criteria = _mongoid_flatten_arrays(criteria)
case criteria.length
when 0
clone
when 1
# When we have a single criteria, any_of behaves like and.
# Note: criteria can be a Query object, which #where method does
# not support.
self.and(*criteria)
else
# When we have multiple criteria, combine them all with $or
# and add the result to self.
exprs = criteria.map do |criterion|
if criterion.is_a?(Selectable)
_mongoid_normalize_expr(criterion.selector)
else
Hash[criterion.map do |k, v|
if k.is_a?(Symbol)
[k.to_s, v]
else
[k, v]
end
end]
end
end
# Should be able to do:
#where('$or' => exprs)
# But since that is broken do instead:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clone.tap do |query|
if query.selector['$or']
query.selector.store('$or', query.selector['$or'] + exprs)
else
query.selector.store('$or', exprs)
end
end
end
end

# Add a $size selection for array fields.
#
Expand Down