Skip to content

Commit fcb727c

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 aaf9005 commit fcb727c

File tree

7 files changed

+162
-10
lines changed

7 files changed

+162
-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*
@@ -1492,7 +1498,8 @@ it easier to take advantage of future improvements to query planning
14921498
"stale": false,
14931499
"update": true,
14941500
"stable": false,
1495-
"execution_stats": false
1501+
"execution_stats": false,
1502+
"allow_fallback": true
14961503
},
14971504
"limit": 2,
14981505
"skip": 0,

src/mango/src/mango_cursor.erl

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,24 @@ 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+
[];
68+
UseIndex ->
69+
[DesignId | Rest] = UseIndex,
70+
[ddoc_name(DesignId) | Rest]
71+
end,
72+
?MANGO_ERROR({invalid_index, Details})
73+
end;
5874
UserSpecifiedIndex ->
5975
create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
6076
end.
@@ -366,13 +382,21 @@ execute(#cursor{index = Idx} = Cursor, UserFun, UserAcc) ->
366382
Mod = mango_idx:cursor_mod(Idx),
367383
Mod:execute(Cursor, UserFun, UserAcc).
368384

385+
use_index(Opts) ->
386+
{use_index, UseIndex} = lists:keyfind(use_index, 1, Opts),
387+
UseIndex.
388+
389+
allow_fallback(Opts) ->
390+
{allow_fallback, AllowFallback} = lists:keyfind(allow_fallback, 1, Opts),
391+
AllowFallback.
392+
369393
maybe_filter_indexes_by_ddoc(Indexes, Opts) ->
370-
case lists:keyfind(use_index, 1, Opts) of
371-
{use_index, []} ->
394+
case use_index(Opts) of
395+
[] ->
372396
[];
373-
{use_index, [DesignId]} ->
397+
[DesignId] ->
374398
filter_indexes(Indexes, DesignId);
375-
{use_index, [DesignId, ViewName]} ->
399+
[DesignId, ViewName] ->
376400
filter_indexes(Indexes, DesignId, ViewName)
377401
end.
378402

@@ -573,7 +597,9 @@ create_test_() ->
573597
[
574598
?TDEF_FE(t_create_regular, 10),
575599
?TDEF_FE(t_create_user_specified_index, 10),
576-
?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)
577603
]
578604
}.
579605

@@ -589,7 +615,7 @@ t_create_regular(_) ->
589615
filtered_indexes => sets:from_list(FilteredIndexes),
590616
indexes_of_type => sets:from_list(IndexesOfType)
591617
},
592-
Options = [{use_index, []}],
618+
Options = [{use_index, []}, {allow_fallback, true}],
593619
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
594620
meck:expect(
595621
mango_idx,
@@ -648,7 +674,7 @@ t_create_invalid_user_specified_index(_) ->
648674
filtered_indexes => sets:from_list(UsableIndexes),
649675
indexes_of_type => sets:from_list(IndexesOfType)
650676
},
651-
Options = [{use_index, [<<"foobar">>]}],
677+
Options = [{use_index, [<<"foobar">>]}, {allow_fallback, true}],
652678
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
653679
meck:expect(
654680
mango_idx,
@@ -664,6 +690,68 @@ t_create_invalid_user_specified_index(_) ->
664690
),
665691
?assertEqual(view_cursor, create(db, selector, Options, target)).
666692

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+
667755
enhance_candidates_test() ->
668756
Candidates1 = #{index => #{reason => [], usable => true}},
669757
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
@@ -248,6 +248,7 @@ def find(
248248
update=True,
249249
executionStats=False,
250250
partition=None,
251+
allow_fallback=None,
251252
):
252253
body = {
253254
"selector": selector,
@@ -267,6 +268,8 @@ def find(
267268
body["update"] = False
268269
if executionStats == True:
269270
body["execution_stats"] = True
271+
if allow_fallback is not None:
272+
body["allow_fallback"] = allow_fallback
270273
body = json.dumps(body)
271274
if partition:
272275
ppath = "_partition/{}/".format(partition)

0 commit comments

Comments
 (0)