This is an automated email from the ASF dual-hosted git repository.

jcoglan pushed a commit to branch mango-match-failures
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit f62d50cc62a60867851d4a54518fc514a4ccb9b6
Author: James Coglan <[email protected]>
AuthorDate: Fri Jan 16 15:41:43 2026 +0000

    feat: "verbose" mode for `mango_selector:match/3`
    
    Collecting detailed `#failure` records rather than a boolean true/false
    when evaluating selectors imposes a performance penalty, so we would
    like to only do this when a selector is used for a VDU, not when it is
    used for indexing/filtering.
    
    To this end we introduce "verbose" mode signalled via the `#ctx.verbose`
    field, and each branch of `mango_selector:match/3` now has 3 distinct
    versions:
    
    - `#ctx{verbose = false}`: this is the original version that returns
      true/false, taken when a selector is used for Mango queries.
    
    - `#ctx{verbose = true, negate = false}`: verbose mode, when the
      operator is not negated by an enclosing `$not` operator. Returns a
      list of `#failure` records which may be empty.
    
    - `#ctx{verbose = true, negate = true}`: verbose mode, when the operator
      is negated by an enclosing `$not` operator. Returns a list of
      `#failure` records.
    
    The different negation modes are needed because, in order to generate
    meaningful failure messages, we need to record whether an operator was
    negated. The behaviour of combinators like `$and`, `$or`, `$allMatch`
    and `$elemMatch` means not all `$not` operators can be normalized out of
    the selector before evaluation. Instead, when we encounter a `$not`
    during evaluation, we flip the `#ctx.negate` field before evaluating the
    inner operator.
---
 src/mango/src/mango_selector.erl | 487 +++++++++++++++++++++++++--------------
 1 file changed, 314 insertions(+), 173 deletions(-)

diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl
index 73db59ca8..9333822ba 100644
--- a/src/mango/src/mango_selector.erl
+++ b/src/mango/src/mango_selector.erl
@@ -15,6 +15,7 @@
 -export([
     normalize/1,
     match/2,
+    match_failures/2,
     has_required_fields/2,
     is_constant_field/2,
     fields/1
@@ -25,6 +26,8 @@
 
 -record(ctx, {
     cmp,
+    verbose = false,
+    negate = false,
     path = []
 }).
 
@@ -32,7 +35,7 @@
     op,
     type = mismatch,
     params = [],
-    path = []
+    ctx
 }).
 
 % Validate and normalize each operator. This translates
@@ -65,20 +68,19 @@ match(Selector, D) ->
     couch_stats:increment_counter([mango, evaluate_selector]),
     match_int(Selector, D).
 
+match_failures(Selector, D) ->
+    couch_stats:increment_counter([mango, evaluate_selector]),
+    match_int(Selector, D, true).
+
 match_int(Selector, D) ->
-    case match_failures(Selector, D) of
-        [] -> true;
-        [_ | _] -> false;
-        Other -> Other
-    end.
+    match_int(Selector, D, false).
 
-% An empty selector matches any value.
-match_failures({[]}, _) ->
-    [];
-match_failures(Selector, #doc{body = Body}) ->
-    match_failures(Selector, Body);
-match_failures(Selector, {Props}) ->
-    match(Selector, {Props}, #ctx{cmp = fun mango_json:cmp/2}).
+match_int(Selector, D, Verbose) ->
+    Ctx = #ctx{cmp = fun mango_json:cmp/2, verbose = Verbose},
+    case D of
+        #doc{body = Body} -> match(Selector, Body, Ctx);
+        Other -> match(Selector, Other, Ctx)
+    end.
 
 % Convert each operator into a normalized version as well
 % as convert an implicit operators into their explicit
@@ -387,48 +389,76 @@ negate({[{<<"$", _/binary>>, _}]} = Cond) ->
 negate({[{Field, Cond}]}) ->
     {[{Field, negate(Cond)}]}.
 
+% An empty selector matches any value.
+match({[]}, _, #ctx{verbose = false}) ->
+    true;
+match({[]}, _, #ctx{verbose = true}) ->
+    [];
 % We need to treat an empty array as always true. This will be applied
 % for $or, $in, $all, $nin as well.
-match({[{<<"$and">>, []}]}, _, _) ->
+match({[{<<"$and">>, []}]}, _, #ctx{verbose = false}) ->
+    true;
+match({[{<<"$and">>, []}]}, _, #ctx{negate = false}) ->
     [];
+match({[{<<"$and">>, []}]}, _, Ctx) ->
+    [#failure{op = 'and', type = empty_list, params = [[]], ctx = Ctx}];
+match({[{<<"$and">>, Args}]}, Value, #ctx{verbose = false} = Ctx) ->
+    Pred = fun(SubSel) -> match(SubSel, Value, Ctx) end,
+    lists:all(Pred, Args);
+match({[{<<"$and">>, Args}]}, Value, #ctx{negate = true} = Ctx) ->
+    NotArgs = [{[{<<"$not">>, A}]} || A <- Args],
+    PosCtx = Ctx#ctx{negate = false},
+    match({[{<<"$or">>, NotArgs}]}, Value, PosCtx);
 match({[{<<"$and">>, Args}]}, Value, Ctx) ->
     MatchSubSel = fun(SubSel) -> match(SubSel, Value, Ctx) end,
     lists:flatmap(MatchSubSel, Args);
-match({[{<<"$or">>, []}]}, _, _) ->
+match({[{<<"$or">>, []}]}, _, #ctx{verbose = false}) ->
+    true;
+match({[{<<"$or">>, []}]}, _, #ctx{negate = false}) ->
     [];
+match({[{<<"$or">>, []}]}, _, Ctx) ->
+    [#failure{op = 'or', type = empty_list, params = [[]], ctx = Ctx}];
+match({[{<<"$or">>, Args}]}, Value, #ctx{verbose = false} = Ctx) ->
+    Pred = fun(SubSel) -> match(SubSel, Value, Ctx) end,
+    lists:any(Pred, Args);
+match({[{<<"$or">>, Args}]}, Value, #ctx{negate = true} = Ctx) ->
+    NotArgs = [{[{<<"$not">>, A}]} || A <- Args],
+    PosCtx = Ctx#ctx{negate = false},
+    match({[{<<"$and">>, NotArgs}]}, Value, PosCtx);
 match({[{<<"$or">>, Args}]}, Value, Ctx) ->
     SubSelFailures = [match(A, Value, Ctx) || A <- Args],
-    case lists:any(fun(Res) -> Res == [] end, SubSelFailures) of
+    case lists:member([], SubSelFailures) of
         true -> [];
-        _ -> lists:flatten(SubSelFailures)
-    end;
-% TODO: producing good failure messages requires that normalize/1 fully removes
-% $not from the tree by pushing it to the leaves.
-match({[{<<"$not">>, Arg}]}, Value, Ctx) ->
-    case match(Arg, Value, Ctx) of
-        [] -> [#failure{op = 'not', path = Ctx#ctx.path}];
-        _ -> []
+        false -> lists:flatten(SubSelFailures)
     end;
+match({[{<<"$not">>, Arg}]}, Value, #ctx{verbose = false} = Ctx) ->
+    not match(Arg, Value, Ctx);
+match({[{<<"$not">>, Arg}]}, Value, #ctx{negate = Neg} = Ctx) ->
+    match(Arg, Value, Ctx#ctx{negate = not Neg});
+match({[{<<"$all">>, []}]}, _, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$all">>, []}]}, _, #ctx{negate = false} = Ctx) ->
+    [#failure{op = all, type = empty_list, params = [[]], ctx = Ctx}];
+match({[{<<"$all">>, []}]}, _, #ctx{negate = true}) ->
+    [];
 % All of the values in Args must exist in Values or
 % Values == hd(Args) if Args is a single element list
 % that contains a list.
-match({[{<<"$all">>, []}]}, _Values, Ctx) ->
-    % { "$all": [] } is defined to eval to false, so return a failure
-    [#failure{op = all, params = [[]], path = Ctx#ctx.path}];
-match({[{<<"$all">>, [A]}]}, Values, _Ctx) when is_list(A), A == Values ->
-    [];
-match({[{<<"$all">>, Args}]}, Values, Ctx) when is_list(Values) ->
-    lists:flatmap(
-        fun(Arg) ->
-            case lists:member(Arg, Values) of
-                true -> [];
-                _ -> [#failure{op = all, params = [Arg], path = Ctx#ctx.path}]
-            end
+match({[{<<"$all">>, Args}]}, Values, #ctx{verbose = false}) when 
is_list(Values) ->
+    Pred = fun(A) -> lists:member(A, Values) end,
+    HasArgs = lists:all(Pred, Args),
+    IsArgs =
+        case Args of
+            [A] when is_list(A) ->
+                A == Values;
+            _ ->
+                false
         end,
-        Args
-    );
-match({[{<<"$all">>, _}]}, Value, Ctx) ->
-    [#failure{op = all, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
+    HasArgs orelse IsArgs;
+match({[{<<"$all">>, _Args}]}, _Values, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$all">>, Args}]} = Expr, Values, Ctx) ->
+    match_with_failure(Expr, Values, all, [Args], Ctx);
 %% 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:
@@ -445,8 +475,33 @@ match({[{[], Arg}]}, Values, Ctx) ->
     match(Arg, Values, Ctx);
 % Matches when any element in values matches the
 % sub-selector Arg.
-match({[{<<"$elemMatch">>, _Arg}]}, [], Ctx) ->
-    [#failure{op = elemMatch, type = empty_list, path = Ctx#ctx.path}];
+match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{verbose = false} = Ctx) when 
is_list(Values) ->
+    try
+        lists:foreach(
+            fun(V) ->
+                case match(Arg, V, Ctx) of
+                    true -> throw(matched);
+                    _ -> ok
+                end
+            end,
+            Values
+        ),
+        false
+    catch
+        throw:matched ->
+            true;
+        _:_ ->
+            false
+    end;
+match({[{<<"$elemMatch">>, _Arg}]}, _Value, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$elemMatch">>, _Arg}]}, [], #ctx{negate = false} = Ctx) ->
+    [#failure{op = elemMatch, type = empty_list, ctx = Ctx}];
+match({[{<<"$elemMatch">>, _Arg}]}, [], #ctx{negate = true}) ->
+    [];
+match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{negate = true} = Ctx) ->
+    PosCtx = Ctx#ctx{negate = false},
+    match({[{<<"$allMatch">>, {[{<<"$not">>, Arg}]}}]}, Values, PosCtx);
 match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{path = Path} = Ctx) when 
is_list(Values) ->
     ValueFailures = [
         match(Arg, V, Ctx#ctx{path = [Idx | Path]})
@@ -454,44 +509,100 @@ match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{path = 
Path} = Ctx) when is_list
     ],
     case lists:member([], ValueFailures) of
         true -> [];
-        _ -> lists:flatten(ValueFailures)
+        false -> lists:flatten(ValueFailures)
     end;
-match({[{<<"$elemMatch">>, _}]}, Value, Ctx) ->
-    [#failure{op = elemMatch, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
+match({[{<<"$elemMatch">>, _Arg}]}, Values, #ctx{} = Ctx) ->
+    [#failure{op = elemMatch, type = bad_value, params = [Values], ctx = Ctx}];
 % Matches when all elements in values match the
 % sub-selector Arg.
+match({[{<<"$allMatch">>, Arg}]}, [_ | _] = Values, #ctx{verbose = false} = 
Ctx) ->
+    try
+        lists:foreach(
+            fun(V) ->
+                case match(Arg, V, Ctx) of
+                    false -> throw(unmatched);
+                    _ -> ok
+                end
+            end,
+            Values
+        ),
+        true
+    catch
+        _:_ ->
+            false
+    end;
+match({[{<<"$allMatch">>, _Arg}]}, _Value, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$allMatch">>, _Arg}]}, [], #ctx{negate = false} = Ctx) ->
+    [#failure{op = allMatch, type = empty_list, ctx = Ctx}];
+match({[{<<"$allMatch">>, _Arg}]}, [], #ctx{negate = true}) ->
+    [];
+match({[{<<"$allMatch">>, Arg}]}, Values, #ctx{negate = true} = Ctx) ->
+    PosCtx = Ctx#ctx{negate = false},
+    match({[{<<"$elemMatch">>, {[{<<"$not">>, Arg}]}}]}, Values, PosCtx);
 match({[{<<"$allMatch">>, Arg}]}, [_ | _] = Values, #ctx{path = Path} = Ctx) ->
+    MatchValue = fun({Idx, V}) -> match(Arg, V, Ctx#ctx{path = [Idx | Path]}) 
end,
     EnumValues = lists:enumerate(0, Values),
-    MatchValue = fun({Idx, Value}) -> match(Arg, Value, Ctx#ctx{path = [Idx | 
Path]}) end,
     lists:flatmap(MatchValue, EnumValues);
-match({[{<<"$allMatch">>, _}]}, Value, Ctx) ->
-    [#failure{op = allMatch, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
+match({[{<<"$allMatch">>, _Arg}]}, Values, #ctx{} = Ctx) ->
+    [#failure{op = allMatch, type = bad_value, params = [Values], ctx = Ctx}];
 % Matches when any key in the map value matches the
 % sub-selector Arg.
-match({[{<<"$keyMapMatch">>, _Arg}]}, {[]}, Ctx) ->
-    [#failure{op = keyMapMatch, type = empty_list, path = Ctx#ctx.path}];
-match({[{<<"$keyMapMatch">>, Arg}]}, {Value}, Ctx) when is_list(Value) ->
-    KeyFailures = [match(Arg, K, Ctx) || {K, _} <- Value],
+match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{verbose = false} = Ctx) when 
is_tuple(Value) ->
+    try
+        lists:foreach(
+            fun(V) ->
+                case match(Arg, V, Ctx) of
+                    true -> throw(matched);
+                    _ -> ok
+                end
+            end,
+            [Key || {Key, _} <- element(1, Value)]
+        ),
+        false
+    catch
+        throw:matched ->
+            true;
+        _:_ ->
+            false
+    end;
+match({[{<<"$keyMapMatch">>, _Arg}]}, _Value, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$keyMapMatch">>, _Arg}]}, {[]}, #ctx{negate = false} = Ctx) ->
+    [#failure{op = keyMapMatch, type = empty_list, ctx = Ctx}];
+match({[{<<"$keyMapMatch">>, _Arg}]}, {[]}, #ctx{negate = true}) ->
+    [];
+match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{negate = true, path = Path} = 
Ctx) when
+    is_tuple(Value)
+->
+    Keys = [Key || {Key, _} <- element(1, Value)],
+    MatchKey = fun(K) -> match(Arg, K, Ctx#ctx{path = [K | Path]}) end,
+    lists:flatmap(MatchKey, Keys);
+match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{path = Path} = Ctx) when 
is_tuple(Value) ->
+    Keys = [Key || {Key, _} <- element(1, Value)],
+    KeyFailures = [match(Arg, K, Ctx#ctx{path = [K | Path]}) || K <- Keys],
     case lists:member([], KeyFailures) of
         true -> [];
-        _ -> lists:flatten(KeyFailures)
+        false -> lists:flatten(KeyFailures)
     end;
-match({[{<<"$keyMapMatch">>, _}]}, Value, Ctx) ->
-    [#failure{op = keyMapMatch, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
+match({[{<<"$keyMapMatch">>, _Arg}]}, _Value, Ctx) ->
+    [#failure{op = keyMapMatch, type = bad_value, ctx = Ctx}];
 % Our comparison operators are fairly straight forward
-match({[{<<"$lt">>, Arg}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
-    compare(lt, Arg, Path, Cmp(Value, Arg) < 0);
-match({[{<<"$lte">>, Arg}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
-    compare(lte, Arg, Path, Cmp(Value, Arg) =< 0);
-match({[{<<"$eq">>, Arg}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
-    compare(eq, Arg, Path, Cmp(Value, Arg) == 0);
-match({[{<<"$ne">>, Arg}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
-    compare(ne, Arg, Path, Cmp(Value, Arg) /= 0);
-match({[{<<"$gte">>, Arg}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
-    compare(gte, Arg, Path, Cmp(Value, Arg) >= 0);
-match({[{<<"$gt">>, Arg}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
-    compare(gt, Arg, Path, Cmp(Value, Arg) > 0);
-match({[{<<"$in">>, Args}]}, Values, #ctx{cmp = Cmp, path = Path}) when 
is_list(Values) ->
+match({[{<<"$lt">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
+    compare(lt, Arg, Ctx, Cmp(Value, Arg) < 0);
+match({[{<<"$lte">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
+    compare(lte, Arg, Ctx, Cmp(Value, Arg) =< 0);
+match({[{<<"$eq">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
+    compare(eq, Arg, Ctx, Cmp(Value, Arg) == 0);
+match({[{<<"$ne">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
+    compare(ne, Arg, Ctx, Cmp(Value, Arg) /= 0);
+match({[{<<"$gte">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
+    compare(gte, Arg, Ctx, Cmp(Value, Arg) >= 0);
+match({[{<<"$gt">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
+    compare(gt, Arg, Ctx, Cmp(Value, Arg) > 0);
+match({[{<<"$in">>, []}]}, _, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$in">>, Args}]}, Values, #ctx{verbose = false, cmp = Cmp}) when 
is_list(Values) ->
     Pred = fun(Arg) ->
         lists:foldl(
             fun(Value, Match) ->
@@ -501,88 +612,68 @@ match({[{<<"$in">>, Args}]}, Values, #ctx{cmp = Cmp, path 
= Path}) when is_list(
             Values
         )
     end,
-    case lists:any(Pred, Args) of
-        true -> [];
-        _ -> [#failure{op = in, params = [Args], path = Path}]
-    end;
-match({[{<<"$in">>, Args}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
+    lists:any(Pred, Args);
+match({[{<<"$in">>, Args}]}, Value, #ctx{verbose = false, cmp = Cmp}) ->
     Pred = fun(Arg) -> Cmp(Value, Arg) == 0 end,
-    case lists:any(Pred, Args) of
-        true -> [];
-        _ -> [#failure{op = in, params = [Args], path = Path}]
-    end;
-match({[{<<"$nin">>, Args}]}, Values, #ctx{cmp = Cmp, path = Path}) when 
is_list(Values) ->
-    Pred = fun(Arg) ->
-        lists:foldl(
-            fun(Value, Match) ->
-                (Cmp(Value, Arg) /= 0) and Match
-            end,
-            true,
-            Values
-        )
-    end,
-    case lists:all(Pred, Args) of
-        true -> [];
-        _ -> [#failure{op = nin, params = [Args], path = Path}]
-    end;
-match({[{<<"$nin">>, Args}]}, Value, #ctx{cmp = Cmp, path = Path}) ->
+    lists:any(Pred, Args);
+match({[{<<"$in">>, Args}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, in, [Args], Ctx);
+match({[{<<"$nin">>, []}]}, _, #ctx{verbose = false}) ->
+    true;
+match({[{<<"$nin">>, Args}]}, Values, #ctx{verbose = false} = Ctx) when 
is_list(Values) ->
+    not match({[{<<"$in">>, Args}]}, Values, Ctx);
+match({[{<<"$nin">>, Args}]}, Value, #ctx{verbose = false, cmp = Cmp}) ->
     Pred = fun(Arg) -> Cmp(Value, Arg) /= 0 end,
-    case lists:all(Pred, Args) of
-        true -> [];
-        _ -> [#failure{op = nin, params = [Args], path = Path}]
-    end;
+    lists:all(Pred, Args);
+match({[{<<"$nin">>, Args}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, nin, [Args], Ctx);
 % This logic is a bit subtle. Basically, if value is
 % not undefined, then it exists.
-match({[{<<"$exists">>, ShouldExist}]}, Value, Ctx) ->
-    case {ShouldExist, Value} of
-        {true, undefined} -> [#failure{op = exists, params = [ShouldExist], 
path = Ctx#ctx.path}];
-        {true, _} -> [];
-        {false, undefined} -> [];
-        {false, _} -> [#failure{op = exists, params = [ShouldExist], path = 
Ctx#ctx.path}]
-    end;
-match({[{<<"$type">>, Arg}]}, Value, Ctx) when is_binary(Arg) ->
-    case mango_json:type(Value) of
-        Arg -> [];
-        _ -> [#failure{op = type, params = [Arg], path = Ctx#ctx.path}]
-    end;
-match({[{<<"$mod">>, [D, R]}]}, Value, Ctx) when is_integer(Value) ->
-    case Value rem D of
-        R -> [];
-        _ -> [#failure{op = mod, params = [D, R], path = Ctx#ctx.path}]
-    end;
-match({[{<<"$mod">>, _}]}, Value, Ctx) ->
-    [#failure{op = mod, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
-match({[{<<"$beginsWith">>, Prefix}]}, Value, Ctx) when is_binary(Prefix), 
is_binary(Value) ->
-    case string:prefix(Value, Prefix) of
-        nomatch -> [#failure{op = beginsWith, params = [Prefix], path = 
Ctx#ctx.path}];
-        _ -> []
-    end;
+match({[{<<"$exists">>, ShouldExist}]}, Value, #ctx{verbose = false}) ->
+    Exists = Value /= undefined,
+    ShouldExist andalso Exists;
+match({[{<<"$exists">>, ShouldExist}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, exists, [ShouldExist], Ctx);
+match({[{<<"$type">>, Arg}]}, Value, #ctx{verbose = false}) when 
is_binary(Arg) ->
+    Arg == mango_json:type(Value);
+match({[{<<"$type">>, Arg}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, type, [Arg], Ctx);
+match({[{<<"$mod">>, [D, R]}]}, Value, #ctx{verbose = false}) when 
is_integer(Value) ->
+    Value rem D == R;
+match({[{<<"$mod">>, _}]}, _Value, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$mod">>, [D, R]}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, mod, [D, R], Ctx);
+match({[{<<"$beginsWith">>, Prefix}]}, Value, #ctx{verbose = false}) when
+    is_binary(Prefix), is_binary(Value)
+->
+    string:prefix(Value, Prefix) /= nomatch;
 % When Value is not a string, do not match
-match({[{<<"$beginsWith">>, Prefix}]}, Value, Ctx) when is_binary(Prefix) ->
-    [#failure{op = beginsWith, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
-match({[{<<"$regex">>, Regex}]}, Value, Ctx) when is_binary(Value) ->
+match({[{<<"$beginsWith">>, Prefix}]}, _, #ctx{verbose = false}) when 
is_binary(Prefix) ->
+    false;
+match({[{<<"$beginsWith">>, Prefix}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, beginsWith, [Prefix], Ctx);
+match({[{<<"$regex">>, Regex}]}, Value, #ctx{verbose = false}) when 
is_binary(Value) ->
     try
-        case re:run(Value, Regex, [{capture, none}]) of
-            match -> [];
-            _ -> [#failure{op = regex, params = [Regex], path = Ctx#ctx.path}]
-        end
+        match == re:run(Value, Regex, [{capture, none}])
     catch
         _:_ ->
-            [#failure{op = regex, params = [Regex], path = Ctx#ctx.path}]
-    end;
-match({[{<<"$regex">>, _}]}, Value, Ctx) ->
-    [#failure{op = regex, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
-match({[{<<"$size">>, Arg}]}, Values, Ctx) when is_list(Values) ->
-    case length(Values) of
-        Arg -> [];
-        _ -> [#failure{op = size, params = [Arg], path = Ctx#ctx.path}]
+            false
     end;
-match({[{<<"$size">>, _}]}, Value, Ctx) ->
-    [#failure{op = size, type = bad_value, params = [Value], path = 
Ctx#ctx.path}];
+match({[{<<"$regex">>, _}]}, _Value, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$regex">>, Regex}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, regex, [Regex], Ctx);
+match({[{<<"$size">>, Arg}]}, Values, #ctx{verbose = false}) when 
is_list(Values) ->
+    length(Values) == Arg;
+match({[{<<"$size">>, _}]}, _Value, #ctx{verbose = false}) ->
+    false;
+match({[{<<"$size">>, Arg}]} = Expr, Value, Ctx) ->
+    match_with_failure(Expr, Value, size, [Arg], Ctx);
 % We don't have any choice but to believe that the text
 % index returned valid matches
-match({[{<<"$default">>, _}]}, _Value, _Ctx) ->
-    [];
+match({[{<<"$default">>, _}]}, _Value, #ctx{verbose = false}) ->
+    true;
 % All other operators are internal assertion errors for
 % matching because we either should've removed them during
 % normalization or something else broke.
@@ -591,16 +682,25 @@ match({[{<<"$", _/binary>> = Op, _}]}, _, _) ->
 % We need to traverse value to find field. The call to
 % mango_doc:get_field/2 may return either not_found or
 % bad_path in which case matching fails.
-match({[{Field, Cond}]}, Value, #ctx{path = Path} = Ctx) ->
+match({[{Field, Cond}]}, Value, #ctx{verbose = Verb, path = Path} = Ctx) ->
     InnerPath = extend_path(Field, Path),
     InnerCtx = Ctx#ctx{path = InnerPath},
     case mango_doc:get_field(Value, Field) of
         not_found when Cond == {[{<<"$exists">>, false}]} ->
-            [];
+            case Verb of
+                true -> [];
+                false -> true
+            end;
         not_found ->
-            [#failure{op = field, type = not_found, path = InnerCtx#ctx.path}];
+            case Verb of
+                true -> [#failure{op = field, type = not_found, ctx = 
InnerCtx}];
+                false -> false
+            end;
         bad_path ->
-            [#failure{op = field, type = bad_path, path = InnerCtx#ctx.path}];
+            case Verb of
+                true -> [#failure{op = field, type = bad_path, ctx = 
InnerCtx}];
+                false -> false
+            end;
         SubValue when Field == <<"_id">> ->
             match(Cond, SubValue, InnerCtx#ctx{cmp = fun 
mango_json:cmp_raw/2});
         SubValue ->
@@ -614,10 +714,18 @@ extend_path(Field, Path) when is_binary(Field) ->
 extend_path(Field, Path) when is_list(Field) ->
     lists:foldl(fun(F, Acc) -> [F | Acc] end, Path, Field).
 
-compare(Op, Arg, Path, Cond) ->
-    case Cond of
-        true -> [];
-        _ -> [#failure{op = Op, params = [Arg], path = Path}]
+match_with_failure(Expr, Value, Op, Params, #ctx{negate = Neg} = Ctx) ->
+    case not match(Expr, Value, Ctx#ctx{verbose = false}) of
+        Neg -> [];
+        _ -> [#failure{op = Op, params = Params, ctx = Ctx}]
+    end.
+
+compare(_, _, #ctx{verbose = false}, Cond) ->
+    Cond;
+compare(Op, Arg, #ctx{negate = Neg} = Ctx, Cond) ->
+    case not Cond of
+        Neg -> [];
+        _ -> [#failure{op = Op, params = [Arg], ctx = Ctx}]
     end.
 
 % Returns true if Selector requires all
@@ -1156,10 +1264,21 @@ check_selector(Selector, Results) ->
     SelPos = normalize({[{<<"x">>, Selector}]}),
     SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, Selector}]}}]}),
 
+    ListToBool = fun(List) ->
+        case List of
+            [] -> true;
+            [_ | _] -> false
+        end
+    end,
+
     Check = fun({Result, Value}) ->
         Doc = {[{<<"x">>, Value}]},
-        ?assertEqual(Result, match_int(SelPos, Doc)),
-        ?assertEqual(not Result, match_int(SelNeg, Doc))
+
+        ?assertEqual(Result, match_int(SelPos, Doc, false)),
+        ?assertEqual(Result, ListToBool(match_int(SelPos, Doc, true))),
+
+        ?assertEqual(not Result, match_int(SelNeg, Doc, false)),
+        ?assertEqual(not Result, ListToBool(match_int(SelNeg, Doc, true)))
     end,
 
     lists:foreach(Check, Results).
@@ -1851,8 +1970,8 @@ match_failures_object_test() ->
             {<<"b">>, {[{<<"c">>, 3}]}}
         ]}
     ),
-    ?assertEqual(
-        [#failure{op = eq, type = mismatch, params = [1], path = [<<"a">>]}],
+    ?assertMatch(
+        [#failure{op = eq, type = mismatch, params = [1], ctx = #ctx{path = 
[<<"a">>]}}],
         Fails1
     ),
 
@@ -1863,8 +1982,8 @@ match_failures_object_test() ->
             {<<"b">>, {[{<<"c">>, 4}]}}
         ]}
     ),
-    ?assertEqual(
-        [#failure{op = eq, type = mismatch, params = [3], path = [<<"c">>, 
<<"b">>]}],
+    ?assertMatch(
+        [#failure{op = eq, type = mismatch, params = [3], ctx = #ctx{path = 
[<<"c">>, <<"b">>]}}],
         Fails2
     ),
 
@@ -1875,10 +1994,10 @@ match_failures_object_test() ->
             {<<"b">>, {[{<<"c">>, 4}]}}
         ]}
     ),
-    ?assertEqual(
+    ?assertMatch(
         [
-            #failure{op = eq, type = mismatch, params = [1], path = [<<"a">>]},
-            #failure{op = eq, type = mismatch, params = [3], path = [<<"c">>, 
<<"b">>]}
+            #failure{op = eq, type = mismatch, params = [1], ctx = #ctx{path = 
[<<"a">>]}},
+            #failure{op = eq, type = mismatch, params = [3], ctx = #ctx{path = 
[<<"c">>, <<"b">>]}}
         ],
         Fails3
     ).
@@ -1901,18 +2020,18 @@ match_failures_elemmatch_test() ->
     Fails1 = match_failures(
         SelElemMatch, {[{<<"a">>, []}]}
     ),
-    ?assertEqual(
-        [#failure{op = elemMatch, type = empty_list, params = [], path = 
[<<"a">>]}],
+    ?assertMatch(
+        [#failure{op = elemMatch, type = empty_list, params = [], ctx = 
#ctx{path = [<<"a">>]}}],
         Fails1
     ),
 
     Fails2 = match_failures(
         SelElemMatch, {[{<<"a">>, [3, 2]}]}
     ),
-    ?assertEqual(
+    ?assertMatch(
         [
-            #failure{op = gt, type = mismatch, params = [4], path = [0, 
<<"a">>]},
-            #failure{op = gt, type = mismatch, params = [4], path = [1, 
<<"a">>]}
+            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[0, <<"a">>]}},
+            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[1, <<"a">>]}}
         ],
         Fails2
     ).
@@ -1935,18 +2054,18 @@ match_failures_allmatch_test() ->
     Fails1 = match_failures(
         SelAllMatch, {[{<<"a">>, [4]}]}
     ),
-    ?assertEqual(
-        [#failure{op = gt, type = mismatch, params = [4], path = [0, 
<<"a">>]}],
+    ?assertMatch(
+        [#failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[0, <<"a">>]}}],
         Fails1
     ),
 
     Fails2 = match_failures(
         SelAllMatch, {[{<<"a">>, [5, 6, 3, 7, 0]}]}
     ),
-    ?assertEqual(
+    ?assertMatch(
         [
-            #failure{op = gt, type = mismatch, params = [4], path = [2, 
<<"a">>]},
-            #failure{op = gt, type = mismatch, params = [4], path = [4, 
<<"a">>]}
+            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[2, <<"a">>]}},
+            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[4, <<"a">>]}}
         ],
         Fails2
     ).
@@ -1969,8 +2088,15 @@ match_failures_allmatch_object_test() ->
     Fails1 = match_failures(
         SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 4}]}]}]}}]}
     ),
-    ?assertEqual(
-        [#failure{op = gt, type = mismatch, params = [4], path = [<<"c">>, 0, 
<<"b">>, <<"a">>]}],
+    ?assertMatch(
+        [
+            #failure{
+                op = gt,
+                type = mismatch,
+                params = [4],
+                ctx = #ctx{path = [<<"c">>, 0, <<"b">>, <<"a">>]}
+            }
+        ],
         Fails1
     ),
 
@@ -1978,8 +2104,15 @@ match_failures_allmatch_object_test() ->
         SelAllMatch,
         {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}, {[{<<"c">>, 6}]}, 
{[{<<"c">>, 3}]}]}]}}]}
     ),
-    ?assertEqual(
-        [#failure{op = gt, type = mismatch, params = [4], path = [<<"c">>, 2, 
<<"b">>, <<"a">>]}],
+    ?assertMatch(
+        [
+            #failure{
+                op = gt,
+                type = mismatch,
+                params = [4],
+                ctx = #ctx{path = [<<"c">>, 2, <<"b">>, <<"a">>]}
+            }
+        ],
         Fails2
     ),
 
@@ -1987,11 +2120,19 @@ match_failures_allmatch_object_test() ->
         SelAllMatch,
         {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 1}]}, {[]}]}]}}]}
     ),
-    ?assertEqual(
+    ?assertMatch(
         [
-            #failure{op = gt, type = mismatch, params = [4], path = [<<"c">>, 
0, <<"b">>, <<"a">>]},
             #failure{
-                op = field, type = not_found, params = [], path = [<<"c">>, 1, 
<<"b">>, <<"a">>]
+                op = gt,
+                type = mismatch,
+                params = [4],
+                ctx = #ctx{path = [<<"c">>, 0, <<"b">>, <<"a">>]}
+            },
+            #failure{
+                op = field,
+                type = not_found,
+                params = [],
+                ctx = #ctx{path = [<<"c">>, 1, <<"b">>, <<"a">>]}
             }
         ],
         Fails3

Reply via email to