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.
diff --git a/README.md b/README.md
index e9d4a66..4c4bb60 100644
--- a/README.md
+++ b/README.md
@@ -272,6 +272,7 @@
* "$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
diff --git a/src/mango_selector.erl b/src/mango_selector.erl
index c6004cd..691aac7 100644
--- a/src/mango_selector.erl
+++ b/src/mango_selector.erl
@@ -127,6 +127,11 @@
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 @@
% 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 @@
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({[{<<"$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) ->
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}]}, _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;
diff --git a/src/mango_selector_text.erl b/src/mango_selector_text.erl
index b6e1f09..cfa3baf 100644
--- a/src/mango_selector_text.erl
+++ b/src/mango_selector_text.erl
@@ -86,6 +86,9 @@
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 ->
diff --git a/test/03-operator-test.py b/test/03-operator-test.py
index 50d5bd2..56c2862 100644
--- a/test/03-operator-test.py
+++ b/test/03-operator-test.py
@@ -63,6 +63,46 @@
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,
diff --git a/test/06-basic-text-test.py b/test/06-basic-text-test.py
index 1e3d5df..7f5ce63 100644
--- a/test/06-basic-text-test.py
+++ b/test/06-basic-text-test.py
@@ -571,6 +571,49 @@
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")
diff --git a/test/07-text-custom-field-list-test.py b/test/07-text-custom-field-list-test.py
index 029c91c..4db11a5 100644
--- a/test/07-text-custom-field-list-test.py
+++ b/test/07-text-custom-field-list-test.py
@@ -145,3 +145,14 @@
{"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
diff --git a/test/friend_docs.py b/test/friend_docs.py
index e0cf60e..0757961 100644
--- a/test/friend_docs.py
+++ b/test/friend_docs.py
@@ -566,5 +566,39 @@
"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"
+ }
+ ]
}
-]
\ No newline at end of file
+]