blob: 1f8609ac27bcfaa18de2bde0c75e986c2604a3ce [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(mango_selector_text).
-export([
convert/1,
convert/2,
append_sort_type/2
]).
-include("mango.hrl").
%% Regex for <<"\\.">>
-define(PERIOD, "\\.").
-spec convert(SelectorObject) -> LuceneQueryString when
SelectorObject :: any(),
LuceneQueryString :: binary().
convert(Object) ->
TupleTree = convert([], Object),
iolist_to_binary(to_query(TupleTree)).
-spec convert(CurrentPath, SelectorObject) -> abstract_text_selector() when
CurrentPath :: [binary()],
SelectorObject :: any().
convert(Path, {[{<<"$and">>, Args}]}) ->
Parts = [convert(Path, Arg) || Arg <- Args],
{op_and, Parts};
convert(Path, {[{<<"$or">>, Args}]}) ->
Parts = [convert(Path, Arg) || Arg <- Args],
{op_or, Parts};
convert(Path, {[{<<"$not">>, Arg}]}) ->
{op_not, {field_exists_query(Path), convert(Path, Arg)}};
convert(Path, {[{<<"$default">>, Arg}]}) ->
{op_field, {_, Query}} = convert(Path, Arg),
{op_default, Query};
% The $text operator specifies a Lucene syntax query
% so we just pull it in directly.
convert(Path, {[{<<"$text">>, Query}]}) when is_binary(Query) ->
{op_field, {make_field(Path, Query), value_str(Query)}};
% The MongoDB docs for $all are super confusing and read more
% like they screwed up the implementation of this operator
% and then just documented it as a feature.
%
% This implementation will match the behavior as closely as
% possible based on the available docs but we'll need to have
% the testing team validate how MongoDB handles edge conditions
convert(Path, {[{<<"$all">>, Args}]}) ->
case Args of
[Values] when is_list(Values) ->
% If Args is a single element array then we have to
% either match if Path is that array or if it contains
% the array as an element of an array (which isn't at all
% confusing). For Lucene to return us all possible matches
% that means we just need to search for each value in
% Path.[] and Path.[].[] and rely on our filtering to limit
% the results properly.
Fields1 = convert(Path, {[{<<"$eq">>, Values}]}),
Fields2 = convert([<<"[]">> | Path], {[{<<"$eq">>, Values}]}),
{op_or, [Fields1, Fields2]};
_ ->
% Otherwise the $all operator is equivalent to an $and
% operator so we treat it as such.
convert([<<"[]">> | Path], {[{<<"$and">>, Args}]})
end;
% The $elemMatch Lucene query is not an exact translation
% as we can't enforce that the matches are all for the same
% item in an array. We just rely on the final selector match
% to filter out anything that doesn't match. The only trick
% is that we have to add the `[]` path element since the docs
% say this has to match against an array.
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
->
field_exists_query(Path);
convert(Path, {[{<<"$lt">>, Arg}]}) ->
{op_field, {make_field(Path, Arg), range(lt, Arg)}};
convert(Path, {[{<<"$lte">>, Arg}]}) when
is_list(Arg);
is_tuple(Arg);
Arg =:= null
->
field_exists_query(Path);
convert(Path, {[{<<"$lte">>, Arg}]}) ->
{op_field, {make_field(Path, Arg), range(lte, Arg)}};
%% This is for indexable_fields
convert(Path, {[{<<"$eq">>, Arg}]}) when Arg =:= null ->
{op_null, {make_field(Path, Arg), value_str(Arg)}};
convert(Path, {[{<<"$eq">>, Args}]}) when is_list(Args) ->
Path0 = [<<"[]">> | Path],
LPart = {op_field, {make_field(Path0, length), value_str(length(Args))}},
Parts0 = [convert(Path0, {[{<<"$eq">>, Arg}]}) || Arg <- Args],
Parts = [LPart | Parts0],
{op_and, Parts};
convert(Path, {[{<<"$eq">>, {_} = Arg}]}) ->
convert(Path, Arg);
convert(Path, {[{<<"$eq">>, Arg}]}) ->
{op_field, {make_field(Path, Arg), value_str(Arg)}};
convert(Path, {[{<<"$ne">>, Arg}]}) ->
{op_not, {field_exists_query(Path), convert(Path, {[{<<"$eq">>, Arg}]})}};
convert(Path, {[{<<"$gte">>, Arg}]}) when
is_list(Arg);
is_tuple(Arg);
Arg =:= null
->
field_exists_query(Path);
convert(Path, {[{<<"$gte">>, Arg}]}) ->
{op_field, {make_field(Path, Arg), range(gte, Arg)}};
convert(Path, {[{<<"$gt">>, Arg}]}) when
is_list(Arg);
is_tuple(Arg);
Arg =:= null
->
field_exists_query(Path);
convert(Path, {[{<<"$gt">>, Arg}]}) ->
{op_field, {make_field(Path, Arg), range(gt, Arg)}};
convert(Path, {[{<<"$in">>, Args}]}) ->
{op_or, convert_in(Path, Args)};
convert(Path, {[{<<"$nin">>, Args}]}) ->
{op_not, {field_exists_query(Path), convert(Path, {[{<<"$in">>, Args}]})}};
convert(Path, {[{<<"$exists">>, ShouldExist}]}) ->
FieldExists = field_exists_query(Path),
case ShouldExist of
true -> FieldExists;
false -> {op_not, {FieldExists, false}}
end;
% We're not checking the actual type here, just looking for
% anything that has a possibility of matching by checking
% for the field name. We use the same logic for $exists on
% the actual query.
convert(Path, {[{<<"$type">>, _}]}) ->
field_exists_query(Path);
convert(Path, {[{<<"$mod">>, _}]}) ->
field_exists_query(Path, "number");
% The lucene regular expression engine does not use java's regex engine but
% instead a custom implementation. The syntax is therefore different, so we do
% would get different behavior than our view indexes. To be consistent, we will
% simply return docs for fields that exist and then run our match filter.
%
% It has a dedicated type so that `mango_idx_text:indexable_fields/1`
% could handle this case properly.
convert(Path, {[{<<"$regex">>, _}]}) ->
{op_regex, path_str(Path)};
convert(Path, {[{<<"$size">>, Arg}]}) ->
{op_field, {make_field([<<"[]">> | Path], length), value_str(Arg)}};
% All other operators are internal assertion errors for
% matching because we either should've removed them during
% normalization or something else broke.
convert(_Path, {[{<<"$", _/binary>> = Op, _}]}) ->
?MANGO_ERROR({invalid_operator, Op});
% We've hit a field name specifier. Check if the field name is accessing
% arrays. Convert occurrences of element position references to .[]. Then we
% need to break the name into path parts and continue our conversion.
convert(Path, {[{Field0, Cond}]}) ->
{ok, PP0} =
case Field0 of
<<>> ->
{ok, []};
_ ->
mango_util:parse_field(Field0)
end,
% Later on, we perform a lucene_escape_user call on the
% final Path, which calls parse_field again. Calling the function
% twice converts <<"a\\.b">> to [<<"a">>,<<"b">>]. This leads to
% an incorrect query since we need [<<"a.b">>]. Without breaking
% our escaping mechanism, we simply revert this first parse_field
% effect and replace instances of "." to "\\.".
MP = mango_util:cached_re(mango_period, ?PERIOD),
PP1 = [
re:replace(
P,
MP,
<<"\\\\.">>,
[global, {return, binary}]
)
|| P <- PP0
],
{PP2, HasInteger} = replace_array_indexes(PP1, [], false),
NewPath = PP2 ++ Path,
case HasInteger of
true ->
OldPath = lists:reverse(PP1, Path),
OldParts = convert(OldPath, Cond),
NewParts = convert(NewPath, Cond),
{op_or, [OldParts, NewParts]};
false ->
convert(NewPath, Cond)
end;
%% For $in
convert(Path, Val) when is_binary(Val); is_number(Val); is_boolean(Val) ->
{op_field, {make_field(Path, Val), value_str(Val)}};
% Anything else is a bad selector.
convert(_Path, {Props} = Sel) when length(Props) > 1 ->
erlang:error({unnormalized_selector, Sel}).
to_query_nested(Args) ->
QueryArgs = lists:map(fun to_query/1, Args),
% removes empty queries that result from selectors with empty arrays
FilterFun = fun(A) -> A =/= [] andalso A =/= "()" end,
lists:filter(FilterFun, QueryArgs).
-spec to_query(abstract_text_selector()) -> LuceneQueryStringPieces when
LuceneQueryStringPieces :: [binary()].
to_query({op_and, []}) ->
[];
to_query({op_and, Args}) when is_list(Args) ->
case to_query_nested(Args) of
[] -> [];
QueryArgs -> ["(", mango_util:join(<<" AND ">>, QueryArgs), ")"]
end;
to_query({op_or, []}) ->
[];
to_query({op_or, Args}) when is_list(Args) ->
case to_query_nested(Args) of
[] -> [];
QueryArgs -> ["(", mango_util:join(" OR ", QueryArgs), ")"]
end;
to_query({op_not, {ExistsQuery, Arg}}) when is_tuple(Arg) ->
case to_query(Arg) of
[] -> ["(", to_query(ExistsQuery), ")"];
Query -> ["(", to_query(ExistsQuery), " AND NOT (", Query, "))"]
end;
%% For $exists:false
to_query({op_not, {ExistsQuery, false}}) ->
["($fieldnames:/.*/ ", " AND NOT (", to_query(ExistsQuery), "))"];
%% We escape : and / for now for values and all lucene chars for fieldnames
%% This needs to be resolved.
to_query({op_field, {Name, Value}}) ->
NameBin = iolist_to_binary(Name),
["(", mango_util:lucene_escape_user(NameBin), ":", Value, ")"];
%% This is for indexable_fields
to_query({op_null, {Name, Value}}) ->
NameBin = iolist_to_binary(Name),
["(", mango_util:lucene_escape_user(NameBin), ":", Value, ")"];
to_query({op_fieldname, {Name, Wildcard}}) ->
NameBin = iolist_to_binary(Name),
["($fieldnames:", mango_util:lucene_escape_user(NameBin), Wildcard, ")"];
%% This is for indexable_fields
to_query({op_regex, Name}) ->
NameBin = iolist_to_binary([Name, ":"]),
["($fieldnames:", mango_util:lucene_escape_user(NameBin), "string)"];
to_query({op_default, Value}) ->
["($default:", Value, ")"].
%% We match on fieldname and fieldname.[]
convert_in(Path, Args) ->
Path0 = [<<"[]">> | Path],
lists:map(
fun(Arg) ->
case Arg of
{Object} ->
Parts = lists:map(
fun(SubObject) ->
Fields1 = convert(Path, {[SubObject]}),
Fields2 = convert(Path0, {[SubObject]}),
{op_or, [Fields1, Fields2]}
end,
Object
),
{op_or, Parts};
SingleVal ->
Fields1 = {op_field, {make_field(Path, SingleVal), value_str(SingleVal)}},
Fields2 = {op_field, {make_field(Path0, SingleVal), value_str(SingleVal)}},
{op_or, [Fields1, Fields2]}
end
end,
Args
).
make_field(Path, length) ->
[path_str(Path), <<":length">>];
make_field(Path, Arg) ->
[path_str(Path), <<":">>, type_str(Arg)].
range(lt, Arg) ->
Min = get_range(min, Arg),
[<<"[", Min/binary, " TO ">>, value_str(Arg), <<"}">>];
range(lte, Arg) ->
Min = get_range(min, Arg),
[<<"[", Min/binary, " TO ">>, value_str(Arg), <<"]">>];
range(gte, Arg) ->
Max = get_range(max, Arg),
[<<"[">>, value_str(Arg), <<" TO ", Max/binary, "]">>];
range(gt, Arg) ->
Max = get_range(max, Arg),
[<<"{">>, value_str(Arg), <<" TO ", Max/binary, "]">>].
get_range(min, Arg) when is_number(Arg) ->
<<"-Infinity">>;
get_range(min, _Arg) ->
<<"\"\"">>;
get_range(max, Arg) when is_number(Arg) ->
<<"Infinity">>;
get_range(max, _Arg) ->
<<"\u0x10FFFF">>.
field_exists_query(Path) ->
% We specify two here for :* and .* so that we don't incorrectly
% match a path foo.name against foo.name_first (if were to just
% append * instead).
Parts = [
% We need to remove the period from the path list to indicate that it is
% a path separator. We escape the colon because it is not used as a
% separator and we escape colons in field names.
{op_fieldname, {[path_str(Path), ":"], "*"}},
{op_fieldname, {[path_str(Path)], ".*"}}
],
{op_or, Parts}.
field_exists_query(Path, Type) ->
{op_fieldname, {[path_str(Path), ":"], Type}}.
path_str(Path) ->
path_str(Path, []).
path_str([], Acc) ->
Acc;
path_str([Part], Acc) ->
% No reverse because Path is backwards
% during recursion of convert.
[Part | Acc];
path_str([Part | Rest], Acc) ->
case Part of
% do not append a period if Part is blank
<<>> ->
path_str(Rest, [Acc]);
_ ->
path_str(Rest, [<<".">>, Part | Acc])
end.
type_str(Value) when is_number(Value) ->
<<"number">>;
type_str(Value) when is_boolean(Value) ->
<<"boolean">>;
type_str(Value) when is_binary(Value) ->
<<"string">>;
type_str(null) ->
<<"null">>.
value_str(Value) when is_binary(Value) ->
case mango_util:is_number_string(Value) of
true ->
<<"\"", Value/binary, "\"">>;
false ->
Escaped = mango_util:lucene_escape_query_value(Value),
<<"\"", Escaped/binary, "\"">>
end;
value_str(Value) when is_integer(Value) ->
list_to_binary(integer_to_list(Value));
value_str(Value) when is_float(Value) ->
list_to_binary(float_to_list(Value));
value_str(true) ->
<<"true">>;
value_str(false) ->
<<"false">>;
value_str(null) ->
<<"true">>.
append_sort_type(RawSortField, Selector) ->
EncodeField = mango_util:lucene_escape_user(RawSortField),
String = mango_util:has_suffix(EncodeField, <<"_3astring">>),
Number = mango_util:has_suffix(EncodeField, <<"_3anumber">>),
case {String, Number} of
{true, _} ->
<<EncodeField/binary, "<string>">>;
{_, true} ->
<<EncodeField/binary, "<number>">>;
_ ->
Type = get_sort_type(RawSortField, Selector),
<<EncodeField/binary, Type/binary>>
end.
get_sort_type(Field, Selector) ->
Types = get_sort_types(Field, Selector, []),
case lists:usort(Types) of
[str] -> <<"_3astring<string>">>;
[num] -> <<"_3anumber<number>">>;
_ -> ?MANGO_ERROR({text_sort_error, Field})
end.
get_sort_types(Field, {[{Field, {[{<<"$", _/binary>>, Cond}]}}]}, Acc) when
is_binary(Cond)
->
[str | Acc];
get_sort_types(Field, {[{Field, {[{<<"$", _/binary>>, Cond}]}}]}, Acc) when
is_number(Cond)
->
[num | Acc];
get_sort_types(Field, {[{_, Cond}]}, Acc) when is_list(Cond) ->
lists:foldl(
fun(Arg, InnerAcc) ->
get_sort_types(Field, Arg, InnerAcc)
end,
Acc,
Cond
);
get_sort_types(Field, {[{_, Cond}]}, Acc) when is_tuple(Cond) ->
get_sort_types(Field, Cond, Acc);
get_sort_types(_Field, _, Acc) ->
Acc.
replace_array_indexes([], NewPartsAcc, HasIntAcc) ->
{NewPartsAcc, HasIntAcc};
replace_array_indexes([Part | Rest], NewPartsAcc, HasIntAcc) ->
{NewPart, HasInt} =
try
_ = list_to_integer(binary_to_list(Part)),
{<<"[]">>, true}
catch
_:_ ->
{Part, false}
end,
replace_array_indexes(
Rest,
[NewPart | NewPartsAcc],
HasInt or HasIntAcc
).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
convert_selector(Selector) ->
convert([], test_util:as_selector(Selector)).
convert_fields_test() ->
?assertEqual(
{op_null, {[[<<"field">>], <<":">>, <<"null">>], <<"true">>}},
convert_selector(#{<<"field">> => null})
),
?assertEqual(
{op_field, {[[<<"field">>], <<":">>, <<"boolean">>], <<"true">>}},
convert_selector(#{<<"field">> => true})
),
?assertEqual(
{op_field, {[[<<"field">>], <<":">>, <<"number">>], <<"42">>}},
convert_selector(#{<<"field">> => 42})
),
?assertEqual(
{op_field, {[[<<"field">>], <<":">>, <<"string">>], <<"\"value\"">>}},
convert_selector(#{<<"field">> => <<"value">>})
),
?assertEqual(
{op_and, [
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":length">>], <<"3">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"1">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"2">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"3">>}}
]},
convert_selector(#{<<"field">> => [1, 2, 3]})
),
?assertEqual(
{op_field, {
[[<<"field1">>, <<".">>, <<"field2">>], <<":">>, <<"string">>], <<"\"value\"">>
}},
convert_selector(#{<<"field1">> => #{<<"field2">> => <<"value">>}})
),
?assertEqual(
{op_and, [
{op_field, {[[<<"field2">>], <<":">>, <<"string">>], <<"\"value2\"">>}},
{op_field, {[[<<"field1">>], <<":">>, <<"string">>], <<"\"value1\"">>}}
]},
convert_selector(#{<<"field1">> => <<"value1">>, <<"field2">> => <<"value2">>})
).
convert_default_test() ->
?assertEqual(
{op_default, <<"\"text\"">>},
convert_selector(#{<<"$default">> => #{<<"$text">> => <<"text">>}})
).
convert_lt_test() ->
?assertEqual(
{op_field,
{[[<<"field">>], <<":">>, <<"number">>], [<<"[-Infinity TO ">>, <<"42">>, <<"}">>]}},
convert_selector(#{<<"field">> => #{<<"$lt">> => 42}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$lt">> => [1, 2, 3]}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$lt">> => null}})
).
convert_lte_test() ->
?assertEqual(
{op_field,
{[[<<"field">>], <<":">>, <<"number">>], [<<"[-Infinity TO ">>, <<"42">>, <<"]">>]}},
convert_selector(#{<<"field">> => #{<<"$lte">> => 42}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$lte">> => [1, 2, 3]}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$lte">> => null}})
).
convert_eq_test() ->
?assertEqual(
{op_field, {[[<<"field">>], <<":">>, <<"number">>], <<"42">>}},
convert_selector(#{<<"field">> => #{<<"$eq">> => 42}})
),
?assertEqual(
{op_and, [
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":length">>], <<"3">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"1">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"2">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"3">>}}
]},
convert_selector(#{<<"field">> => #{<<"$eq">> => [1, 2, 3]}})
),
?assertEqual(
{op_null, {[[<<"field">>], <<":">>, <<"null">>], <<"true">>}},
convert_selector(#{<<"field">> => #{<<"$eq">> => null}})
).
convert_ne_test() ->
?assertEqual(
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
{op_field, {[[<<"field">>], <<":">>, <<"number">>], <<"42">>}}
}},
convert_selector(#{<<"field">> => #{<<"$ne">> => 42}})
).
convert_gte_test() ->
?assertEqual(
{op_field,
{[[<<"field">>], <<":">>, <<"number">>], [<<"[">>, <<"42">>, <<" TO Infinity]">>]}},
convert_selector(#{<<"field">> => #{<<"$gte">> => 42}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$gte">> => [1, 2, 3]}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$gte">> => null}})
).
convert_gt_test() ->
?assertEqual(
{op_field,
{[[<<"field">>], <<":">>, <<"number">>], [<<"{">>, <<"42">>, <<" TO Infinity]">>]}},
convert_selector(#{<<"field">> => #{<<"$gt">> => 42}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$gt">> => [1, 2, 3]}})
),
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$gt">> => null}})
).
convert_all_test() ->
?assertEqual(
{op_and, [
{op_field, {
[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"string">>], <<"\"value1\"">>
}},
{op_field, {
[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"string">>], <<"\"value2\"">>
}}
]},
convert_selector(#{<<"field">> => #{<<"$all">> => [<<"value1">>, <<"value2">>]}})
).
convert_elemMatch_test() ->
?assertEqual(
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"string">>], <<"\"value\"">>}},
convert_selector(#{<<"field">> => #{<<"$elemMatch">> => #{<<"$eq">> => <<"value">>}}})
).
convert_allMatch_test() ->
?assertEqual(
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"string">>], <<"\"value\"">>}},
convert_selector(#{<<"field">> => #{<<"$allMatch">> => #{<<"$eq">> => <<"value">>}}})
).
convert_keyMapMatch_test() ->
?assertThrow(
{mango_error, mango_selector_text, {invalid_operator, <<"$keyMapMatch">>}},
convert_selector(#{<<"field">> => #{<<"$keyMapMatch">> => #{<<"key">> => <<"value">>}}})
).
convert_in_test() ->
?assertEqual(
{op_or, []},
convert_selector(#{<<"field">> => #{<<"$in">> => []}})
),
?assertEqual(
{op_or, [
{op_or, [
{op_field, {[[<<"field">>], <<":">>, <<"string">>], <<"\"value1\"">>}},
{op_field, {
[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"string">>], <<"\"value1\"">>
}}
]},
{op_or, [
{op_field, {[[<<"field">>], <<":">>, <<"string">>], <<"\"value2\"">>}},
{op_field, {
[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"string">>], <<"\"value2\"">>
}}
]}
]},
convert_selector(#{<<"field">> => #{<<"$in">> => [<<"value1">>, <<"value2">>]}})
).
convert_nin_test() ->
?assertEqual(
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
{op_or, []}
}},
convert_selector(#{<<"field">> => #{<<"$nin">> => []}})
),
?assertEqual(
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
{op_or, [
{op_or, [
{op_field, {[[<<"field">>], <<":">>, <<"number">>], <<"1">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"1">>}}
]},
{op_or, [
{op_field, {[[<<"field">>], <<":">>, <<"number">>], <<"2">>}},
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":">>, <<"number">>], <<"2">>}}
]}
]}
}},
convert_selector(#{<<"field">> => #{<<"$nin">> => [1, 2]}})
).
convert_exists_test() ->
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$exists">> => true}})
),
?assertEqual(
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
false
}},
convert_selector(#{<<"field">> => #{<<"$exists">> => false}})
).
convert_type_test() ->
?assertEqual(
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
convert_selector(#{<<"field">> => #{<<"$type">> => <<"string">>}})
).
convert_mod_test() ->
?assertEqual(
{op_fieldname, {[[<<"field">>], ":"], "number"}},
convert_selector(#{<<"field">> => #{<<"$mod">> => [2, 0]}})
).
convert_regex_test() ->
?assertEqual(
{op_regex, [<<"field">>]},
convert_selector(#{<<"field">> => #{<<"$regex">> => <<".*">>}})
).
convert_size_test() ->
?assertEqual(
{op_field, {[[<<"field">>, <<".">>, <<"[]">>], <<":length">>], <<"6">>}},
convert_selector(#{<<"field">> => #{<<"$size">> => 6}})
).
convert_not_test() ->
?assertEqual(
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
{op_fieldname, {[[<<"field">>], ":"], "number"}}
}},
convert_selector(#{<<"field">> => #{<<"$not">> => #{<<"$mod">> => [2, 0]}}})
).
convert_and_test() ->
?assertEqual(
{op_and, []},
convert_selector(#{<<"$and">> => []})
),
?assertEqual(
{op_and, [{op_field, {[[<<"field">>], <<":">>, <<"string">>], <<"\"value\"">>}}]},
convert_selector(#{<<"$and">> => [#{<<"field">> => <<"value">>}]})
),
?assertEqual(
{op_and, [
{op_field, {[[<<"field1">>], <<":">>, <<"string">>], <<"\"value1\"">>}},
{op_field, {[[<<"field2">>], <<":">>, <<"string">>], <<"\"value2\"">>}}
]},
convert_selector(#{
<<"$and">> => [#{<<"field1">> => <<"value1">>}, #{<<"field2">> => <<"value2">>}]
})
).
convert_or_test() ->
?assertEqual(
{op_or, []},
convert_selector(#{<<"$or">> => []})
),
?assertEqual(
{op_or, [{op_field, {[[<<"field">>], <<":">>, <<"string">>], <<"\"value\"">>}}]},
convert_selector(#{<<"$or">> => [#{<<"field">> => <<"value">>}]})
),
?assertEqual(
{op_or, [
{op_field, {[[<<"field1">>], <<":">>, <<"string">>], <<"\"value1\"">>}},
{op_field, {[[<<"field2">>], <<":">>, <<"string">>], <<"\"value2\"">>}}
]},
convert_selector(#{
<<"$or">> => [#{<<"field1">> => <<"value1">>}, #{<<"field2">> => <<"value2">>}]
})
).
convert_nor_test() ->
?assertEqual(
{op_and, []},
convert_selector(#{<<"$nor">> => []})
),
?assertEqual(
{op_and, [
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field">>], ":"], "*"}},
{op_fieldname, {[[<<"field">>]], ".*"}}
]},
{op_field, {[[<<"field">>], <<":">>, <<"string">>], <<"\"value\"">>}}
}}
]},
convert_selector(#{<<"$nor">> => [#{<<"field">> => <<"value">>}]})
),
?assertEqual(
{op_and, [
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field1">>], ":"], "*"}},
{op_fieldname, {[[<<"field1">>]], ".*"}}
]},
{op_field, {[[<<"field1">>], <<":">>, <<"string">>], <<"\"value1\"">>}}
}},
{op_not, {
{op_or, [
{op_fieldname, {[[<<"field2">>], ":"], "*"}},
{op_fieldname, {[[<<"field2">>]], ".*"}}
]},
{op_field, {[[<<"field2">>], <<":">>, <<"string">>], <<"\"value2\"">>}}
}}
]},
convert_selector(#{
<<"$nor">> => [#{<<"field1">> => <<"value1">>}, #{<<"field2">> => <<"value2">>}]
})
).
to_query_test() ->
F = fun(S) -> iolist_to_binary(to_query(S)) end,
Input = {<<"name">>, <<"value">>},
?assertEqual(<<"(name:value)">>, F({op_field, Input})),
?assertEqual(
<<"(name1.name2_3atype:value)">>,
F({op_field, {[[<<"name1">>, <<".">>, <<"name2">>], <<":">>, <<"type">>], <<"value">>}})
),
?assertEqual(<<"(name:value)">>, F({op_null, Input})),
?assertEqual(<<"($fieldnames:name_3astring)">>, F({op_regex, <<"name">>})),
?assertEqual(<<"($fieldnames:name_3a.*)">>, F({op_fieldname, {<<"name">>, <<"_3a.*">>}})),
Arg1 = {op_default, <<"value">>},
?assertEqual(<<"($default:value)">>, F(Arg1)),
Arg2 = {op_field, Input},
?assertEqual(<<"($fieldnames:/.*/ AND NOT ((name:value)))">>, F({op_not, {Arg2, false}})),
?assertEqual(<<"((name:value))">>, F({op_not, {Arg2, {op_and, []}}})),
?assertEqual(<<"">>, F({op_and, []})),
?assertEqual(<<"(($default:value))">>, F({op_and, [Arg1]})),
?assertEqual(<<"(($default:value) AND (name:value))">>, F({op_and, [Arg1, Arg2]})),
?assertEqual(
<<"(($default:value) AND (name:value))">>, F({op_and, [Arg1, {op_and, []}, Arg2]})
),
?assertEqual(<<"">>, F({op_or, []})),
?assertEqual(<<"(($default:value))">>, F({op_or, [Arg1]})),
?assertEqual(<<"(($default:value) OR (name:value))">>, F({op_or, [Arg1, Arg2]})),
?assertEqual(<<"(($default:value) OR (name:value))">>, F({op_or, [Arg1, Arg2, {op_or, []}]})).
-endif.