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]

Reply via email to