Skip to content
Permalink
Browse files

Add `$allMatch` selector

This selector is similar to the existing `$elemMatch` one but requires
all elements of an array value to match the inner selector.
  • Loading branch information...
satabin authored and garrensmith committed Feb 7, 2017
1 parent a319d92 commit 312e2c45535913c190cdef51f6ea65066ccd89dc
@@ -272,6 +272,7 @@ The list of combining characters:
* "$nor" - array argument
* "$all" - array argument (special operator for array values)
* "$elemMatch" - single argument (special operator for array values)
* "$allMatch" - single argument (special operator for array values)
### Condition Operators
@@ -127,6 +127,11 @@ norm_ops({[{<<"$elemMatch">>, {_}=Arg}]}) ->
norm_ops({[{<<"$elemMatch">>, Arg}]}) ->
?MANGO_ERROR({bad_arg, '$elemMatch', Arg});

norm_ops({[{<<"$allMatch">>, {_}=Arg}]}) ->
{[{<<"$allMatch">>, norm_ops(Arg)}]};
norm_ops({[{<<"$allMatch">>, Arg}]}) ->
?MANGO_ERROR({bad_arg, '$allMatch', Arg});

norm_ops({[{<<"$size">>, Arg}]}) when is_integer(Arg), Arg >= 0 ->
{[{<<"$size">>, Arg}]};
norm_ops({[{<<"$size">>, Arg}]}) ->
@@ -209,8 +214,9 @@ norm_ops(Value) ->
% Its important to note that we can only normalize
% field names like this through boolean operators where
% we can gaurantee commutativity. We can't necessarily
% do the same through the '$elemMatch' operators but we
% can apply the same algorithm to its arguments.
% do the same through the '$elemMatch' or '$allMatch'
% operators but we can apply the same algorithm to its
% arguments.
norm_fields({[]}) ->
{[]};
norm_fields(Selector) ->
@@ -237,6 +243,10 @@ norm_fields({[{<<"$elemMatch">>, Arg}]}, Path) ->
Cond = {[{<<"$elemMatch">>, norm_fields(Arg)}]},
{[{Path, Cond}]};

norm_fields({[{<<"$allMatch">>, Arg}]}, Path) ->
Cond = {[{<<"$allMatch">>, norm_fields(Arg)}]},
{[{Path, Cond}]};


% The text operator operates against the internal
% $default field. This also asserts that the $default
@@ -315,6 +325,9 @@ norm_negations({[{<<"$or">>, Args}]}) ->
norm_negations({[{<<"$elemMatch">>, Arg}]}) ->
{[{<<"$elemMatch">>, norm_negations(Arg)}]};

norm_negations({[{<<"$allMatch">>, Arg}]}) ->
{[{<<"$allMatch">>, norm_negations(Arg)}]};

% All other conditions can't introduce negations anywhere
% further down the operator tree.
norm_negations(Cond) ->
@@ -411,7 +424,7 @@ match({[{<<"$all">>, Args}]}, Values, _Cmp) when is_list(Values) ->
match({[{<<"$all">>, _Args}]}, _Values, _Cmp) ->
false;

%% This is for $elemMatch and possibly $in because of our normalizer.
%% This is for $elemMatch, $allMatch, and possibly $in because of our normalizer.
%% A selector such as {"field_name": {"$elemMatch": {"$gte": 80, "$lt": 85}}}
%% gets normalized to:
%% {[{<<"field_name">>,
@@ -446,6 +459,24 @@ match({[{<<"$elemMatch">>, Arg}]}, Values, Cmp) when is_list(Values) ->
match({[{<<"$elemMatch">>, _Arg}]}, _Value, _Cmp) ->
false;

% Matches when all elements in values match the
% sub-selector Arg.
match({[{<<"$allMatch">>, Arg}]}, Values, Cmp) when is_list(Values) ->
try
lists:foreach(fun(V) ->
case match(Arg, V, Cmp) of
false -> throw(unmatched);
_ -> ok
end
end, Values),
true
catch
_:_ ->
false
end;
match({[{<<"$allMatch">>, _Arg}]}, _Value, _Cmp) ->
false;

% Our comparison operators are fairly straight forward
match({[{<<"$lt">>, Arg}]}, Value, Cmp) ->
Cmp(Value, Arg) < 0;
@@ -86,6 +86,9 @@ convert(Path, {[{<<"$all">>, Args}]}) ->
convert(Path, {[{<<"$elemMatch">>, Arg}]}) ->
convert([<<"[]">> | Path], Arg);

convert(Path, {[{<<"$allMatch">>, Arg}]}) ->
convert([<<"[]">> | Path], Arg);

% Our comparison operators are fairly straight forward
convert(Path, {[{<<"$lt">>, Arg}]}) when is_list(Arg); is_tuple(Arg);
Arg =:= null ->
@@ -63,6 +63,46 @@ def test_elem_match(self):
assert len(docs) == 1
assert docs[0]["user_id"] == "b"

def test_all_match(self):
amdocs = [
{
"user_id": "a",
"bang": [
{
"foo": 1,
"bar": 2
},
{
"foo": 3,
"bar": 4
}
]
},
{
"user_id": "b",
"bang": [
{
"foo": 1,
"bar": 2
},
{
"foo": 4,
"bar": 4
}
]
}
]
self.db.save_docs(amdocs, w=3)
docs = self.db.find({
"_id": {"$gt": None},
"bang": {"$allMatch": {
"foo": {"$mod": [2,1]},
"bar": {"$mod": [2,0]}
}}
})
assert len(docs) == 1
assert docs[0]["user_id"] == "a"

def test_in_operator_array(self):
docs = self.db.find({
"manager": True,
@@ -571,6 +571,49 @@ def test_elem_match(self):
for d in docs:
assert d["user_id"] in (10, 11,12)

@unittest.skipUnless(mango.has_text_service(), "requires text service")
class AllMatchTests(mango.FriendDocsTextTests):

def test_all_match(self):
q = {"friends": {
"$allMatch":
{"type": "personal"}
}
}
docs = self.db.find(q)
assert len(docs) == 2
for d in docs:
assert d["user_id"] in (8, 5)

# Check that we can do logic in allMatch
q = {
"friends": {
"$allMatch": {
"name.first": "Ochoa",
"$or": [
{"type": "work"},
{"type": "personal"}
]
}
}
}
docs = self.db.find(q)
assert len(docs) == 1
assert docs[0]["user_id"] == 15

# Same as last, but using $in
q = {
"friends": {
"$allMatch": {
"name.first": "Ochoa",
"type": {"$in": ["work", "personal"]}
}
}
}
docs = self.db.find(q)
assert len(docs) == 1
assert docs[0]["user_id"] == 15


# Test numeric strings for $text
@unittest.skipUnless(mango.has_text_service(), "requires text service")
@@ -145,3 +145,14 @@ def test_two_or(self):
{"location.state": "Don't Exist"}]})
assert len(docs) == 1
assert docs[0]["user_id"] == 10

def test_all_match(self):
docs = self.db.find({
"favorites": {
"$allMatch": {
"$eq": "Erlang"
}
}
})
assert len(docs) == 1
assert docs[0]["user_id"] == 10
@@ -566,5 +566,39 @@ def add_text_indexes(db):
"type": "work"
}
]
},
{
"_id": "589f32af493145f890e1b051",
"user_id": 15,
"name": {
"first": "Tanisha",
"last": "Bowers"
},
"friends": [
{
"id": 0,
"name": {
"first": "Ochoa",
"last": "Pratt"
},
"type": "personal"
},
{
"id": 1,
"name": {
"first": "Ochoa",
"last": "Romero"
},
"type": "personal"
},
{
"id": 2,
"name": {
"first": "Ochoa",
"last": "Bowman"
},
"type": "work"
}
]
}
]
]

0 comments on commit 312e2c4

Please sign in to comment.
You can’t perform that action at this time.