add mango catch-all feature

fall back on the all_docs index if a index for the currently
searched field does not exist.

warn about slower execution and advise to create an index.

PR: #27
PR-URL: https://github.com/apache/couchdb-mango/pull/27
Reviewed-By: Alexander Shorin
Reviewed-By: Tony Sun <tony.sun@cloudant.com>
diff --git a/src/mango_cursor.erl b/src/mango_cursor.erl
index 6cb7fee..911ecdb 100644
--- a/src/mango_cursor.erl
+++ b/src/mango_cursor.erl
@@ -16,7 +16,8 @@
 -export([
     create/3,
     explain/1,
-    execute/3
+    execute/3,
+    maybe_filter_indexes/2
 ]).
 
 
@@ -30,29 +31,18 @@
 
 create(Db, Selector0, Opts) ->
     Selector = mango_selector:normalize(Selector0),
+    UsableIndexes = mango_idx:get_usable_indexes(Db, Selector0, Opts),
 
-    ExistingIndexes = mango_idx:list(Db),
-    if ExistingIndexes /= [] -> ok; true ->
-        ?MANGO_ERROR({no_usable_index, no_indexes_defined})
-    end,
-
-    FilteredIndexes = maybe_filter_indexes(ExistingIndexes, Opts),
-    if FilteredIndexes /= [] -> ok; true ->
-        ?MANGO_ERROR({no_usable_index, no_index_matching_name})
-    end,
-
-    SortIndexes = mango_idx:for_sort(FilteredIndexes, Opts),
-    if SortIndexes /= [] -> ok; true ->
-        ?MANGO_ERROR({no_usable_index, missing_sort_index})
-    end,
-
-    UsableFilter = fun(I) -> mango_idx:is_usable(I, Selector) end,
-    UsableIndexes = lists:filter(UsableFilter, SortIndexes),
-    if UsableIndexes /= [] -> ok; true ->
-        ?MANGO_ERROR({no_usable_index, selector_unsupported})
-    end,
-
-    create_cursor(Db, UsableIndexes, Selector, Opts).
+    {use_index, IndexSpecified} = proplists:lookup(use_index, Opts),
+    case {length(UsableIndexes), length(IndexSpecified)} of
+        {0, 1} ->
+            ?MANGO_ERROR({no_usable_index, selector_unsupported});
+        {0, 0} ->
+            AllDocs = mango_idx:special(Db),
+            create_cursor(Db, AllDocs, Selector, Opts);
+        _ ->
+            create_cursor(Db, UsableIndexes, Selector, Opts)
+    end.
 
 
 explain(#cursor{}=Cursor) ->
@@ -125,9 +115,9 @@
     % queries.
     CursorModules = case module_loaded(dreyfus_index) of
         true ->
-            [mango_cursor_view, mango_cursor_text];
+            [mango_cursor_view, mango_cursor_text, mango_cursor_special];
         false ->
-            [mango_cursor_view]
+            [mango_cursor_view, mango_cursor_special]
     end,
     lists:flatmap(fun(CMod) ->
         case dict:find(CMod, IdxDict) of
diff --git a/src/mango_cursor_special.erl b/src/mango_cursor_special.erl
new file mode 100644
index 0000000..6058217
--- /dev/null
+++ b/src/mango_cursor_special.erl
@@ -0,0 +1,61 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(mango_cursor_special).
+
+-export([
+    create/4,
+    explain/1,
+    execute/3
+]).
+
+-export([
+    handle_message/2
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch_mrview/include/couch_mrview.hrl").
+-include("mango_cursor.hrl").
+
+
+create(Db, Indexes, Selector, Opts) ->
+    InitialRange = mango_idx_view:field_ranges(Selector),
+    CatchAll = [{<<"_id">>, {'$gt', null, '$lt', mango_json_max}}],
+    FieldRanges = lists:append(CatchAll, InitialRange),
+    Composited = mango_cursor_view:composite_indexes(Indexes, FieldRanges),
+    {Index, IndexRanges} = mango_cursor_view:choose_best_index(Db, Composited),
+
+    Limit = couch_util:get_value(limit, Opts, 10000000000),
+    Skip = couch_util:get_value(skip, Opts, 0),
+    Fields = couch_util:get_value(fields, Opts, all_fields),
+
+    {ok, #cursor{
+        db = Db,
+        index = Index,
+        ranges = IndexRanges,
+        selector = Selector,
+        opts = Opts,
+        limit = Limit,
+        skip = Skip,
+        fields = Fields
+    }}.
+
+
+explain(Cursor) ->
+    mango_cursor_view:explain(Cursor).
+
+execute(Cursor0, UserFun, UserAcc) ->
+    mango_cursor_view:execute(Cursor0, UserFun, UserAcc).
+
+handle_message(Msg, Cursor) ->
+    mango_cursor_view:handle_message(Msg, Cursor).
diff --git a/src/mango_cursor_view.erl b/src/mango_cursor_view.erl
index cb35968..5f109cd 100644
--- a/src/mango_cursor_view.erl
+++ b/src/mango_cursor_view.erl
@@ -19,7 +19,9 @@
 ]).
 
 -export([
-    handle_message/2
+    handle_message/2,
+    composite_indexes/2,
+    choose_best_index/2
 ]).
 
 
diff --git a/src/mango_httpd.erl b/src/mango_httpd.erl
index ef7135c..bde3850 100644
--- a/src/mango_httpd.erl
+++ b/src/mango_httpd.erl
@@ -181,7 +181,7 @@
     chttpd:validate_ctype(Req, "application/json"),
     {ok, Opts0} = mango_opts:validate_find(chttpd:json_body_obj(Req)),
     {value, {selector, Sel}, Opts} = lists:keytake(selector, 1, Opts0),
-    {ok, Resp0} = start_find_resp(Req),
+    {ok, Resp0} = start_find_resp(Req, Db, Sel, Opts),
     {ok, AccOut} = run_find(Resp0, Db, Sel, Opts),
     end_find_resp(AccOut);
 
@@ -228,8 +228,18 @@
     end.
 
 
-start_find_resp(Req) ->
-    chttpd:start_delayed_json_response(Req, 200, [], "{\"docs\":[").
+start_find_resp(Req, Db, Sel, Opts) ->
+    chttpd:start_delayed_json_response(Req, 200, [], maybe_add_warning(Db, Sel, Opts)).
+
+
+maybe_add_warning(Db, Selector, Opts) ->
+    UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
+    case length(UsableIndexes) of
+        0 ->
+            "{\"warning\":\"no matching index found, create an index to optimize query time\",\r\n\"docs\":[";
+        _ ->
+            "{\"docs\":["
+    end.
 
 
 end_find_resp(Acc0) ->
diff --git a/src/mango_idx.erl b/src/mango_idx.erl
index 377af91..11c713b 100644
--- a/src/mango_idx.erl
+++ b/src/mango_idx.erl
@@ -42,7 +42,8 @@
     cursor_mod/1,
     idx_mod/1,
     to_json/1,
-    delete/4
+    delete/4,
+    get_usable_indexes/3
 ]).
 
 
@@ -55,6 +56,27 @@
     {ok, Indexes} = ddoc_cache:open(db_to_name(Db), ?MODULE),
     Indexes.
 
+get_usable_indexes(Db, Selector0, Opts) ->
+    Selector = mango_selector:normalize(Selector0),
+
+    ExistingIndexes = mango_idx:list(Db),
+    if ExistingIndexes /= [] -> ok; true ->
+        ?MANGO_ERROR({no_usable_index, no_indexes_defined})
+    end,
+
+    FilteredIndexes = mango_cursor:maybe_filter_indexes(ExistingIndexes, Opts),
+    if FilteredIndexes /= [] -> ok; true ->
+        ?MANGO_ERROR({no_usable_index, no_index_matching_name})
+    end,
+
+    SortIndexes = mango_idx:for_sort(FilteredIndexes, Opts),
+    if SortIndexes /= [] -> ok; true ->
+        ?MANGO_ERROR({no_usable_index, missing_sort_index})
+    end,
+
+    UsableFilter = fun(I) -> mango_idx:is_usable(I, Selector) end,
+    lists:filter(UsableFilter, SortIndexes).
+
 recover(Db) ->
     {ok, DDocs0} = mango_util:open_ddocs(Db),
     Pred = fun({Props}) ->
@@ -245,7 +267,7 @@
 cursor_mod(#idx{type = <<"json">>}) ->
     mango_cursor_view;
 cursor_mod(#idx{def = all_docs, type= <<"special">>}) ->
-    mango_cursor_view;
+    mango_cursor_special;
 cursor_mod(#idx{type = <<"text">>}) ->
     case module_loaded(dreyfus_index) of
         true ->
diff --git a/test/02-basic-find-test.py b/test/02-basic-find-test.py
index 37459a2..c7eb348 100644
--- a/test/02-basic-find-test.py
+++ b/test/02-basic-find-test.py
@@ -236,12 +236,9 @@
             assert len(docs) == 15
 
     def test_empty(self):
-        try:
-            self.db.find({})
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+        docs = self.db.find({})
+        # 15 users and 9 design docs
+        assert len(docs) == 24
 
     def test_empty_subsel(self):
         docs = self.db.find({
diff --git a/test/05-index-selection-test.py b/test/05-index-selection-test.py
index 3a757f8..bbd3aa7 100644
--- a/test/05-index-selection-test.py
+++ b/test/05-index-selection-test.py
@@ -74,6 +74,16 @@
             }, use_index=ddocid, explain=True)
         assert resp["index"]["ddoc"] == ddocid
 
+    def test_use_most_columns(self):
+        # ddoc id for the age index
+        ddocid = "_design/ad3d537c03cd7c6a43cf8dff66ef70ea54c2b40f"
+        try:
+            self.db.find({}, use_index=ddocid)
+        except Exception, e:
+            assert e.response.status_code == 400
+        else:
+            raise AssertionError("bad find")
+
     # This doc will not be saved given the new ddoc validation code
     # in couch_mrview
     def test_manual_bad_view_idx01(self):