Skip to content

Commit e21d0e9

Browse files
committed
mango: add allow_fallback for user-specified indexes on _find
It is not always beneficial for the performance if the Mango query planner tries to assign an index to the selector. User-specified indexes may save the day, but since they are only hints for the planner, fallbacks may still happen. Introduce the `allow_fallback` flag which can be used to tell if falling back to other indexes is acceptable when an index is explicitly specified by the user. When set to `false`, give up on planning and return an HTTP 400 response right away. This way the user has the chance to learn about the requested but missing index, optionally create it and try again. By default, fallbacks are allowed to maintain backwards compatibility. It is possible to set `allow_fallback` to `true` but currently it coincides with the default behavior hence becomes a no-op in practice. Fixes #4511
1 parent cf99034 commit e21d0e9

File tree

7 files changed

+160
-10
lines changed

7 files changed

+160
-10
lines changed

src/docs/src/api/database/find.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
is attempted. Therefore that is more like a hint. When
5151
fallback occurs, the details are given in the ``warning``
5252
field of the response. *Optional*
53+
:<json boolean allow_fallback: Tell if it is allowed to fall back
54+
to a valid index when requesting a query to use a specific
55+
index that is not deemed usable. Default is ``true``. This
56+
is meant to be used in combination with ``use_index`` and
57+
setting ``allow_fallback`` to ``false`` can make the query
58+
fail if the user-specified index is not suitable. *Optional*
5359
:<json boolean conflicts: Include conflicted documents if ``true``.
5460
Intended use is to easily find conflicted documents, without an
5561
index or view. Default is ``false``. *Optional*
@@ -1498,7 +1504,8 @@ it easier to take advantage of future improvements to query planning
14981504
"stale": false,
14991505
"update": true,
15001506
"stable": false,
1501-
"execution_stats": false
1507+
"execution_stats": false,
1508+
"allow_fallback": true
15021509
},
15031510
"limit": 2,
15041511
"skip": 0,

src/mango/src/mango_cursor.erl

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,22 @@ create(Db, Selector0, Opts, Kind) ->
5353
{UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts, Kind),
5454
case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
5555
[] ->
56-
% use_index doesn't match a valid index - fall back to a valid one
57-
create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
56+
% use_index doesn't match a valid index - determine how
57+
% this shall be handled by the further settings
58+
case allow_fallback(Opts) of
59+
true ->
60+
% fall back to a valid index
61+
create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
62+
false ->
63+
% return an error
64+
Details =
65+
case use_index(Opts) of
66+
[] -> [];
67+
[DesignId] -> [ddoc_name(DesignId)];
68+
[DesignId, ViewName] -> [ddoc_name(DesignId), ViewName]
69+
end,
70+
?MANGO_ERROR({invalid_index, Details})
71+
end;
5872
UserSpecifiedIndex ->
5973
create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
6074
end.
@@ -366,13 +380,21 @@ execute(#cursor{index = Idx} = Cursor, UserFun, UserAcc) ->
366380
Mod = mango_idx:cursor_mod(Idx),
367381
Mod:execute(Cursor, UserFun, UserAcc).
368382

383+
use_index(Opts) ->
384+
{use_index, UseIndex} = lists:keyfind(use_index, 1, Opts),
385+
UseIndex.
386+
387+
allow_fallback(Opts) ->
388+
{allow_fallback, AllowFallback} = lists:keyfind(allow_fallback, 1, Opts),
389+
AllowFallback.
390+
369391
maybe_filter_indexes_by_ddoc(Indexes, Opts) ->
370-
case lists:keyfind(use_index, 1, Opts) of
371-
{use_index, []} ->
392+
case use_index(Opts) of
393+
[] ->
372394
[];
373-
{use_index, [DesignId]} ->
395+
[DesignId] ->
374396
filter_indexes(Indexes, DesignId);
375-
{use_index, [DesignId, ViewName]} ->
397+
[DesignId, ViewName] ->
376398
filter_indexes(Indexes, DesignId, ViewName)
377399
end.
378400

@@ -575,7 +597,9 @@ create_test_() ->
575597
[
576598
?TDEF_FE(t_create_regular, 10),
577599
?TDEF_FE(t_create_user_specified_index, 10),
578-
?TDEF_FE(t_create_invalid_user_specified_index, 10)
600+
?TDEF_FE(t_create_invalid_user_specified_index, 10),
601+
?TDEF_FE(t_create_invalid_user_specified_index_no_fallback_1, 10),
602+
?TDEF_FE(t_create_invalid_user_specified_index_no_fallback_2, 10)
579603
]
580604
}.
581605

@@ -591,7 +615,7 @@ t_create_regular(_) ->
591615
filtered_indexes => sets:from_list(FilteredIndexes),
592616
indexes_of_type => sets:from_list(IndexesOfType)
593617
},
594-
Options = [{use_index, []}],
618+
Options = [{use_index, []}, {allow_fallback, true}],
595619
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
596620
meck:expect(
597621
mango_idx,
@@ -650,7 +674,7 @@ t_create_invalid_user_specified_index(_) ->
650674
filtered_indexes => sets:from_list(UsableIndexes),
651675
indexes_of_type => sets:from_list(IndexesOfType)
652676
},
653-
Options = [{use_index, [<<"foobar">>]}],
677+
Options = [{use_index, [<<"foobar">>]}, {allow_fallback, true}],
654678
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
655679
meck:expect(
656680
mango_idx,
@@ -666,6 +690,68 @@ t_create_invalid_user_specified_index(_) ->
666690
),
667691
?assertEqual(view_cursor, create(db, selector, Options, target)).
668692

693+
t_create_invalid_user_specified_index_no_fallback_1(_) ->
694+
IndexSpecial = #idx{type = <<"special">>, def = all_docs},
695+
IndexView1 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx1">>},
696+
IndexView2 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx2">>},
697+
IndexView3 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx3">>},
698+
UsableIndexes = [IndexSpecial, IndexView1, IndexView2, IndexView3],
699+
IndexesOfType = [IndexView1, IndexView2, IndexView3],
700+
Trace1 = #{},
701+
Trace2 =
702+
#{
703+
filtered_indexes => sets:from_list(UsableIndexes),
704+
indexes_of_type => sets:from_list(IndexesOfType)
705+
},
706+
UseIndex = [<<"design">>, <<"foobar">>],
707+
Options = [{use_index, UseIndex}, {allow_fallback, false}],
708+
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
709+
meck:expect(
710+
mango_idx,
711+
get_usable_indexes,
712+
[db, normalized_selector, Options, target],
713+
meck:val({UsableIndexes, Trace1})
714+
),
715+
meck:expect(
716+
mango_cursor_view,
717+
create,
718+
[db, {IndexesOfType, Trace2}, normalized_selector, Options],
719+
meck:val(view_cursor)
720+
),
721+
Exception = {mango_error, mango_cursor, {invalid_index, UseIndex}},
722+
?assertThrow(Exception, create(db, selector, Options, target)).
723+
724+
t_create_invalid_user_specified_index_no_fallback_2(_) ->
725+
IndexSpecial = #idx{type = <<"special">>, def = all_docs},
726+
IndexView1 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx1">>},
727+
IndexView2 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx2">>},
728+
IndexView3 = #idx{type = <<"json">>, ddoc = <<"_design/view_idx3">>},
729+
UsableIndexes = [IndexSpecial, IndexView1, IndexView2, IndexView3],
730+
IndexesOfType = [IndexView1, IndexView2, IndexView3],
731+
Trace1 = #{},
732+
Trace2 =
733+
#{
734+
filtered_indexes => sets:from_list(UsableIndexes),
735+
indexes_of_type => sets:from_list(IndexesOfType)
736+
},
737+
UseIndex = [],
738+
Options = [{use_index, UseIndex}, {allow_fallback, false}],
739+
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
740+
meck:expect(
741+
mango_idx,
742+
get_usable_indexes,
743+
[db, normalized_selector, Options, target],
744+
meck:val({UsableIndexes, Trace1})
745+
),
746+
meck:expect(
747+
mango_cursor_view,
748+
create,
749+
[db, {IndexesOfType, Trace2}, normalized_selector, Options],
750+
meck:val(view_cursor)
751+
),
752+
Exception = {mango_error, mango_cursor, {invalid_index, UseIndex}},
753+
?assertThrow(Exception, create(db, selector, Options, target)).
754+
669755
enhance_candidates_test() ->
670756
Candidates1 = #{index => #{reason => [], usable => true}},
671757
Candidates2 = #{index => #{reason => [reason1], usable => true}},

src/mango/src/mango_error.erl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ info(mango_json_bookmark, {invalid_bookmark, BadBookmark}) ->
4848
<<"invalid_bookmark">>,
4949
fmt("Invalid bookmark value: ~s", [?JSON_ENCODE(BadBookmark)])
5050
};
51+
info(mango_cursor, {invalid_index, []}) ->
52+
{
53+
400,
54+
<<"invalid_index">>,
55+
<<"You must specify an index with the `use_index` parameter.">>
56+
};
57+
info(mango_cursor, {invalid_index, [DDocName]}) ->
58+
{
59+
400,
60+
<<"invalid_index">>,
61+
fmt("_design/~s specified by `use_index` could not be found or it is not suitable.", [
62+
DDocName
63+
])
64+
};
65+
info(mango_cursor, {invalid_index, [DDocName, ViewName]}) ->
66+
{
67+
400,
68+
<<"invalid_index">>,
69+
fmt("_design/~s, ~s specified by `use_index` could not be found or it is not suitable.", [
70+
DDocName, ViewName
71+
])
72+
};
5173
info(mango_cursor_text, {invalid_bookmark, BadBookmark}) ->
5274
{
5375
400,

src/mango/src/mango_opts.erl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ validate_find({Props}) ->
162162
{optional, true},
163163
{default, false},
164164
{validator, fun mango_opts:is_boolean/1}
165+
]},
166+
{<<"allow_fallback">>, [
167+
{tag, allow_fallback},
168+
{optional, true},
169+
{default, true},
170+
{validator, fun mango_opts:is_boolean/1}
165171
]}
166172
],
167173
validate(Props, Opts).

src/mango/test/02-basic-find-test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ def test_explain_options(self):
311311
assert opts["stale"] == False
312312
assert opts["update"] == True
313313
assert opts["use_index"] == []
314+
assert opts["allow_fallback"] == True
314315

315316
def test_sort_with_all_docs(self):
316317
explain = self.db.find(

src/mango/test/05-index-selection-test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,31 @@ def test_explain_sort_reverse(self):
211211
)
212212
self.assertEqual(resp_explain["index"]["type"], "json")
213213

214+
def test_use_index_without_fallback(self):
215+
with self.subTest(use_index="valid"):
216+
docs = self.db.find(
217+
{"manager": True}, use_index="manager", allow_fallback=False
218+
)
219+
assert len(docs) > 0
220+
221+
with self.subTest(use_index="invalid"):
222+
try:
223+
self.db.find(
224+
{"manager": True}, use_index="invalid", allow_fallback=False
225+
)
226+
except Exception as e:
227+
self.assertEqual(e.response.status_code, 400)
228+
else:
229+
raise AssertionError("did not fail on invalid index")
230+
231+
with self.subTest(use_index="empty"):
232+
try:
233+
self.db.find({"manager": True}, use_index=[], allow_fallback=False)
234+
except Exception as e:
235+
self.assertEqual(e.response.status_code, 400)
236+
else:
237+
raise AssertionError("did not fail due to missing use_index")
238+
214239

215240
class JSONIndexSelectionTests(mango.UserDocsTests, IndexSelectionTests):
216241
@classmethod

src/mango/test/mango.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def find(
282282
update=True,
283283
executionStats=False,
284284
partition=None,
285+
allow_fallback=None,
285286
):
286287
body = {
287288
"selector": selector,
@@ -301,6 +302,8 @@ def find(
301302
body["update"] = False
302303
if executionStats == True:
303304
body["execution_stats"] = True
305+
if allow_fallback is not None:
306+
body["allow_fallback"] = allow_fallback
304307
body = json.dumps(body)
305308
if partition:
306309
ppath = "_partition/{}/".format(partition)

0 commit comments

Comments
 (0)