nickva commented on code in PR #4662:
URL: https://github.com/apache/couchdb/pull/4662#discussion_r1305054596
##########
src/mango/src/mango_idx.erl:
##########
@@ -479,7 +500,186 @@ get_legacy_selector(Def) ->
end.
-ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+
+get_usable_indexes_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(fabric_util),
+ meck:new(ddoc_cache),
+ meck:new(mango_idx_special),
+ meck:new(mango_idx_view),
+ meck:new(mango_sort),
+ meck:new(mango_cursor)
+ end,
+ fun(_) ->
+ meck:unload(mango_cursor),
+ meck:unload(mango_sort),
+ meck:unload(mango_idx_view),
+ meck:unload(mango_idx_special),
+ meck:unload(ddoc_cache),
+ meck:unload(fabric_util)
Review Comment:
Let's just do `meck:unload()` so we don't have keep track of all the modules.
##########
src/mango/src/mango_cursor.erl:
##########
@@ -263,3 +547,664 @@ ddoc_name(<<"_design/", Name/binary>>) ->
Name;
ddoc_name(Name) ->
Name.
+
+-ifdef(TEST).
+-include_lib("couch/include/couch_eunit.hrl").
+
+create_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(mango_selector),
+ meck:new(mango_idx, [passthrough]),
+ meck:new(mango_cursor_view)
+ end,
+ fun(_) ->
+ meck:unload(mango_cursor_view),
+ meck:unload(mango_idx),
+ meck:unload(mango_selector)
Review Comment:
Let's use `meck:unload()` to save a few lines and stay consistent with most
of other eunit tests.
##########
src/mango/src/mango.hrl:
##########
@@ -41,3 +47,31 @@
-type row_property_key() :: id | key | value | doc.
-type row_properties() :: [{row_property_key(), any()}].
+
+-type reason() :: needs_text_search
+ | field_mismatch
+ | sort_order_mismatch
+ | empty_selector
+ | less_overlap
+ | too_many_fields
+ | alphabetically_comes_after
+ | is_partial
+ | scope_mismatch
+ | excluded_by_user
+ | unfavored_type.
+
+-type rejection_details() :: #{ reason => [reason()] }.
+
+-type trace() ::
+ #{
+ all_indexes := sets:set(#idx{}),
+ global_indexes := sets:set(#idx{}),
+ partition_indexes := sets:set(#idx{}),
+ usable_indexes := sets:set(#idx{}),
+ filtered_indexes := sets:set(#idx{}),
+ indexes_of_type := sets:set(#idx{}),
+ usability_map := [{#idx{}, {boolean(), rejection_details()}}],
+ sorted_index_ranges => [{#idx{}, [range()], integer()}]
Review Comment:
`:=` here means required and `=>` optional? If so, that's a neat thing, I
had no idea it can do that.
##########
src/mango/src/mango_cursor.erl:
##########
@@ -44,27 +44,286 @@
-define(SUPERVISOR, mango_cursor_sup).
-create(Db, Selector0, Opts) ->
+-spec create(Db, Selector, Options, Kind) -> {ok, #cursor{}} when
+ Db :: database(),
+ Selector :: selector(),
+ Options :: cursor_options(),
+ Kind :: cursor_kind().
+create(Db, Selector0, Opts, Kind) ->
Selector = mango_selector:normalize(Selector0),
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case mango_cursor:maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
+ {UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts,
Kind),
+ case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
[] ->
% use_index doesn't match a valid index - fall back to a valid one
- create_cursor(Db, UsableIndexes, Selector, Opts);
+ create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
UserSpecifiedIndex ->
- create_cursor(Db, UserSpecifiedIndex, Selector, Opts)
+ create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
end.
+-spec enhance_candidates(Candidates, [{#idx{}, properties()}]) -> Candidates
when
+ Candidates :: #{#idx{} => properties()}.
+enhance_candidates(Entries, Inputs) ->
+ Combiner =
+ fun(Key, Value1, Value2) ->
+ case Key of
+ usable -> Value1 andalso Value2;
+ reason -> lists:append(Value1, Value2);
+ ranking -> Value1 + Value2
+ end
+ end,
+ lists:foldr(
+ fun({Index, Delta}, Map) ->
+ Updater = fun(Value) -> maps:merge_with(Combiner, Value, Delta)
end,
+ maps:update_with(Index, Updater, Map)
+ end,
+ Entries,
+ Inputs
+ ).
+
+-type ranking() :: pos_integer().
+-type properties() ::
+ #{
+ usable := boolean(),
+ ranking := ranking(),
+ reason := [reason()]
+ }.
+
+-spec tag_elems(properties(), sets:set(#idx{})) -> [{#idx{}, properties()}].
+tag_elems(Properties, Set) ->
+ sets:fold(fun(Index, Acc) -> [{Index, Properties} | Acc] end, [], Set).
+
+-type reason_description() ::
+ {name, reason()}.
+-type analysis_attribute() ::
+ {usable, boolean()}
+ | {reasons, [reason_description()]}
+ | {ranking, ranking()}
+ | {covering, boolean()}.
+-type analysis() ::
+ {[analysis_attribute()]}.
+-type candidate_index_attribute() ::
+ {index, #idx{}} | {analysis, analysis()}.
+-type candidate_index() ::
+ {[candidate_index_attribute()]}.
+
+-spec extract_candidate_indexes(#cursor{}) -> [candidate_index()].
+extract_candidate_indexes(Cursor) ->
+ #cursor{trace = Trace, index = Winner, fields = Fields} = Cursor,
+ #{
+ all_indexes := AllIndexes,
+ global_indexes := GlobalIndexes,
+ partition_indexes := PartitionIndexes,
+ usable_indexes := UsableIndexes,
+ usability_map := UsabilityMap,
+ filtered_indexes := FilteredIndexes,
+ indexes_of_type := IndexesOfType
+ } = Trace,
+ % specific to view indexes
+ SortedIndexRanges = maps:get(sorted_index_ranges, Trace, []),
+ % simple difference calculations to determine the results in each stage,
+ % without looking at the implementation
+ PartialIndexes = sets:subtract(AllIndexes, GlobalIndexes),
+ OutOfScopeIndexes = sets:subtract(GlobalIndexes, PartitionIndexes),
+ NotUsableIndexes = sets:subtract(PartitionIndexes, UsableIndexes),
+ ExcludedIndexes = sets:subtract(UsableIndexes, FilteredIndexes),
+ UnfavoredIndexes = sets:subtract(FilteredIndexes, IndexesOfType),
+ % determine rankings
+ UnfavoredIndexesRank = max(1, length(SortedIndexRanges)),
+ {_, [PartialIndexesRank, OutOfScopeIndexesRank, NotUsableIndexesRank,
ExcludedIndexesRank]} =
+ lists:foldl(
+ fun(Indexes, {Rank0, Acc}) ->
+ Rank =
+ case (sets:size(Indexes) > 0) of
Review Comment:
Style nit: since we're testing for sets not being empty a `not
sets:is_empty(Indexes)` might look a bit better here
##########
src/mango/src/mango_idx_special.erl:
##########
@@ -58,10 +58,16 @@ columns(#idx{def = all_docs}) ->
[<<"_id">>].
is_usable(#idx{def = all_docs}, _Selector, []) ->
- true;
+ {true, #{reason => []}};
is_usable(#idx{def = all_docs} = Idx, Selector, SortFields) ->
Fields = mango_idx_view:indexable_fields(Selector),
- lists:member(<<"_id">>, Fields) and can_use_sort(Idx, SortFields,
Selector).
+ SelectorHasRequiredFields = lists:member(<<"_id">>, Fields),
+ CanUseSort = can_use_sort(Idx, SortFields, Selector),
+ Reason =
+ [field_mismatch || not SelectorHasRequiredFields] ++
+ [sort_order_mismatch || not CanUseSort],
Review Comment:
This is bit of an odd pattern but it works well here and it's kind of neat!
##########
src/mango/src/mango_cursor.erl:
##########
@@ -44,27 +44,286 @@
-define(SUPERVISOR, mango_cursor_sup).
-create(Db, Selector0, Opts) ->
+-spec create(Db, Selector, Options, Kind) -> {ok, #cursor{}} when
+ Db :: database(),
+ Selector :: selector(),
+ Options :: cursor_options(),
+ Kind :: cursor_kind().
+create(Db, Selector0, Opts, Kind) ->
Selector = mango_selector:normalize(Selector0),
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case mango_cursor:maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
+ {UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts,
Kind),
+ case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
[] ->
% use_index doesn't match a valid index - fall back to a valid one
- create_cursor(Db, UsableIndexes, Selector, Opts);
+ create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
UserSpecifiedIndex ->
- create_cursor(Db, UserSpecifiedIndex, Selector, Opts)
+ create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
end.
+-spec enhance_candidates(Candidates, [{#idx{}, properties()}]) -> Candidates
when
+ Candidates :: #{#idx{} => properties()}.
+enhance_candidates(Entries, Inputs) ->
+ Combiner =
+ fun(Key, Value1, Value2) ->
+ case Key of
+ usable -> Value1 andalso Value2;
+ reason -> lists:append(Value1, Value2);
+ ranking -> Value1 + Value2
+ end
+ end,
+ lists:foldr(
+ fun({Index, Delta}, Map) ->
+ Updater = fun(Value) -> maps:merge_with(Combiner, Value, Delta)
end,
+ maps:update_with(Index, Updater, Map)
+ end,
+ Entries,
+ Inputs
+ ).
+
+-type ranking() :: pos_integer().
+-type properties() ::
+ #{
+ usable := boolean(),
+ ranking := ranking(),
+ reason := [reason()]
+ }.
+
+-spec tag_elems(properties(), sets:set(#idx{})) -> [{#idx{}, properties()}].
+tag_elems(Properties, Set) ->
+ sets:fold(fun(Index, Acc) -> [{Index, Properties} | Acc] end, [], Set).
+
+-type reason_description() ::
+ {name, reason()}.
+-type analysis_attribute() ::
+ {usable, boolean()}
+ | {reasons, [reason_description()]}
+ | {ranking, ranking()}
+ | {covering, boolean()}.
+-type analysis() ::
+ {[analysis_attribute()]}.
+-type candidate_index_attribute() ::
+ {index, #idx{}} | {analysis, analysis()}.
+-type candidate_index() ::
+ {[candidate_index_attribute()]}.
+
+-spec extract_candidate_indexes(#cursor{}) -> [candidate_index()].
+extract_candidate_indexes(Cursor) ->
+ #cursor{trace = Trace, index = Winner, fields = Fields} = Cursor,
+ #{
+ all_indexes := AllIndexes,
+ global_indexes := GlobalIndexes,
+ partition_indexes := PartitionIndexes,
+ usable_indexes := UsableIndexes,
+ usability_map := UsabilityMap,
+ filtered_indexes := FilteredIndexes,
+ indexes_of_type := IndexesOfType
+ } = Trace,
+ % specific to view indexes
+ SortedIndexRanges = maps:get(sorted_index_ranges, Trace, []),
+ % simple difference calculations to determine the results in each stage,
+ % without looking at the implementation
+ PartialIndexes = sets:subtract(AllIndexes, GlobalIndexes),
+ OutOfScopeIndexes = sets:subtract(GlobalIndexes, PartitionIndexes),
+ NotUsableIndexes = sets:subtract(PartitionIndexes, UsableIndexes),
+ ExcludedIndexes = sets:subtract(UsableIndexes, FilteredIndexes),
+ UnfavoredIndexes = sets:subtract(FilteredIndexes, IndexesOfType),
+ % determine rankings
+ UnfavoredIndexesRank = max(1, length(SortedIndexRanges)),
+ {_, [PartialIndexesRank, OutOfScopeIndexesRank, NotUsableIndexesRank,
ExcludedIndexesRank]} =
+ lists:foldl(
+ fun(Indexes, {Rank0, Acc}) ->
+ Rank =
+ case (sets:size(Indexes) > 0) of
+ true -> Rank0 + 1;
+ false -> Rank0
+ end,
+ {Rank, [Rank | Acc]}
+ end,
+ {UnfavoredIndexesRank, []},
+ [UnfavoredIndexes, ExcludedIndexes, NotUsableIndexes,
OutOfScopeIndexes]
+ ),
+ % start building the list of candidates
+ AddCandidate =
+ fun(Index, Map) ->
+ maps:put(Index, #{}, Map)
+ end,
+ Candidates0 = sets:fold(AddCandidate, #{}, AllIndexes),
+ PartialIndexesTags = tag_elems(
+ #{usable => false, reason => [is_partial], ranking =>
PartialIndexesRank},
+ PartialIndexes
+ ),
+ Candidates1 = enhance_candidates(Candidates0, PartialIndexesTags),
+ OutOfScopeIndexesTags = tag_elems(
+ #{usable => false, reason => [scope_mismatch], ranking =>
OutOfScopeIndexesRank},
+ OutOfScopeIndexes
+ ),
+ Candidates2 = enhance_candidates(Candidates1, OutOfScopeIndexesTags),
+ NotUsableIndexesTags = tag_elems(
+ % the reason is going to be filled out by the mango_idx modules
+ #{usable => false, ranking => NotUsableIndexesRank},
+ NotUsableIndexes
+ ),
+ Candidates3 = enhance_candidates(Candidates2, NotUsableIndexesTags),
+ ExcludedIndexesTags = tag_elems(
+ #{usable => true, reason => [excluded_by_user], ranking =>
ExcludedIndexesRank},
+ ExcludedIndexes
+ ),
+ Candidates4 = enhance_candidates(Candidates3, ExcludedIndexesTags),
+ UnfavoredIndexesTags = tag_elems(
+ #{usable => true, reason => [unfavored_type], ranking =>
UnfavoredIndexesRank},
+ UnfavoredIndexes
+ ),
+ Candidates5 = enhance_candidates(Candidates4, UnfavoredIndexesTags),
+ NotChosenIndexesTags = tag_elems(
+ #{usable => true},
+ IndexesOfType
+ ),
+ Candidates6 = enhance_candidates(Candidates5, NotChosenIndexesTags),
+ NotUsableDetails =
+ lists:flatmap(
+ fun({Index, {Usable, Details}}) ->
+ case Usable of
+ true -> [];
+ false -> [{Index, Details}]
+ end
+ end,
+ UsabilityMap
+ ),
+ Candidates7 = enhance_candidates(Candidates6, NotUsableDetails),
+ WinnerColumnWidth =
+ case Winner of
+ none ->
+ 0;
+ W ->
+ case mango_idx:columns(W) of
+ X when is_list(X) -> length(X);
+ all_fields -> 256
+ end
+ end,
+ {_, NotChosenDetails} =
+ lists:foldl(
+ fun({Index, _Prefix, Distance}, {N, Acc}) ->
+ ColumnWidth = length(mango_idx:columns(Index)),
+ Reason =
+ case {} of
+ _ when Distance > 0 -> [less_overlap];
+ _ when ColumnWidth > WinnerColumnWidth ->
[too_many_fields];
+ _ -> [alphabetically_comes_after]
+ end,
+ Tags = #{
+ reason => Reason,
+ ranking => N
+ },
+ {N + 1, [{Index, Tags} | Acc]}
+ end,
+ {0, []},
+ SortedIndexRanges
+ ),
+ Candidates8 = enhance_candidates(Candidates7, NotChosenDetails),
+ Candidates = maps:remove(Winner, Candidates8),
+ % produce the final list
+ ToList =
+ fun(Index, Tags, Acc) ->
+ #idx{type = IndexType} = Index,
+ #{usable := Usable, reason := Reason, ranking := Ranking} = Tags,
+ Covering =
+ case IndexType of
+ <<"json">> -> mango_idx_view:covers(Index, Fields);
+ _ -> null
+ end,
+ Reasons = [{[{name, hd(Reason)}]}],
+ Analysis =
+ {[
+ {usable, Usable},
+ {reasons, Reasons},
+ {ranking, Ranking},
+ {covering, Covering}
+ ]},
+ Entry =
+ {[
+ {index, mango_idx:to_json(Index)},
+ {analysis, Analysis}
+ ]},
+ [Entry | Acc]
+ end,
+ lists:reverse(maps:fold(ToList, [], Candidates)).
+
+-type selector_hint() :: {indexable_fields, [field()]} | {unindexable_fields,
[field()]}.
+-type selector_hints() :: {[selector_hint()]}.
+
+-spec extract_selector_hints(selector()) -> selector_hints().
+extract_selector_hints(Selector) ->
+ DreyfusAvailable = dreyfus:available(),
+ NouveauEnabled = nouveau:enabled(),
+ AsIndex =
+ fun(Module) ->
+ case Module of
+ mango_cursor_view -> [{mango_idx_view, json}];
+ mango_cursor_text when DreyfusAvailable -> [{mango_idx_text,
text}];
+ mango_cursor_nouveau when NouveauEnabled ->
[{mango_idx_nouveau, nouveau}];
+ _ -> []
+ end
+ end,
+ Modules = lists:flatmap(AsIndex, ?CURSOR_MODULES),
+ Populate =
+ fun({Module, IndexType}) ->
+ AllFields = sets:from_list(mango_selector:fields(Selector)),
+ Normalize = fun(N) -> hd(string:split(N, ":")) end,
+ IndexableFields = sets:from_list(
+ lists:map(Normalize, Module:indexable_fields(Selector))
+ ),
+ UnindexableFields = sets:subtract(AllFields, IndexableFields),
+ {[
+ {type, IndexType},
+ {indexable_fields, sets:to_list(IndexableFields)},
+ {unindexable_fields, sets:to_list(UnindexableFields)}
+ ]}
+ end,
+ lists:map(Populate, Modules).
+
+-type explain_attribute() ::
+ {dbname, database()}
+ | {index, ejson()}
+ | {partitioned, boolean()}
+ | {selector, ejson()}
+ | {opts, ejson()}
+ | {limit, integer()}
+ | {skip, integer()}
+ | {fields, fields()}
+ | {index_candidates, [candidate_index()]}
+ | {selector_hints, selector_hints()}
+ | {atom(), any()}.
+-type explain_response() :: {[explain_attribute()]}.
+
+-spec explain(#cursor{}) -> explain_response().
explain(#cursor{} = Cursor) ->
#cursor{
- index = Idx,
+ db = Db,
+ index = Index,
selector = Selector,
opts = Opts0,
limit = Limit,
skip = Skip,
fields = Fields
} = Cursor,
- Mod = mango_idx:cursor_mod(Idx),
+ {ModExplain, DbName, JSON, Partitioned} =
+ case Index of
+ none ->
+ {
+ [],
+ couch_db:name(Db),
+ null,
+ null
Review Comment:
What are the possible `mango_idx:partioned(Idx)` return values? Would `null`
be a new return type for it?
I looked in `mango_idx(Idx)` but go confused as I found a mix of
`false/true/undefined/db_default` values.
##########
src/mango/src/mango_cursor.hrl:
##########
@@ -17,6 +17,7 @@
db,
index,
ranges,
+ trace = undefined,
Review Comment:
`undefined` is the default anyway I believe?
##########
src/mango/src/mango_cursor.erl:
##########
@@ -44,27 +44,286 @@
-define(SUPERVISOR, mango_cursor_sup).
-create(Db, Selector0, Opts) ->
+-spec create(Db, Selector, Options, Kind) -> {ok, #cursor{}} when
+ Db :: database(),
+ Selector :: selector(),
+ Options :: cursor_options(),
+ Kind :: cursor_kind().
+create(Db, Selector0, Opts, Kind) ->
Selector = mango_selector:normalize(Selector0),
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case mango_cursor:maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
+ {UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts,
Kind),
+ case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
[] ->
% use_index doesn't match a valid index - fall back to a valid one
- create_cursor(Db, UsableIndexes, Selector, Opts);
+ create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
UserSpecifiedIndex ->
- create_cursor(Db, UserSpecifiedIndex, Selector, Opts)
+ create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
end.
+-spec enhance_candidates(Candidates, [{#idx{}, properties()}]) -> Candidates
when
+ Candidates :: #{#idx{} => properties()}.
+enhance_candidates(Entries, Inputs) ->
+ Combiner =
+ fun(Key, Value1, Value2) ->
+ case Key of
+ usable -> Value1 andalso Value2;
+ reason -> lists:append(Value1, Value2);
+ ranking -> Value1 + Value2
+ end
+ end,
+ lists:foldr(
+ fun({Index, Delta}, Map) ->
+ Updater = fun(Value) -> maps:merge_with(Combiner, Value, Delta)
end,
+ maps:update_with(Index, Updater, Map)
+ end,
+ Entries,
+ Inputs
+ ).
+
+-type ranking() :: pos_integer().
+-type properties() ::
+ #{
+ usable := boolean(),
+ ranking := ranking(),
+ reason := [reason()]
+ }.
+
+-spec tag_elems(properties(), sets:set(#idx{})) -> [{#idx{}, properties()}].
+tag_elems(Properties, Set) ->
+ sets:fold(fun(Index, Acc) -> [{Index, Properties} | Acc] end, [], Set).
+
+-type reason_description() ::
+ {name, reason()}.
+-type analysis_attribute() ::
+ {usable, boolean()}
+ | {reasons, [reason_description()]}
+ | {ranking, ranking()}
+ | {covering, boolean()}.
+-type analysis() ::
+ {[analysis_attribute()]}.
+-type candidate_index_attribute() ::
+ {index, #idx{}} | {analysis, analysis()}.
+-type candidate_index() ::
+ {[candidate_index_attribute()]}.
+
+-spec extract_candidate_indexes(#cursor{}) -> [candidate_index()].
+extract_candidate_indexes(Cursor) ->
+ #cursor{trace = Trace, index = Winner, fields = Fields} = Cursor,
+ #{
+ all_indexes := AllIndexes,
+ global_indexes := GlobalIndexes,
+ partition_indexes := PartitionIndexes,
+ usable_indexes := UsableIndexes,
+ usability_map := UsabilityMap,
+ filtered_indexes := FilteredIndexes,
+ indexes_of_type := IndexesOfType
+ } = Trace,
+ % specific to view indexes
+ SortedIndexRanges = maps:get(sorted_index_ranges, Trace, []),
+ % simple difference calculations to determine the results in each stage,
+ % without looking at the implementation
+ PartialIndexes = sets:subtract(AllIndexes, GlobalIndexes),
+ OutOfScopeIndexes = sets:subtract(GlobalIndexes, PartitionIndexes),
+ NotUsableIndexes = sets:subtract(PartitionIndexes, UsableIndexes),
+ ExcludedIndexes = sets:subtract(UsableIndexes, FilteredIndexes),
+ UnfavoredIndexes = sets:subtract(FilteredIndexes, IndexesOfType),
+ % determine rankings
+ UnfavoredIndexesRank = max(1, length(SortedIndexRanges)),
+ {_, [PartialIndexesRank, OutOfScopeIndexesRank, NotUsableIndexesRank,
ExcludedIndexesRank]} =
+ lists:foldl(
+ fun(Indexes, {Rank0, Acc}) ->
+ Rank =
+ case (sets:size(Indexes) > 0) of
+ true -> Rank0 + 1;
+ false -> Rank0
+ end,
+ {Rank, [Rank | Acc]}
+ end,
+ {UnfavoredIndexesRank, []},
+ [UnfavoredIndexes, ExcludedIndexes, NotUsableIndexes,
OutOfScopeIndexes]
+ ),
+ % start building the list of candidates
+ AddCandidate =
+ fun(Index, Map) ->
+ maps:put(Index, #{}, Map)
+ end,
+ Candidates0 = sets:fold(AddCandidate, #{}, AllIndexes),
+ PartialIndexesTags = tag_elems(
+ #{usable => false, reason => [is_partial], ranking =>
PartialIndexesRank},
+ PartialIndexes
+ ),
+ Candidates1 = enhance_candidates(Candidates0, PartialIndexesTags),
+ OutOfScopeIndexesTags = tag_elems(
+ #{usable => false, reason => [scope_mismatch], ranking =>
OutOfScopeIndexesRank},
+ OutOfScopeIndexes
+ ),
+ Candidates2 = enhance_candidates(Candidates1, OutOfScopeIndexesTags),
+ NotUsableIndexesTags = tag_elems(
+ % the reason is going to be filled out by the mango_idx modules
+ #{usable => false, ranking => NotUsableIndexesRank},
+ NotUsableIndexes
+ ),
+ Candidates3 = enhance_candidates(Candidates2, NotUsableIndexesTags),
+ ExcludedIndexesTags = tag_elems(
+ #{usable => true, reason => [excluded_by_user], ranking =>
ExcludedIndexesRank},
+ ExcludedIndexes
+ ),
+ Candidates4 = enhance_candidates(Candidates3, ExcludedIndexesTags),
+ UnfavoredIndexesTags = tag_elems(
+ #{usable => true, reason => [unfavored_type], ranking =>
UnfavoredIndexesRank},
+ UnfavoredIndexes
+ ),
+ Candidates5 = enhance_candidates(Candidates4, UnfavoredIndexesTags),
+ NotChosenIndexesTags = tag_elems(
+ #{usable => true},
+ IndexesOfType
+ ),
+ Candidates6 = enhance_candidates(Candidates5, NotChosenIndexesTags),
+ NotUsableDetails =
+ lists:flatmap(
+ fun({Index, {Usable, Details}}) ->
+ case Usable of
+ true -> [];
+ false -> [{Index, Details}]
+ end
+ end,
+ UsabilityMap
+ ),
+ Candidates7 = enhance_candidates(Candidates6, NotUsableDetails),
+ WinnerColumnWidth =
+ case Winner of
+ none ->
+ 0;
+ W ->
+ case mango_idx:columns(W) of
+ X when is_list(X) -> length(X);
+ all_fields -> 256
+ end
+ end,
+ {_, NotChosenDetails} =
+ lists:foldl(
+ fun({Index, _Prefix, Distance}, {N, Acc}) ->
+ ColumnWidth = length(mango_idx:columns(Index)),
+ Reason =
+ case {} of
+ _ when Distance > 0 -> [less_overlap];
+ _ when ColumnWidth > WinnerColumnWidth ->
[too_many_fields];
+ _ -> [alphabetically_comes_after]
+ end,
+ Tags = #{
+ reason => Reason,
+ ranking => N
+ },
+ {N + 1, [{Index, Tags} | Acc]}
+ end,
+ {0, []},
+ SortedIndexRanges
+ ),
+ Candidates8 = enhance_candidates(Candidates7, NotChosenDetails),
+ Candidates = maps:remove(Winner, Candidates8),
+ % produce the final list
+ ToList =
+ fun(Index, Tags, Acc) ->
+ #idx{type = IndexType} = Index,
+ #{usable := Usable, reason := Reason, ranking := Ranking} = Tags,
+ Covering =
+ case IndexType of
+ <<"json">> -> mango_idx_view:covers(Index, Fields);
+ _ -> null
+ end,
+ Reasons = [{[{name, hd(Reason)}]}],
+ Analysis =
+ {[
+ {usable, Usable},
+ {reasons, Reasons},
+ {ranking, Ranking},
+ {covering, Covering}
+ ]},
+ Entry =
+ {[
+ {index, mango_idx:to_json(Index)},
+ {analysis, Analysis}
+ ]},
+ [Entry | Acc]
+ end,
+ lists:reverse(maps:fold(ToList, [], Candidates)).
Review Comment:
Wondering, what is the reason for using `lists:reverse/1`? maps will
traverse the elements in some unspecified order, maybe `lists:sort` would work
better here?
(It's true that in most cases it turns out maps will sort its keys,
especially for small map sizes when it just uses a list of KVs, but we
shouldn't rely on it).
##########
src/mango/src/mango_cursor.erl:
##########
@@ -44,27 +44,286 @@
-define(SUPERVISOR, mango_cursor_sup).
-create(Db, Selector0, Opts) ->
+-spec create(Db, Selector, Options, Kind) -> {ok, #cursor{}} when
+ Db :: database(),
+ Selector :: selector(),
+ Options :: cursor_options(),
+ Kind :: cursor_kind().
+create(Db, Selector0, Opts, Kind) ->
Selector = mango_selector:normalize(Selector0),
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case mango_cursor:maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
+ {UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts,
Kind),
+ case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
[] ->
% use_index doesn't match a valid index - fall back to a valid one
- create_cursor(Db, UsableIndexes, Selector, Opts);
+ create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
UserSpecifiedIndex ->
- create_cursor(Db, UserSpecifiedIndex, Selector, Opts)
+ create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
end.
+-spec enhance_candidates(Candidates, [{#idx{}, properties()}]) -> Candidates
when
+ Candidates :: #{#idx{} => properties()}.
+enhance_candidates(Entries, Inputs) ->
+ Combiner =
+ fun(Key, Value1, Value2) ->
+ case Key of
+ usable -> Value1 andalso Value2;
+ reason -> lists:append(Value1, Value2);
+ ranking -> Value1 + Value2
+ end
+ end,
+ lists:foldr(
+ fun({Index, Delta}, Map) ->
+ Updater = fun(Value) -> maps:merge_with(Combiner, Value, Delta)
end,
+ maps:update_with(Index, Updater, Map)
+ end,
+ Entries,
+ Inputs
+ ).
+
+-type ranking() :: pos_integer().
+-type properties() ::
+ #{
+ usable := boolean(),
+ ranking := ranking(),
+ reason := [reason()]
+ }.
+
+-spec tag_elems(properties(), sets:set(#idx{})) -> [{#idx{}, properties()}].
+tag_elems(Properties, Set) ->
+ sets:fold(fun(Index, Acc) -> [{Index, Properties} | Acc] end, [], Set).
+
+-type reason_description() ::
+ {name, reason()}.
+-type analysis_attribute() ::
+ {usable, boolean()}
+ | {reasons, [reason_description()]}
+ | {ranking, ranking()}
+ | {covering, boolean()}.
+-type analysis() ::
+ {[analysis_attribute()]}.
+-type candidate_index_attribute() ::
+ {index, #idx{}} | {analysis, analysis()}.
+-type candidate_index() ::
+ {[candidate_index_attribute()]}.
+
+-spec extract_candidate_indexes(#cursor{}) -> [candidate_index()].
+extract_candidate_indexes(Cursor) ->
+ #cursor{trace = Trace, index = Winner, fields = Fields} = Cursor,
+ #{
+ all_indexes := AllIndexes,
+ global_indexes := GlobalIndexes,
+ partition_indexes := PartitionIndexes,
+ usable_indexes := UsableIndexes,
+ usability_map := UsabilityMap,
+ filtered_indexes := FilteredIndexes,
+ indexes_of_type := IndexesOfType
+ } = Trace,
+ % specific to view indexes
+ SortedIndexRanges = maps:get(sorted_index_ranges, Trace, []),
+ % simple difference calculations to determine the results in each stage,
+ % without looking at the implementation
+ PartialIndexes = sets:subtract(AllIndexes, GlobalIndexes),
+ OutOfScopeIndexes = sets:subtract(GlobalIndexes, PartitionIndexes),
+ NotUsableIndexes = sets:subtract(PartitionIndexes, UsableIndexes),
+ ExcludedIndexes = sets:subtract(UsableIndexes, FilteredIndexes),
+ UnfavoredIndexes = sets:subtract(FilteredIndexes, IndexesOfType),
+ % determine rankings
+ UnfavoredIndexesRank = max(1, length(SortedIndexRanges)),
+ {_, [PartialIndexesRank, OutOfScopeIndexesRank, NotUsableIndexesRank,
ExcludedIndexesRank]} =
+ lists:foldl(
+ fun(Indexes, {Rank0, Acc}) ->
+ Rank =
+ case (sets:size(Indexes) > 0) of
+ true -> Rank0 + 1;
+ false -> Rank0
+ end,
+ {Rank, [Rank | Acc]}
+ end,
+ {UnfavoredIndexesRank, []},
+ [UnfavoredIndexes, ExcludedIndexes, NotUsableIndexes,
OutOfScopeIndexes]
+ ),
+ % start building the list of candidates
+ AddCandidate =
+ fun(Index, Map) ->
+ maps:put(Index, #{}, Map)
Review Comment:
Minor style preference: use map syntax directly when possible. `Map#{Index
=> #{})`
##########
src/mango/src/mango.hrl:
##########
@@ -41,3 +47,31 @@
-type row_property_key() :: id | key | value | doc.
-type row_properties() :: [{row_property_key(), any()}].
+
+-type reason() :: needs_text_search
+ | field_mismatch
+ | sort_order_mismatch
+ | empty_selector
+ | less_overlap
+ | too_many_fields
+ | alphabetically_comes_after
+ | is_partial
+ | scope_mismatch
+ | excluded_by_user
+ | unfavored_type.
+
+-type rejection_details() :: #{ reason => [reason()] }.
Review Comment:
Minor style nit: I think in most places we use the `#{reason => [reason()]}`
style. I wonder why erlfmt didn't complain, maybe because it's in a type...
##########
src/mango/src/mango_cursor.erl:
##########
@@ -263,3 +547,664 @@ ddoc_name(<<"_design/", Name/binary>>) ->
Name;
ddoc_name(Name) ->
Name.
+
+-ifdef(TEST).
+-include_lib("couch/include/couch_eunit.hrl").
+
+create_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(mango_selector),
+ meck:new(mango_idx, [passthrough]),
+ meck:new(mango_cursor_view)
+ end,
+ fun(_) ->
+ meck:unload(mango_cursor_view),
+ meck:unload(mango_idx),
+ meck:unload(mango_selector)
+ end,
+ [
+ ?TDEF_FE(t_create_regular),
+ ?TDEF_FE(t_create_user_specified_index),
+ ?TDEF_FE(t_create_invalid_user_specified_index)
+ ]
+ }.
+
+t_create_regular(_) ->
+ IndexSpecial = #idx{type = <<"special">>, def = all_docs},
+ IndexView = #idx{type = <<"json">>},
+ UsableIndexes = [IndexView, IndexSpecial],
+ FilteredIndexes = UsableIndexes,
+ IndexesOfType = [IndexView],
+ Trace1 = #{},
+ Trace2 =
+ #{
+ filtered_indexes => sets:from_list(FilteredIndexes),
+ indexes_of_type => sets:from_list(IndexesOfType)
+ },
+ Options = [{use_index, []}],
+ meck:expect(mango_selector, normalize, [selector],
meck:val(normalized_selector)),
+ meck:expect(
+ mango_idx,
+ get_usable_indexes,
+ [db, normalized_selector, Options, target],
+ meck:val({UsableIndexes, Trace1})
+ ),
+ meck:expect(
+ mango_cursor_view,
+ create,
+ [db, {IndexesOfType, Trace2}, normalized_selector, Options],
+ meck:val(view_cursor)
+ ),
+ ?assertEqual(view_cursor, create(db, selector, Options, target)).
+
+t_create_user_specified_index(_) ->
+ IndexSpecial = #idx{type = <<"special">>, def = all_docs},
+ IndexView1 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx1">>},
+ IndexView2 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx2">>},
+ IndexView3 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx3">>},
+ UsableIndexes = [IndexSpecial, IndexView1, IndexView2, IndexView3],
+ FilteredIndexes = [IndexView2],
+ IndexesOfType = FilteredIndexes,
+ Trace1 = #{},
+ Trace2 =
+ #{
+ filtered_indexes => sets:from_list(FilteredIndexes),
+ indexes_of_type => sets:from_list(IndexesOfType)
+ },
+ Options = [{use_index, [<<"_design/view_idx2">>]}],
+ meck:expect(mango_selector, normalize, [selector],
meck:val(normalized_selector)),
+ meck:expect(
+ mango_idx,
+ get_usable_indexes,
+ [db, normalized_selector, Options, target],
+ meck:val({UsableIndexes, Trace1})
+ ),
+ meck:expect(
+ mango_cursor_view,
+ create,
+ [db, {IndexesOfType, Trace2}, normalized_selector, Options],
+ meck:val(view_cursor)
+ ),
+ ?assertEqual(view_cursor, create(db, selector, Options, target)).
+
+t_create_invalid_user_specified_index(_) ->
+ IndexSpecial = #idx{type = <<"special">>, def = all_docs},
+ IndexView1 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx1">>},
+ IndexView2 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx2">>},
+ IndexView3 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx3">>},
+ UsableIndexes = [IndexSpecial, IndexView1, IndexView2, IndexView3],
+ IndexesOfType = [IndexView1, IndexView2, IndexView3],
+ Trace1 = #{},
+ Trace2 =
+ #{
+ filtered_indexes => sets:from_list(UsableIndexes),
+ indexes_of_type => sets:from_list(IndexesOfType)
+ },
+ Options = [{use_index, [<<"foobar">>]}],
+ meck:expect(mango_selector, normalize, [selector],
meck:val(normalized_selector)),
+ meck:expect(
+ mango_idx,
+ get_usable_indexes,
+ [db, normalized_selector, Options, target],
+ meck:val({UsableIndexes, Trace1})
+ ),
+ meck:expect(
+ mango_cursor_view,
+ create,
+ [db, {IndexesOfType, Trace2}, normalized_selector, Options],
+ meck:val(view_cursor)
+ ),
+ ?assertEqual(view_cursor, create(db, selector, Options, target)).
+
+enhance_candidates_test() ->
+ Candidates1 = #{index => #{reason => [], usable => true}},
+ Candidates2 = #{index => #{reason => [reason1], usable => true}},
+ Candidates3 = #{index => #{reason => [reason1, reason2], usable => false}},
+ Deltas1 = [{index, #{reason => [reason1], usable => true}}],
+ Deltas2 = [{index, #{reason => [reason2], usable => false}}],
+ ?assertEqual(Candidates2, enhance_candidates(Candidates1, Deltas1)),
+ ?assertEqual(Candidates3, enhance_candidates(Candidates2, Deltas2)).
+
+extract_candidate_indexes_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(mango_idx, [passthrough]),
+ meck:new(mango_idx_view, [passthrough])
+ end,
+ fun(_) ->
+ meck:unload(mango_idx_view),
+ meck:unload(mango_idx)
+ end,
+ [
+ ?TDEF_FE(t_extract_candidate_indexes_empty),
+ ?TDEF_FE(t_extract_candidate_indexes_singleton),
+ ?TDEF_FE(t_extract_candidate_indexes_user_specified),
+ ?TDEF_FE(t_extract_candidate_indexes_regular)
+ ]
+ }.
+
+t_extract_candidate_indexes_empty(_) ->
+ Indexes = sets:new(),
+ UsabilityMap = [],
+ Trace =
+ #{
+ all_indexes => Indexes,
+ global_indexes => Indexes,
+ partition_indexes => Indexes,
+ usable_indexes => Indexes,
+ usability_map => UsabilityMap,
+ filtered_indexes => Indexes,
+ indexes_of_type => Indexes
+ },
+ Cursor =
+ #cursor{
+ index = none,
+ trace = Trace
+ },
+ Candidates = [],
+ ?assertNot(meck:called(mango_idx, columns, '_')),
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+t_extract_candidate_indexes_singleton(_) ->
+ Indexes = sets:from_list([winner]),
+ UsabilityMap = [{winner, {true, #{reason => []}}}],
+ Trace =
+ #{
+ all_indexes => Indexes,
+ global_indexes => Indexes,
+ partition_indexes => Indexes,
+ usable_indexes => Indexes,
+ usability_map => UsabilityMap,
+ filtered_indexes => Indexes,
+ indexes_of_type => Indexes
+ },
+ Cursor =
+ #cursor{
+ index = winner,
+ trace = Trace
+ },
+ Candidates = [],
+ meck:expect(mango_idx, columns, [winner], meck:val([column])),
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+t_extract_candidate_indexes_user_specified(_) ->
+ Partial = #idx{type = <<"json">>, name = partial},
+ Partitioned = #idx{type = <<"json">>, name = partitioned},
+ NotUsable = #idx{type = <<"json">>, name = not_usable},
+ Filtered = #idx{type = <<"json">>, name = filtered},
+ Unfavored = #idx{type = <<"special">>, name = unfavored},
+ UsabilityMap =
+ [
+ {winner, {true, #{reason => []}}},
+ {NotUsable, {false, #{reason => [field_mismatch]}}},
+ {Filtered, {true, #{reason => []}}},
+ {Unfavored, {true, #{reason => []}}}
+ ],
+ Trace =
+ #{
+ all_indexes => sets:from_list([
+ winner, Partial, Partitioned, NotUsable, Filtered, Unfavored
+ ]),
+ global_indexes => sets:from_list([winner, Partitioned, NotUsable,
Filtered, Unfavored]),
+ partition_indexes => sets:from_list([winner, NotUsable, Filtered,
Unfavored]),
+ usable_indexes => sets:from_list([winner, Filtered, Unfavored]),
+ usability_map => UsabilityMap,
+ filtered_indexes => sets:from_list([winner, Unfavored]),
+ indexes_of_type => sets:from_list([winner])
+ },
+ Cursor =
+ #cursor{
+ index = winner,
+ trace = Trace,
+ fields = fields
+ },
+ meck:expect(mango_idx, columns, [winner], meck:val(all_fields)),
+ meck:expect(mango_idx, to_json, fun(#idx{name = Name}) -> Name end),
+ meck:expect(mango_idx_view, covers, fun(#idx{name = Name}, fields) -> Name
end),
+ Candidates =
+ [
+ {[
+ {index, filtered},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, excluded_by_user}]}]},
+ {ranking, 2},
+ {covering, filtered}
+ ]}}
+ ]},
+ {[
+ {index, not_usable},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, field_mismatch}]}]},
+ {ranking, 3},
+ {covering, not_usable}
+ ]}}
+ ]},
+ {[
+ {index, partial},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, is_partial}]}]},
+ {ranking, 5},
+ {covering, partial}
+ ]}}
+ ]},
+ {[
+ {index, partitioned},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, scope_mismatch}]}]},
+ {ranking, 4},
+ {covering, partitioned}
+ ]}}
+ ]},
+ {[
+ {index, unfavored},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, unfavored_type}]}]},
+ {ranking, 1},
+ {covering, null}
+ ]}}
+ ]}
+ ],
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+t_extract_candidate_indexes_regular(_) ->
+ Partial1 = #idx{type = <<"json">>, name = partial1},
+ Partial2 = #idx{type = <<"json">>, name = partial2},
+ Partitioned1 = #idx{type = <<"json">>, name = partitioned1},
+ Partitioned2 = #idx{type = <<"json">>, name = partitioned2},
+ NotUsable = #idx{type = <<"json">>, name = not_usable},
+ Unfavored1 = #idx{type = <<"special">>, name = unfavored1},
+ Unfavored2 = #idx{type = <<"text">>, name = unfavored2},
+ Usable1 = #idx{type = <<"json">>, name = usable1},
+ Usable2 = #idx{type = <<"json">>, name = usable2},
+ Usable3 = #idx{type = <<"json">>, name = usable3},
+ UsabilityMap =
+ [
+ {winner, {true, #{reason => []}}},
+ {NotUsable, {false, #{reason => [not_usable_reason]}}},
+ {Unfavored1, {true, #{reason => []}}},
+ {Unfavored2, {true, #{reason => []}}},
+ {Usable1, {true, #{reason => []}}},
+ {Usable2, {true, #{reason => []}}},
+ {Usable3, {true, #{reason => []}}}
+ ],
+ SortedIndexRanges = [
+ {winner, prefix0, 0}, {Usable1, prefix1, 1}, {Usable2, prefix2, 0},
{Usable3, prefix3, 0}
+ ],
+ Trace =
+ #{
+ all_indexes => sets:from_list([
+ winner,
+ Partial1,
+ Partial2,
+ Partitioned1,
+ Partitioned2,
+ NotUsable,
+ Unfavored1,
+ Unfavored2,
+ Usable1,
+ Usable2,
+ Usable3
+ ]),
+ global_indexes => sets:from_list([
+ winner,
+ Partitioned1,
+ Partitioned2,
+ NotUsable,
+ Unfavored1,
+ Unfavored2,
+ Usable1,
+ Usable2,
+ Usable3
+ ]),
+ partition_indexes => sets:from_list([
+ winner, NotUsable, Unfavored1, Unfavored2, Usable1, Usable2,
Usable3
+ ]),
+ usable_indexes => sets:from_list([
+ winner, Unfavored1, Unfavored2, Usable1, Usable2, Usable3
+ ]),
+ usability_map => UsabilityMap,
+ filtered_indexes => sets:from_list([
+ winner, Unfavored1, Unfavored2, Usable1, Usable2, Usable3
+ ]),
+ indexes_of_type => sets:from_list([winner, Usable1, Usable2,
Usable3]),
+ sorted_index_ranges => SortedIndexRanges
+ },
+ Cursor =
+ #cursor{
+ index = winner,
+ trace = Trace,
+ fields = fields
+ },
+ meck:expect(
+ mango_idx,
+ columns,
+ fun(Index) ->
+ case Index of
+ winner -> [column];
+ Usable1 -> [column1, column2];
+ Usable2 -> [column1, column2, column3];
+ Usable3 -> [column]
+ end
+ end
+ ),
+ meck:expect(mango_idx, to_json, fun(#idx{name = Name}) -> Name end),
+ meck:expect(mango_idx_view, covers, fun(#idx{name = Name}, fields) -> Name
end),
+ Candidates =
+ [
+ {[
+ {index, not_usable},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, not_usable_reason}]}]},
+ {ranking, 5},
+ {covering, not_usable}
+ ]}}
+ ]},
+ {[
+ {index, partial1},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, is_partial}]}]},
+ {ranking, 7},
+ {covering, partial1}
+ ]}}
+ ]},
+ {[
+ {index, partial2},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, is_partial}]}]},
+ {ranking, 7},
+ {covering, partial2}
+ ]}}
+ ]},
+ {[
+ {index, partitioned1},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, scope_mismatch}]}]},
+ {ranking, 6},
+ {covering, partitioned1}
+ ]}}
+ ]},
+ {[
+ {index, partitioned2},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, scope_mismatch}]}]},
+ {ranking, 6},
+ {covering, partitioned2}
+ ]}}
+ ]},
+ {[
+ {index, unfavored1},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, unfavored_type}]}]},
+ {ranking, 4},
+ {covering, null}
+ ]}}
+ ]},
+ {[
+ {index, unfavored2},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, unfavored_type}]}]},
+ {ranking, 4},
+ {covering, null}
+ ]}}
+ ]},
+ {[
+ {index, usable1},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, less_overlap}]}]},
+ {ranking, 1},
+ {covering, usable1}
+ ]}}
+ ]},
+ {[
+ {index, usable2},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, too_many_fields}]}]},
+ {ranking, 2},
+ {covering, usable2}
+ ]}}
+ ]},
+ {[
+ {index, usable3},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, alphabetically_comes_after}]}]},
+ {ranking, 3},
+ {covering, usable3}
+ ]}}
+ ]}
+ ],
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+extract_selector_hints_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(dreyfus),
+ meck:new(nouveau),
+ meck:new(mango_selector),
+ meck:new(mango_idx_view),
+ meck:new(mango_idx_text),
+ meck:new(mango_idx_nouveau)
+ end,
+ fun(_) ->
+ meck:unload(mango_idx_nouveau),
+ meck:unload(mango_idx_text),
+ meck:unload(mango_idx_view),
+ meck:unload(mango_selector),
+ meck:unload(nouveau),
+ meck:unload(dreyfus)
Review Comment:
Use a single `meck:unload()`
##########
src/mango/src/mango_cursor.erl:
##########
@@ -44,27 +44,286 @@
-define(SUPERVISOR, mango_cursor_sup).
-create(Db, Selector0, Opts) ->
+-spec create(Db, Selector, Options, Kind) -> {ok, #cursor{}} when
+ Db :: database(),
+ Selector :: selector(),
+ Options :: cursor_options(),
+ Kind :: cursor_kind().
+create(Db, Selector0, Opts, Kind) ->
Selector = mango_selector:normalize(Selector0),
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case mango_cursor:maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
+ {UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts,
Kind),
+ case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
[] ->
% use_index doesn't match a valid index - fall back to a valid one
- create_cursor(Db, UsableIndexes, Selector, Opts);
+ create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
UserSpecifiedIndex ->
- create_cursor(Db, UserSpecifiedIndex, Selector, Opts)
+ create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
end.
+-spec enhance_candidates(Candidates, [{#idx{}, properties()}]) -> Candidates
when
+ Candidates :: #{#idx{} => properties()}.
+enhance_candidates(Entries, Inputs) ->
+ Combiner =
+ fun(Key, Value1, Value2) ->
+ case Key of
+ usable -> Value1 andalso Value2;
+ reason -> lists:append(Value1, Value2);
+ ranking -> Value1 + Value2
+ end
+ end,
+ lists:foldr(
+ fun({Index, Delta}, Map) ->
+ Updater = fun(Value) -> maps:merge_with(Combiner, Value, Delta)
end,
+ maps:update_with(Index, Updater, Map)
+ end,
+ Entries,
+ Inputs
+ ).
+
+-type ranking() :: pos_integer().
+-type properties() ::
+ #{
+ usable := boolean(),
+ ranking := ranking(),
+ reason := [reason()]
+ }.
+
+-spec tag_elems(properties(), sets:set(#idx{})) -> [{#idx{}, properties()}].
+tag_elems(Properties, Set) ->
+ sets:fold(fun(Index, Acc) -> [{Index, Properties} | Acc] end, [], Set).
+
+-type reason_description() ::
+ {name, reason()}.
+-type analysis_attribute() ::
+ {usable, boolean()}
+ | {reasons, [reason_description()]}
+ | {ranking, ranking()}
+ | {covering, boolean()}.
+-type analysis() ::
+ {[analysis_attribute()]}.
+-type candidate_index_attribute() ::
+ {index, #idx{}} | {analysis, analysis()}.
+-type candidate_index() ::
+ {[candidate_index_attribute()]}.
+
+-spec extract_candidate_indexes(#cursor{}) -> [candidate_index()].
+extract_candidate_indexes(Cursor) ->
+ #cursor{trace = Trace, index = Winner, fields = Fields} = Cursor,
+ #{
+ all_indexes := AllIndexes,
+ global_indexes := GlobalIndexes,
+ partition_indexes := PartitionIndexes,
+ usable_indexes := UsableIndexes,
+ usability_map := UsabilityMap,
+ filtered_indexes := FilteredIndexes,
+ indexes_of_type := IndexesOfType
+ } = Trace,
+ % specific to view indexes
+ SortedIndexRanges = maps:get(sorted_index_ranges, Trace, []),
+ % simple difference calculations to determine the results in each stage,
+ % without looking at the implementation
+ PartialIndexes = sets:subtract(AllIndexes, GlobalIndexes),
+ OutOfScopeIndexes = sets:subtract(GlobalIndexes, PartitionIndexes),
+ NotUsableIndexes = sets:subtract(PartitionIndexes, UsableIndexes),
+ ExcludedIndexes = sets:subtract(UsableIndexes, FilteredIndexes),
+ UnfavoredIndexes = sets:subtract(FilteredIndexes, IndexesOfType),
+ % determine rankings
+ UnfavoredIndexesRank = max(1, length(SortedIndexRanges)),
+ {_, [PartialIndexesRank, OutOfScopeIndexesRank, NotUsableIndexesRank,
ExcludedIndexesRank]} =
+ lists:foldl(
Review Comment:
Wonder if it would be simpler to have a single accumulator initialized as
`[UnfavoredIndexesRank]` then and in the fun do:
```
fun(Indexes, [PrevRank | _] = Acc) ->
Rank = case ...
[Rank | Acc]
end
```
Up to you, just a suggestion.
##########
src/mango/src/mango_cursor.erl:
##########
@@ -44,27 +44,286 @@
-define(SUPERVISOR, mango_cursor_sup).
-create(Db, Selector0, Opts) ->
+-spec create(Db, Selector, Options, Kind) -> {ok, #cursor{}} when
+ Db :: database(),
+ Selector :: selector(),
+ Options :: cursor_options(),
+ Kind :: cursor_kind().
+create(Db, Selector0, Opts, Kind) ->
Selector = mango_selector:normalize(Selector0),
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case mango_cursor:maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
+ {UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts,
Kind),
+ case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
[] ->
% use_index doesn't match a valid index - fall back to a valid one
- create_cursor(Db, UsableIndexes, Selector, Opts);
+ create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
UserSpecifiedIndex ->
- create_cursor(Db, UserSpecifiedIndex, Selector, Opts)
+ create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
end.
+-spec enhance_candidates(Candidates, [{#idx{}, properties()}]) -> Candidates
when
+ Candidates :: #{#idx{} => properties()}.
+enhance_candidates(Entries, Inputs) ->
+ Combiner =
+ fun(Key, Value1, Value2) ->
+ case Key of
+ usable -> Value1 andalso Value2;
+ reason -> lists:append(Value1, Value2);
+ ranking -> Value1 + Value2
+ end
+ end,
+ lists:foldr(
+ fun({Index, Delta}, Map) ->
+ Updater = fun(Value) -> maps:merge_with(Combiner, Value, Delta)
end,
+ maps:update_with(Index, Updater, Map)
+ end,
+ Entries,
+ Inputs
+ ).
+
+-type ranking() :: pos_integer().
+-type properties() ::
+ #{
+ usable := boolean(),
+ ranking := ranking(),
+ reason := [reason()]
+ }.
+
+-spec tag_elems(properties(), sets:set(#idx{})) -> [{#idx{}, properties()}].
+tag_elems(Properties, Set) ->
+ sets:fold(fun(Index, Acc) -> [{Index, Properties} | Acc] end, [], Set).
+
+-type reason_description() ::
+ {name, reason()}.
+-type analysis_attribute() ::
+ {usable, boolean()}
+ | {reasons, [reason_description()]}
+ | {ranking, ranking()}
+ | {covering, boolean()}.
+-type analysis() ::
+ {[analysis_attribute()]}.
+-type candidate_index_attribute() ::
+ {index, #idx{}} | {analysis, analysis()}.
+-type candidate_index() ::
+ {[candidate_index_attribute()]}.
+
+-spec extract_candidate_indexes(#cursor{}) -> [candidate_index()].
+extract_candidate_indexes(Cursor) ->
+ #cursor{trace = Trace, index = Winner, fields = Fields} = Cursor,
+ #{
+ all_indexes := AllIndexes,
+ global_indexes := GlobalIndexes,
+ partition_indexes := PartitionIndexes,
+ usable_indexes := UsableIndexes,
+ usability_map := UsabilityMap,
+ filtered_indexes := FilteredIndexes,
+ indexes_of_type := IndexesOfType
+ } = Trace,
+ % specific to view indexes
+ SortedIndexRanges = maps:get(sorted_index_ranges, Trace, []),
+ % simple difference calculations to determine the results in each stage,
+ % without looking at the implementation
+ PartialIndexes = sets:subtract(AllIndexes, GlobalIndexes),
+ OutOfScopeIndexes = sets:subtract(GlobalIndexes, PartitionIndexes),
+ NotUsableIndexes = sets:subtract(PartitionIndexes, UsableIndexes),
+ ExcludedIndexes = sets:subtract(UsableIndexes, FilteredIndexes),
+ UnfavoredIndexes = sets:subtract(FilteredIndexes, IndexesOfType),
+ % determine rankings
+ UnfavoredIndexesRank = max(1, length(SortedIndexRanges)),
+ {_, [PartialIndexesRank, OutOfScopeIndexesRank, NotUsableIndexesRank,
ExcludedIndexesRank]} =
+ lists:foldl(
+ fun(Indexes, {Rank0, Acc}) ->
+ Rank =
+ case (sets:size(Indexes) > 0) of
+ true -> Rank0 + 1;
+ false -> Rank0
+ end,
+ {Rank, [Rank | Acc]}
+ end,
+ {UnfavoredIndexesRank, []},
+ [UnfavoredIndexes, ExcludedIndexes, NotUsableIndexes,
OutOfScopeIndexes]
+ ),
+ % start building the list of candidates
+ AddCandidate =
+ fun(Index, Map) ->
+ maps:put(Index, #{}, Map)
+ end,
+ Candidates0 = sets:fold(AddCandidate, #{}, AllIndexes),
+ PartialIndexesTags = tag_elems(
+ #{usable => false, reason => [is_partial], ranking =>
PartialIndexesRank},
+ PartialIndexes
+ ),
+ Candidates1 = enhance_candidates(Candidates0, PartialIndexesTags),
+ OutOfScopeIndexesTags = tag_elems(
+ #{usable => false, reason => [scope_mismatch], ranking =>
OutOfScopeIndexesRank},
+ OutOfScopeIndexes
+ ),
+ Candidates2 = enhance_candidates(Candidates1, OutOfScopeIndexesTags),
+ NotUsableIndexesTags = tag_elems(
+ % the reason is going to be filled out by the mango_idx modules
+ #{usable => false, ranking => NotUsableIndexesRank},
+ NotUsableIndexes
+ ),
+ Candidates3 = enhance_candidates(Candidates2, NotUsableIndexesTags),
+ ExcludedIndexesTags = tag_elems(
+ #{usable => true, reason => [excluded_by_user], ranking =>
ExcludedIndexesRank},
+ ExcludedIndexes
+ ),
+ Candidates4 = enhance_candidates(Candidates3, ExcludedIndexesTags),
+ UnfavoredIndexesTags = tag_elems(
+ #{usable => true, reason => [unfavored_type], ranking =>
UnfavoredIndexesRank},
+ UnfavoredIndexes
+ ),
+ Candidates5 = enhance_candidates(Candidates4, UnfavoredIndexesTags),
+ NotChosenIndexesTags = tag_elems(
+ #{usable => true},
+ IndexesOfType
+ ),
+ Candidates6 = enhance_candidates(Candidates5, NotChosenIndexesTags),
+ NotUsableDetails =
+ lists:flatmap(
+ fun({Index, {Usable, Details}}) ->
+ case Usable of
+ true -> [];
+ false -> [{Index, Details}]
+ end
+ end,
+ UsabilityMap
+ ),
+ Candidates7 = enhance_candidates(Candidates6, NotUsableDetails),
+ WinnerColumnWidth =
+ case Winner of
+ none ->
+ 0;
+ W ->
+ case mango_idx:columns(W) of
+ X when is_list(X) -> length(X);
+ all_fields -> 256
+ end
+ end,
+ {_, NotChosenDetails} =
+ lists:foldl(
+ fun({Index, _Prefix, Distance}, {N, Acc}) ->
+ ColumnWidth = length(mango_idx:columns(Index)),
+ Reason =
+ case {} of
+ _ when Distance > 0 -> [less_overlap];
+ _ when ColumnWidth > WinnerColumnWidth ->
[too_many_fields];
+ _ -> [alphabetically_comes_after]
Review Comment:
Suggestion: use `if`, `case {}` looks a bit odd and not idiomatic.
```
Reason = if
Distance > 0 -> ... ;
ColumnWidth > Winner -> ...;
true -> ...
end
```
##########
src/mango/src/mango_idx.hrl:
##########
@@ -10,6 +10,8 @@
% License for the specific language governing permissions and limitations under
% the License.
+-ifndef(MANGO_IDX_DEFINED).
+-define(MANGO_IDX_DEFINED, 1).
Review Comment:
Wonder if there is a way to avoid this `ifndef $already_defined` pattern,
it's not a very common in CouchDB code at least.
See if it's possible to move the `idx` record to `mango.hrl` and just have
everyone include that. In most case when we include mango_idx.hrl we include
mango.hrl as well. One place were we don't is mango_native_proc.erl but I don't
think we're even using mango_idx.hrl there at all.
##########
src/mango/src/mango_cursor.erl:
##########
@@ -263,3 +547,664 @@ ddoc_name(<<"_design/", Name/binary>>) ->
Name;
ddoc_name(Name) ->
Name.
+
+-ifdef(TEST).
+-include_lib("couch/include/couch_eunit.hrl").
+
+create_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(mango_selector),
+ meck:new(mango_idx, [passthrough]),
+ meck:new(mango_cursor_view)
+ end,
+ fun(_) ->
+ meck:unload(mango_cursor_view),
+ meck:unload(mango_idx),
+ meck:unload(mango_selector)
+ end,
+ [
+ ?TDEF_FE(t_create_regular),
+ ?TDEF_FE(t_create_user_specified_index),
+ ?TDEF_FE(t_create_invalid_user_specified_index)
+ ]
+ }.
+
+t_create_regular(_) ->
+ IndexSpecial = #idx{type = <<"special">>, def = all_docs},
+ IndexView = #idx{type = <<"json">>},
+ UsableIndexes = [IndexView, IndexSpecial],
+ FilteredIndexes = UsableIndexes,
+ IndexesOfType = [IndexView],
+ Trace1 = #{},
+ Trace2 =
+ #{
+ filtered_indexes => sets:from_list(FilteredIndexes),
+ indexes_of_type => sets:from_list(IndexesOfType)
+ },
+ Options = [{use_index, []}],
+ meck:expect(mango_selector, normalize, [selector],
meck:val(normalized_selector)),
+ meck:expect(
+ mango_idx,
+ get_usable_indexes,
+ [db, normalized_selector, Options, target],
+ meck:val({UsableIndexes, Trace1})
+ ),
+ meck:expect(
+ mango_cursor_view,
+ create,
+ [db, {IndexesOfType, Trace2}, normalized_selector, Options],
+ meck:val(view_cursor)
+ ),
+ ?assertEqual(view_cursor, create(db, selector, Options, target)).
+
+t_create_user_specified_index(_) ->
+ IndexSpecial = #idx{type = <<"special">>, def = all_docs},
+ IndexView1 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx1">>},
+ IndexView2 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx2">>},
+ IndexView3 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx3">>},
+ UsableIndexes = [IndexSpecial, IndexView1, IndexView2, IndexView3],
+ FilteredIndexes = [IndexView2],
+ IndexesOfType = FilteredIndexes,
+ Trace1 = #{},
+ Trace2 =
+ #{
+ filtered_indexes => sets:from_list(FilteredIndexes),
+ indexes_of_type => sets:from_list(IndexesOfType)
+ },
+ Options = [{use_index, [<<"_design/view_idx2">>]}],
+ meck:expect(mango_selector, normalize, [selector],
meck:val(normalized_selector)),
+ meck:expect(
+ mango_idx,
+ get_usable_indexes,
+ [db, normalized_selector, Options, target],
+ meck:val({UsableIndexes, Trace1})
+ ),
+ meck:expect(
+ mango_cursor_view,
+ create,
+ [db, {IndexesOfType, Trace2}, normalized_selector, Options],
+ meck:val(view_cursor)
+ ),
+ ?assertEqual(view_cursor, create(db, selector, Options, target)).
+
+t_create_invalid_user_specified_index(_) ->
+ IndexSpecial = #idx{type = <<"special">>, def = all_docs},
+ IndexView1 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx1">>},
+ IndexView2 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx2">>},
+ IndexView3 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx3">>},
+ UsableIndexes = [IndexSpecial, IndexView1, IndexView2, IndexView3],
+ IndexesOfType = [IndexView1, IndexView2, IndexView3],
+ Trace1 = #{},
+ Trace2 =
+ #{
+ filtered_indexes => sets:from_list(UsableIndexes),
+ indexes_of_type => sets:from_list(IndexesOfType)
+ },
+ Options = [{use_index, [<<"foobar">>]}],
+ meck:expect(mango_selector, normalize, [selector],
meck:val(normalized_selector)),
+ meck:expect(
+ mango_idx,
+ get_usable_indexes,
+ [db, normalized_selector, Options, target],
+ meck:val({UsableIndexes, Trace1})
+ ),
+ meck:expect(
+ mango_cursor_view,
+ create,
+ [db, {IndexesOfType, Trace2}, normalized_selector, Options],
+ meck:val(view_cursor)
+ ),
+ ?assertEqual(view_cursor, create(db, selector, Options, target)).
+
+enhance_candidates_test() ->
+ Candidates1 = #{index => #{reason => [], usable => true}},
+ Candidates2 = #{index => #{reason => [reason1], usable => true}},
+ Candidates3 = #{index => #{reason => [reason1, reason2], usable => false}},
+ Deltas1 = [{index, #{reason => [reason1], usable => true}}],
+ Deltas2 = [{index, #{reason => [reason2], usable => false}}],
+ ?assertEqual(Candidates2, enhance_candidates(Candidates1, Deltas1)),
+ ?assertEqual(Candidates3, enhance_candidates(Candidates2, Deltas2)).
+
+extract_candidate_indexes_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(mango_idx, [passthrough]),
+ meck:new(mango_idx_view, [passthrough])
+ end,
+ fun(_) ->
+ meck:unload(mango_idx_view),
+ meck:unload(mango_idx)
+ end,
+ [
+ ?TDEF_FE(t_extract_candidate_indexes_empty),
+ ?TDEF_FE(t_extract_candidate_indexes_singleton),
+ ?TDEF_FE(t_extract_candidate_indexes_user_specified),
+ ?TDEF_FE(t_extract_candidate_indexes_regular)
+ ]
+ }.
+
+t_extract_candidate_indexes_empty(_) ->
+ Indexes = sets:new(),
+ UsabilityMap = [],
+ Trace =
+ #{
+ all_indexes => Indexes,
+ global_indexes => Indexes,
+ partition_indexes => Indexes,
+ usable_indexes => Indexes,
+ usability_map => UsabilityMap,
+ filtered_indexes => Indexes,
+ indexes_of_type => Indexes
+ },
+ Cursor =
+ #cursor{
+ index = none,
+ trace = Trace
+ },
+ Candidates = [],
+ ?assertNot(meck:called(mango_idx, columns, '_')),
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+t_extract_candidate_indexes_singleton(_) ->
+ Indexes = sets:from_list([winner]),
+ UsabilityMap = [{winner, {true, #{reason => []}}}],
+ Trace =
+ #{
+ all_indexes => Indexes,
+ global_indexes => Indexes,
+ partition_indexes => Indexes,
+ usable_indexes => Indexes,
+ usability_map => UsabilityMap,
+ filtered_indexes => Indexes,
+ indexes_of_type => Indexes
+ },
+ Cursor =
+ #cursor{
+ index = winner,
+ trace = Trace
+ },
+ Candidates = [],
+ meck:expect(mango_idx, columns, [winner], meck:val([column])),
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+t_extract_candidate_indexes_user_specified(_) ->
+ Partial = #idx{type = <<"json">>, name = partial},
+ Partitioned = #idx{type = <<"json">>, name = partitioned},
+ NotUsable = #idx{type = <<"json">>, name = not_usable},
+ Filtered = #idx{type = <<"json">>, name = filtered},
+ Unfavored = #idx{type = <<"special">>, name = unfavored},
+ UsabilityMap =
+ [
+ {winner, {true, #{reason => []}}},
+ {NotUsable, {false, #{reason => [field_mismatch]}}},
+ {Filtered, {true, #{reason => []}}},
+ {Unfavored, {true, #{reason => []}}}
+ ],
+ Trace =
+ #{
+ all_indexes => sets:from_list([
+ winner, Partial, Partitioned, NotUsable, Filtered, Unfavored
+ ]),
+ global_indexes => sets:from_list([winner, Partitioned, NotUsable,
Filtered, Unfavored]),
+ partition_indexes => sets:from_list([winner, NotUsable, Filtered,
Unfavored]),
+ usable_indexes => sets:from_list([winner, Filtered, Unfavored]),
+ usability_map => UsabilityMap,
+ filtered_indexes => sets:from_list([winner, Unfavored]),
+ indexes_of_type => sets:from_list([winner])
+ },
+ Cursor =
+ #cursor{
+ index = winner,
+ trace = Trace,
+ fields = fields
+ },
+ meck:expect(mango_idx, columns, [winner], meck:val(all_fields)),
+ meck:expect(mango_idx, to_json, fun(#idx{name = Name}) -> Name end),
+ meck:expect(mango_idx_view, covers, fun(#idx{name = Name}, fields) -> Name
end),
+ Candidates =
+ [
+ {[
+ {index, filtered},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, excluded_by_user}]}]},
+ {ranking, 2},
+ {covering, filtered}
+ ]}}
+ ]},
+ {[
+ {index, not_usable},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, field_mismatch}]}]},
+ {ranking, 3},
+ {covering, not_usable}
+ ]}}
+ ]},
+ {[
+ {index, partial},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, is_partial}]}]},
+ {ranking, 5},
+ {covering, partial}
+ ]}}
+ ]},
+ {[
+ {index, partitioned},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, scope_mismatch}]}]},
+ {ranking, 4},
+ {covering, partitioned}
+ ]}}
+ ]},
+ {[
+ {index, unfavored},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, unfavored_type}]}]},
+ {ranking, 1},
+ {covering, null}
+ ]}}
+ ]}
+ ],
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+t_extract_candidate_indexes_regular(_) ->
+ Partial1 = #idx{type = <<"json">>, name = partial1},
+ Partial2 = #idx{type = <<"json">>, name = partial2},
+ Partitioned1 = #idx{type = <<"json">>, name = partitioned1},
+ Partitioned2 = #idx{type = <<"json">>, name = partitioned2},
+ NotUsable = #idx{type = <<"json">>, name = not_usable},
+ Unfavored1 = #idx{type = <<"special">>, name = unfavored1},
+ Unfavored2 = #idx{type = <<"text">>, name = unfavored2},
+ Usable1 = #idx{type = <<"json">>, name = usable1},
+ Usable2 = #idx{type = <<"json">>, name = usable2},
+ Usable3 = #idx{type = <<"json">>, name = usable3},
+ UsabilityMap =
+ [
+ {winner, {true, #{reason => []}}},
+ {NotUsable, {false, #{reason => [not_usable_reason]}}},
+ {Unfavored1, {true, #{reason => []}}},
+ {Unfavored2, {true, #{reason => []}}},
+ {Usable1, {true, #{reason => []}}},
+ {Usable2, {true, #{reason => []}}},
+ {Usable3, {true, #{reason => []}}}
+ ],
+ SortedIndexRanges = [
+ {winner, prefix0, 0}, {Usable1, prefix1, 1}, {Usable2, prefix2, 0},
{Usable3, prefix3, 0}
+ ],
+ Trace =
+ #{
+ all_indexes => sets:from_list([
+ winner,
+ Partial1,
+ Partial2,
+ Partitioned1,
+ Partitioned2,
+ NotUsable,
+ Unfavored1,
+ Unfavored2,
+ Usable1,
+ Usable2,
+ Usable3
+ ]),
+ global_indexes => sets:from_list([
+ winner,
+ Partitioned1,
+ Partitioned2,
+ NotUsable,
+ Unfavored1,
+ Unfavored2,
+ Usable1,
+ Usable2,
+ Usable3
+ ]),
+ partition_indexes => sets:from_list([
+ winner, NotUsable, Unfavored1, Unfavored2, Usable1, Usable2,
Usable3
+ ]),
+ usable_indexes => sets:from_list([
+ winner, Unfavored1, Unfavored2, Usable1, Usable2, Usable3
+ ]),
+ usability_map => UsabilityMap,
+ filtered_indexes => sets:from_list([
+ winner, Unfavored1, Unfavored2, Usable1, Usable2, Usable3
+ ]),
+ indexes_of_type => sets:from_list([winner, Usable1, Usable2,
Usable3]),
+ sorted_index_ranges => SortedIndexRanges
+ },
+ Cursor =
+ #cursor{
+ index = winner,
+ trace = Trace,
+ fields = fields
+ },
+ meck:expect(
+ mango_idx,
+ columns,
+ fun(Index) ->
+ case Index of
+ winner -> [column];
+ Usable1 -> [column1, column2];
+ Usable2 -> [column1, column2, column3];
+ Usable3 -> [column]
+ end
+ end
+ ),
+ meck:expect(mango_idx, to_json, fun(#idx{name = Name}) -> Name end),
+ meck:expect(mango_idx_view, covers, fun(#idx{name = Name}, fields) -> Name
end),
+ Candidates =
+ [
+ {[
+ {index, not_usable},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, not_usable_reason}]}]},
+ {ranking, 5},
+ {covering, not_usable}
+ ]}}
+ ]},
+ {[
+ {index, partial1},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, is_partial}]}]},
+ {ranking, 7},
+ {covering, partial1}
+ ]}}
+ ]},
+ {[
+ {index, partial2},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, is_partial}]}]},
+ {ranking, 7},
+ {covering, partial2}
+ ]}}
+ ]},
+ {[
+ {index, partitioned1},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, scope_mismatch}]}]},
+ {ranking, 6},
+ {covering, partitioned1}
+ ]}}
+ ]},
+ {[
+ {index, partitioned2},
+ {analysis,
+ {[
+ {usable, false},
+ {reasons, [{[{name, scope_mismatch}]}]},
+ {ranking, 6},
+ {covering, partitioned2}
+ ]}}
+ ]},
+ {[
+ {index, unfavored1},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, unfavored_type}]}]},
+ {ranking, 4},
+ {covering, null}
+ ]}}
+ ]},
+ {[
+ {index, unfavored2},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, unfavored_type}]}]},
+ {ranking, 4},
+ {covering, null}
+ ]}}
+ ]},
+ {[
+ {index, usable1},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, less_overlap}]}]},
+ {ranking, 1},
+ {covering, usable1}
+ ]}}
+ ]},
+ {[
+ {index, usable2},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, too_many_fields}]}]},
+ {ranking, 2},
+ {covering, usable2}
+ ]}}
+ ]},
+ {[
+ {index, usable3},
+ {analysis,
+ {[
+ {usable, true},
+ {reasons, [{[{name, alphabetically_comes_after}]}]},
+ {ranking, 3},
+ {covering, usable3}
+ ]}}
+ ]}
+ ],
+ ?assertEqual(Candidates, extract_candidate_indexes(Cursor)).
+
+extract_selector_hints_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(dreyfus),
+ meck:new(nouveau),
+ meck:new(mango_selector),
+ meck:new(mango_idx_view),
+ meck:new(mango_idx_text),
+ meck:new(mango_idx_nouveau)
+ end,
+ fun(_) ->
+ meck:unload(mango_idx_nouveau),
+ meck:unload(mango_idx_text),
+ meck:unload(mango_idx_view),
+ meck:unload(mango_selector),
+ meck:unload(nouveau),
+ meck:unload(dreyfus)
+ end,
+ [
+ ?TDEF_FE(t_extract_selector_hints_view),
+ ?TDEF_FE(t_extract_selector_hints_text),
+ ?TDEF_FE(t_extract_selector_hints_nouveau)
+ ]
+ }.
+
+t_extract_selector_hints_view(_) ->
+ meck:expect(dreyfus, available, [], meck:val(false)),
+ meck:expect(nouveau, enabled, [], meck:val(false)),
+ meck:expect(mango_selector, fields, [selector], meck:val(["field1",
"field2", "field3"])),
+ meck:expect(mango_idx_view, indexable_fields, [selector],
meck:val(["field2"])),
+ Hints =
+ [
+ {[
+ {type, json},
+ {indexable_fields, ["field2"]},
+ {unindexable_fields, ["field3", "field1"]}
+ ]}
+ ],
+ ?assertEqual(Hints, extract_selector_hints(selector)).
+
+t_extract_selector_hints_text(_) ->
+ meck:expect(dreyfus, available, [], meck:val(true)),
+ meck:expect(nouveau, enabled, [], meck:val(false)),
+ meck:expect(mango_selector, fields, [selector], meck:val(["field1",
"field2", "field3"])),
+ meck:expect(mango_idx_view, indexable_fields, [selector],
meck:val(["field2"])),
+ meck:expect(mango_idx_text, indexable_fields, [selector],
meck:val(["field1"])),
+ Hints =
+ [
+ {[
+ {type, json},
+ {indexable_fields, ["field2"]},
+ {unindexable_fields, ["field3", "field1"]}
+ ]},
+ {[
+ {type, text},
+ {indexable_fields, ["field1"]},
+ {unindexable_fields, ["field3", "field2"]}
+ ]}
+ ],
+ ?assertEqual(Hints, extract_selector_hints(selector)).
+
+t_extract_selector_hints_nouveau(_) ->
+ meck:expect(dreyfus, available, [], meck:val(false)),
+ meck:expect(nouveau, enabled, [], meck:val(true)),
+ meck:expect(mango_selector, fields, [selector], meck:val(["field1",
"field2", "field3"])),
+ meck:expect(mango_idx_view, indexable_fields, [selector],
meck:val(["field2"])),
+ meck:expect(mango_idx_nouveau, indexable_fields, [selector],
meck:val(["field1"])),
+ Hints =
+ [
+ {[
+ {type, json},
+ {indexable_fields, ["field2"]},
+ {unindexable_fields, ["field3", "field1"]}
+ ]},
+ {[
+ {type, nouveau},
+ {indexable_fields, ["field1"]},
+ {unindexable_fields, ["field3", "field2"]}
+ ]}
+ ],
+ ?assertEqual(Hints, extract_selector_hints(selector)).
+
+explain_test_() ->
+ {
+ foreach,
+ fun() ->
+ meck:new(mango_idx, [passthrough]),
+ meck:new(mango_idx_special, [passthrough]),
+ meck:new(mango_cursor_special),
+ meck:new(dreyfus),
+ meck:new(nouveau),
+ meck:new(couch_db)
+ end,
+ fun(_) ->
+ meck:unload(couch_db),
+ meck:unload(nouveau),
+ meck:unload(dreyfus),
+ meck:unload(mango_cursor_special),
+ meck:unload(mango_idx_special),
+ meck:unload(mango_idx)
Review Comment:
Use `meck:unload()` like in other similar cases.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]