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
