Merge pull request #3254 from apache/3.x-add-mango-operator-match-key-in-map

added $keyMapMatch Mango operator
diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl
index e884dc5..fc6a6d1 100644
--- a/src/mango/src/mango_selector.erl
+++ b/src/mango/src/mango_selector.erl
@@ -138,6 +138,11 @@
 norm_ops({[{<<"$allMatch">>, Arg}]}) ->
     ?MANGO_ERROR({bad_arg, '$allMatch', Arg});
 
+norm_ops({[{<<"$keyMapMatch">>, {_}=Arg}]}) ->
+    {[{<<"$keyMapMatch">>, norm_ops(Arg)}]};
+norm_ops({[{<<"$keyMapMatch">>, Arg}]}) ->
+    ?MANGO_ERROR({bad_arg, '$keyMapMatch', Arg});
+
 norm_ops({[{<<"$size">>, Arg}]}) when is_integer(Arg), Arg >= 0 ->
     {[{<<"$size">>, Arg}]};
 norm_ops({[{<<"$size">>, Arg}]}) ->
@@ -253,6 +258,10 @@
     Cond = {[{<<"$allMatch">>, norm_fields(Arg)}]},
     {[{Path, Cond}]};
 
+norm_fields({[{<<"$keyMapMatch">>, Arg}]}, Path) ->
+    Cond = {[{<<"$keyMapMatch">>, norm_fields(Arg)}]},
+    {[{Path, Cond}]};
+
 
 % The text operator operates against the internal
 % $default field. This also asserts that the $default
@@ -334,6 +343,9 @@
 norm_negations({[{<<"$allMatch">>, Arg}]}) ->
     {[{<<"$allMatch">>, norm_negations(Arg)}]};
 
+norm_negations({[{<<"$keyMapMatch">>, Arg}]}) ->
+    {[{<<"$keyMapMatch">>, norm_negations(Arg)}]};
+
 % All other conditions can't introduce negations anywhere
 % further down the operator tree.
 norm_negations(Cond) ->
@@ -491,6 +503,26 @@
 match({[{<<"$allMatch">>, _Arg}]}, _Value, _Cmp) ->
     false;
 
+% Matches when any key in the map value matches the
+% sub-selector Arg.
+match({[{<<"$keyMapMatch">>, Arg}]}, Value, Cmp) when is_tuple(Value) ->
+    try
+        lists:foreach(fun(V) ->
+            case match(Arg, V, Cmp) of
+                true -> throw(matched);
+                _ -> ok
+            end
+        end, [Key || {Key, _} <- element(1, Value)]),
+        false
+    catch
+        throw:matched ->
+            true;
+        _:_ ->
+            false
+    end;
+match({[{<<"$keyMapMatch">>, _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/test/03-operator-test.py b/src/mango/test/03-operator-test.py
index 935f470..a67ef91 100644
--- a/src/mango/test/03-operator-test.py
+++ b/src/mango/test/03-operator-test.py
@@ -66,6 +66,15 @@
         docs = self.db.find({"emptybang": {"$allMatch": {"foo": {"$eq": 2}}}})
         self.assertEqual(len(docs), 0)
 
+    def test_keymap_match(self):
+        amdocs = [
+            {"foo": {"aa": "bar", "bb": "bang"}},
+            {"foo": {"cc": "bar", "bb": "bang"}},
+        ]
+        self.db.save_docs(amdocs, w=3)
+        docs = self.db.find({"foo": {"$keyMapMatch": {"$eq": "aa"}}})
+        self.assertEqual(len(docs), 1)
+
     def test_in_operator_array(self):
         docs = self.db.find({"manager": True, "favorites": {"$in": ["Ruby", "Python"]}})
         self.assertUserIds([2, 6, 7, 9, 11, 12], docs)