|
@ -0,0 +1,291 @@ |
|
|
|
|
|
%% Copyright (c) 2012, Magnus Klaar <klaar@ninenines.eu> |
|
|
|
|
|
%% |
|
|
|
|
|
%% Permission to use, copy, modify, and/or distribute this software for any |
|
|
|
|
|
%% purpose with or without fee is hereby granted, provided that the above |
|
|
|
|
|
%% copyright notice and this permission notice appear in all copies. |
|
|
|
|
|
%% |
|
|
|
|
|
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
|
|
|
|
|
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|
|
|
|
|
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
|
|
|
|
|
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
|
|
|
|
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
|
|
|
|
|
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
|
|
|
|
|
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
|
|
|
|
|
|
|
|
|
|
%% @doc Query processing functions. |
|
|
|
|
|
-module(glc_lib). |
|
|
|
|
|
|
|
|
|
|
|
-export([ |
|
|
|
|
|
reduce/1 |
|
|
|
|
|
]). |
|
|
|
|
|
|
|
|
|
|
|
%% @doc Return a reduced version of a query. |
|
|
|
|
|
%% |
|
|
|
|
|
%% The purpose of this function is to ensure that a query filter |
|
|
|
|
|
%% is in the simplest possible form. The query filter returned |
|
|
|
|
|
%% from this function is functionally equivalent to the original. |
|
|
|
|
|
reduce(Query) -> |
|
|
|
|
|
repeat(Query, fun(Q0) -> |
|
|
|
|
|
Q1 = flatten(Q0), |
|
|
|
|
|
Q2 = required(Q1), |
|
|
|
|
|
Q3 = flatten(Q2), |
|
|
|
|
|
Q4 = common(Q3), |
|
|
|
|
|
Q4 |
|
|
|
|
|
end). |
|
|
|
|
|
|
|
|
|
|
|
%% @private Repeatedly apply a function to a query. |
|
|
|
|
|
%% This is used for query transformation functions |
|
|
|
|
|
%% applied multiple times |
|
|
|
|
|
repeat(Query, Fun) -> |
|
|
|
|
|
case Fun(Query) of |
|
|
|
|
|
Query -> Query; |
|
|
|
|
|
Query2 -> repeat(Query2, Fun) |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% @private Flatten a condition tree. |
|
|
|
|
|
flatten({all, [Cond]}) -> |
|
|
|
|
|
Cond; |
|
|
|
|
|
flatten({any, [Cond]}) -> |
|
|
|
|
|
Cond; |
|
|
|
|
|
flatten({all, Conds}) -> |
|
|
|
|
|
flatten_all([flatten(Cond) || Cond <- Conds]); |
|
|
|
|
|
flatten({any, [_|_]=Conds}) -> |
|
|
|
|
|
flatten_any([flatten(Cond) || Cond <- Conds]); |
|
|
|
|
|
flatten({with, Cond, Action}) -> |
|
|
|
|
|
{with, flatten(Cond), Action}; |
|
|
|
|
|
flatten(Other) -> |
|
|
|
|
|
return_valid(Other). |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
%% @private Flatten and remove duplicate members of an "all" filter. |
|
|
|
|
|
flatten_all(Conds) -> |
|
|
|
|
|
{all, lists:usort(flatten_all_(Conds))}. |
|
|
|
|
|
|
|
|
|
|
|
flatten_all_([{all, Conds}|T]) -> |
|
|
|
|
|
Conds ++ flatten_all_(T); |
|
|
|
|
|
flatten_all_([H|T]) -> |
|
|
|
|
|
[H|flatten_all_(T)]; |
|
|
|
|
|
flatten_all_([]) -> |
|
|
|
|
|
[]. |
|
|
|
|
|
|
|
|
|
|
|
%% @private Flatten and remove duplicate members of an "any" filter. |
|
|
|
|
|
flatten_any(Conds) -> |
|
|
|
|
|
{any, lists:usort(flatten_any_(Conds))}. |
|
|
|
|
|
|
|
|
|
|
|
flatten_any_([{any, Conds}|T]) -> |
|
|
|
|
|
Conds ++ flatten_any_(T); |
|
|
|
|
|
flatten_any_([H|T]) -> |
|
|
|
|
|
[H|flatten_any_(T)]; |
|
|
|
|
|
flatten_any_([]) -> |
|
|
|
|
|
[]. |
|
|
|
|
|
|
|
|
|
|
|
%% @private Factor out required filters. |
|
|
|
|
|
%% |
|
|
|
|
|
%% Identical conditions may occur in all sub-filters of an "any" filter. These |
|
|
|
|
|
%% filters can be tested once before testing the conditions that are unique to |
|
|
|
|
|
%% each sub-filter. |
|
|
|
|
|
%% |
|
|
|
|
|
%% Assume that the input has been flattened first in order to eliminate all |
|
|
|
|
|
%% occurances of an "any" filters being "sub-filters" of "any" filters. |
|
|
|
|
|
required({any, [H|_]=Conds}) -> |
|
|
|
|
|
Init = ordsets:from_list(case H of {all, Init2} -> Init2; H -> [H] end), |
|
|
|
|
|
case required(Conds, Init) of |
|
|
|
|
|
[] -> |
|
|
|
|
|
Conds2 = [required(Cond) || Cond <- Conds], |
|
|
|
|
|
{any, Conds2}; |
|
|
|
|
|
[_|_]=Req -> |
|
|
|
|
|
Conds2 = [required(deleteall(Cond, Req)) || Cond <- Conds], |
|
|
|
|
|
{all, [{all, Req}, {any, Conds2}]} |
|
|
|
|
|
end; |
|
|
|
|
|
required({all, Conds}) -> |
|
|
|
|
|
{all, [required(Cond) || Cond <- Conds]}; |
|
|
|
|
|
required(Other) -> |
|
|
|
|
|
Other. |
|
|
|
|
|
|
|
|
|
|
|
required([{all, Conds}|T], Acc) -> |
|
|
|
|
|
required(T, ordsets:intersection(ordsets:from_list(Conds), Acc)); |
|
|
|
|
|
required([{any, _}|_]=Cond, Acc) -> |
|
|
|
|
|
erlang:error(badarg, [Cond, Acc]); |
|
|
|
|
|
required([H|T], Acc) -> |
|
|
|
|
|
required(T, ordsets:intersection(ordsets:from_list([H]), Acc)); |
|
|
|
|
|
required([], Acc) -> |
|
|
|
|
|
Acc. |
|
|
|
|
|
|
|
|
|
|
|
%% @private Factor our common filters. |
|
|
|
|
|
%% |
|
|
|
|
|
%% Identical conditions may occur in some sub-filters of an "all" filter. These |
|
|
|
|
|
%% filters can be tested once before testing the conditions that are unique to |
|
|
|
|
|
%% each sub-filter. This is different from factoring out common sub-filters |
|
|
|
|
|
%% in an "any" filter where the only those sub-filters that exist in all |
|
|
|
|
|
%% members. |
|
|
|
|
|
%% |
|
|
|
|
|
%% Assume that the input has been flattened first in order to eliminate all |
|
|
|
|
|
%% occurances of an "any" filters being "sub-filters" of "any" filters. |
|
|
|
|
|
common({all, Conds}) -> |
|
|
|
|
|
case common_(Conds, []) of |
|
|
|
|
|
{found, Found} -> |
|
|
|
|
|
{all, [Found|[delete(Cond, Found) || Cond <- Conds]]}; |
|
|
|
|
|
nonefound -> |
|
|
|
|
|
{all, [common(Cond) || Cond <- Conds]} |
|
|
|
|
|
end; |
|
|
|
|
|
common({any, Conds}) -> |
|
|
|
|
|
{any, [common(Cond) || Cond <- Conds]}; |
|
|
|
|
|
common(Other) -> |
|
|
|
|
|
Other. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
common_([{any, Conds}|T], Seen) -> |
|
|
|
|
|
Set = ordsets:from_list(Conds), |
|
|
|
|
|
case ordsets:intersection(Set, Seen) of |
|
|
|
|
|
[] -> common_(T, ordsets:union(Set, Seen)); |
|
|
|
|
|
[H|_] -> {found, H} |
|
|
|
|
|
end; |
|
|
|
|
|
common_([H|T], Seen) -> |
|
|
|
|
|
case ordsets:is_element(H, Seen) of |
|
|
|
|
|
false -> common_(T, ordsets:union(ordsets:from_list([H]), Seen)); |
|
|
|
|
|
true -> {found, H} |
|
|
|
|
|
end; |
|
|
|
|
|
common_([], _Seen) -> |
|
|
|
|
|
nonefound. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
%% @private Delete all occurances of a filter. |
|
|
|
|
|
%% |
|
|
|
|
|
%% Assume that the function is called because a filter is tested |
|
|
|
|
|
%% by a parent filter. It is therefore safe to replace occurances |
|
|
|
|
|
%% with a null filter that always returns true. |
|
|
|
|
|
delete({all, Conds}, Filter) -> |
|
|
|
|
|
{all, [delete(Cond, Filter) || Cond <- Conds, Cond =/= Filter]}; |
|
|
|
|
|
delete({any, Conds}, Filter) -> |
|
|
|
|
|
{any, [delete(Cond, Filter) || Cond <- Conds, Cond =/= Filter]}; |
|
|
|
|
|
delete(Filter, Filter) -> |
|
|
|
|
|
{null, true}; |
|
|
|
|
|
delete(Cond, _Filter) -> |
|
|
|
|
|
Cond. |
|
|
|
|
|
|
|
|
|
|
|
%% @private Delete all occurances of multiple filters. |
|
|
|
|
|
deleteall(Filter, [H|T]) -> |
|
|
|
|
|
deleteall(delete(Filter, H), T); |
|
|
|
|
|
deleteall(Filter, []) -> |
|
|
|
|
|
Filter. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
%% @private Test if a term is a valid filter. |
|
|
|
|
|
is_valid({Field, '<', _Term}) when is_atom(Field) -> |
|
|
|
|
|
true; |
|
|
|
|
|
is_valid({Field, '=', _Term}) when is_atom(Field) -> |
|
|
|
|
|
true; |
|
|
|
|
|
is_valid({Field, '>', _Term}) when is_atom(Field) -> |
|
|
|
|
|
true; |
|
|
|
|
|
is_valid({null, true}) -> |
|
|
|
|
|
true; |
|
|
|
|
|
is_valid({null, false}) -> |
|
|
|
|
|
true; |
|
|
|
|
|
is_valid(_Other) -> |
|
|
|
|
|
false. |
|
|
|
|
|
|
|
|
|
|
|
%% @private Assert that a term is a valid filter. |
|
|
|
|
|
%% If the term is a valid filter. The original term will be returned. |
|
|
|
|
|
%% If the term is not a valid filter. A `badarg' error is thrown. |
|
|
|
|
|
return_valid(Term) -> |
|
|
|
|
|
case is_valid(Term) of |
|
|
|
|
|
true -> Term; |
|
|
|
|
|
false -> |
|
|
|
|
|
io:format(user, "~w~n", [Term]), |
|
|
|
|
|
erlang:error(badarg, [Term]) |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-ifdef(TEST). |
|
|
|
|
|
-include_lib("eunit/include/eunit.hrl"). |
|
|
|
|
|
|
|
|
|
|
|
all_one_test() -> |
|
|
|
|
|
?assertEqual(glc:eq(a, 1), |
|
|
|
|
|
glc_lib:reduce(glc:all([glc:eq(a, 1)])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
all_sort_test() -> |
|
|
|
|
|
?assertEqual(glc:all([glc:eq(a, 1), glc:eq(b, 2)]), |
|
|
|
|
|
glc_lib:reduce(glc:all([glc:eq(b, 2), glc:eq(a, 1)])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
any_one_test() -> |
|
|
|
|
|
?assertEqual(glc:eq(a, 1), |
|
|
|
|
|
glc_lib:reduce(glc:any([glc:eq(a, 1)])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
any_sort_test() -> |
|
|
|
|
|
?assertEqual(glc:any([glc:eq(a, 1), glc:eq(b, 2)]), |
|
|
|
|
|
glc_lib:reduce(glc:any([glc:eq(b, 2), glc:eq(a, 1)])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
all_nest_test() -> |
|
|
|
|
|
?assertEqual(glc:all([glc:eq(a, 1), glc:eq(b, 2)]), |
|
|
|
|
|
glc_lib:reduce(glc:all([glc:eq(a, 1), glc:all([glc:eq(b, 2)])])) |
|
|
|
|
|
), |
|
|
|
|
|
?assertEqual(glc:all([glc:eq(a, 1), glc:eq(b, 2), glc:eq(c, 3)]), |
|
|
|
|
|
glc_lib:reduce(glc:all([glc:eq(c, 3), |
|
|
|
|
|
glc:all([glc:eq(a, 1), |
|
|
|
|
|
glc:all([glc:eq(b, 2)])])])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
any_nest_test() -> |
|
|
|
|
|
?assertEqual(glc:any([glc:eq(a, 1), glc:eq(b, 2)]), |
|
|
|
|
|
glc_lib:reduce(glc:any([glc:eq(a, 1), glc:any([glc:eq(b, 2)])])) |
|
|
|
|
|
), |
|
|
|
|
|
?assertEqual(glc:any([glc:eq(a, 1), glc:eq(b, 2), glc:eq(c, 3)]), |
|
|
|
|
|
glc_lib:reduce(glc:any([glc:eq(c, 3), |
|
|
|
|
|
glc:any([glc:eq(a, 1), |
|
|
|
|
|
glc:any([glc:eq(b, 2)])])])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
all_equiv_test() -> |
|
|
|
|
|
?assertEqual(glc:eq(a, 1), |
|
|
|
|
|
glc_lib:reduce(glc:all([glc:eq(a, 1), glc:eq(a, 1)])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
any_equiv_test() -> |
|
|
|
|
|
?assertEqual(glc:eq(a, 1), |
|
|
|
|
|
glc_lib:reduce(glc:any([glc:eq(a, 1), glc:eq(a, 1)])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
any_required_test() -> |
|
|
|
|
|
?assertEqual( |
|
|
|
|
|
glc:all([ |
|
|
|
|
|
glc:any([glc:eq(b, 2), glc:eq(c, 3)]), |
|
|
|
|
|
glc:eq(a, 1) |
|
|
|
|
|
]), |
|
|
|
|
|
glc_lib:reduce( |
|
|
|
|
|
glc:any([ |
|
|
|
|
|
glc:all([glc:eq(a, 1), glc:eq(b, 2)]), |
|
|
|
|
|
glc:all([glc:eq(a, 1), glc:eq(c, 3)])])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
all_common_test() -> |
|
|
|
|
|
?assertEqual( |
|
|
|
|
|
glc:all([glc:eq(a, 1), glc:eq(b, 2), glc:eq(c, 3)]), |
|
|
|
|
|
glc_lib:reduce( |
|
|
|
|
|
glc:all([ |
|
|
|
|
|
glc:any([glc:eq(a, 1), glc:eq(b, 2)]), |
|
|
|
|
|
glc:any([glc:eq(a, 1), glc:eq(c, 3)])])) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
delete_from_all_test() -> |
|
|
|
|
|
?assertEqual( |
|
|
|
|
|
glc:all([glc:eq(b,2)]), |
|
|
|
|
|
deleteall( |
|
|
|
|
|
glc:all([glc:eq(a, 1),glc:eq(b,2)]), [glc:eq(a, 1)]) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
delete_from_any_test() -> |
|
|
|
|
|
?assertEqual( |
|
|
|
|
|
glc:any([glc:eq(b,2)]), |
|
|
|
|
|
deleteall( |
|
|
|
|
|
glc:any([glc:eq(a, 1),glc:eq(b,2)]), [glc:eq(a, 1)]) |
|
|
|
|
|
). |
|
|
|
|
|
|
|
|
|
|
|
-endif. |