Skip to content

Commit 4388283

Browse files
authored
Merge pull request #4792 from pgj/feat/mango/find/use_index_allow_fallback
feat(`mango`): add `allow_fallback` to control falling back to other indexes on `_find`
2 parents cf99034 + c92c389 commit 4388283

File tree

7 files changed

+219
-16
lines changed

7 files changed

+219
-16
lines changed

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
.. http:post:: /{db}/_find
2020
:synopsis: Find documents within a given database.
2121

22-
Find documents using a declarative JSON querying syntax.
23-
Queries will use custom indexes, specified using the :ref:`_index <api/db/find/index>`
24-
endpoint, if available.
25-
Otherwise, they use the built-in :ref:`_all_docs <api/db/all_docs>` index, which
26-
can be arbitrarily slow.
22+
Find documents using a declarative JSON querying syntax. Queries
23+
will use custom indexes, specified using the :ref:`_index
24+
<api/db/find/index>` endpoint, if available. Otherwise, when
25+
allowed, they use the built-in :ref:`_all_docs <api/db/all_docs>`
26+
index, which can be arbitrarily slow.
2727

2828
:param db: Database name
2929

@@ -50,6 +50,14 @@
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 another valid index. This can happen on running a query
55+
with an index specified by ``use_index`` which is not deemed
56+
usable, or when only the built-in :ref:`_all_docs
57+
<api/db/all_docs>` index would be picked in lack of indexes
58+
available to support the query. Disabling this fallback logic
59+
causes the endpoint immediately return an error in such cases.
60+
Default is ``true``. *Optional*
5361
:<json boolean conflicts: Include conflicted documents if ``true``.
5462
Intended use is to easily find conflicted documents, without an
5563
index or view. Default is ``false``. *Optional*
@@ -1498,7 +1506,8 @@ it easier to take advantage of future improvements to query planning
14981506
"stale": false,
14991507
"update": true,
15001508
"stable": false,
1501-
"execution_stats": false
1509+
"execution_stats": false,
1510+
"allow_fallback": true
15021511
},
15031512
"limit": 2,
15041513
"skip": 0,

src/mango/src/mango_cursor.erl

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,25 @@
5151
create(Db, Selector0, Opts, Kind) ->
5252
Selector = mango_selector:normalize(Selector0),
5353
{UsableIndexes, Trace} = mango_idx:get_usable_indexes(Db, Selector, Opts, Kind),
54+
UseIndex = use_index(Opts),
5455
case maybe_filter_indexes_by_ddoc(UsableIndexes, Opts) of
5556
[] ->
56-
% use_index doesn't match a valid index - fall back to a valid one
57-
create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
57+
% use_index doesn't match a valid index - determine how
58+
% this shall be handled by the further settings
59+
case allow_fallback(Opts) orelse (UseIndex == [] andalso length(UsableIndexes) > 1) of
60+
true ->
61+
% fall back to a valid index
62+
create_cursor(Db, {UsableIndexes, Trace}, Selector, Opts);
63+
false ->
64+
% no usable index but all_docs, no fallback allowed: return an error
65+
Details =
66+
case UseIndex of
67+
[] -> [];
68+
[DesignId] -> [ddoc_name(DesignId)];
69+
[DesignId, ViewName] -> [ddoc_name(DesignId), ViewName]
70+
end,
71+
?MANGO_ERROR({invalid_index, Details})
72+
end;
5873
UserSpecifiedIndex ->
5974
create_cursor(Db, {UserSpecifiedIndex, Trace}, Selector, Opts)
6075
end.
@@ -366,13 +381,21 @@ execute(#cursor{index = Idx} = Cursor, UserFun, UserAcc) ->
366381
Mod = mango_idx:cursor_mod(Idx),
367382
Mod:execute(Cursor, UserFun, UserAcc).
368383

384+
use_index(Opts) ->
385+
{use_index, UseIndex} = lists:keyfind(use_index, 1, Opts),
386+
UseIndex.
387+
388+
allow_fallback(Opts) ->
389+
{allow_fallback, AllowFallback} = lists:keyfind(allow_fallback, 1, Opts),
390+
AllowFallback.
391+
369392
maybe_filter_indexes_by_ddoc(Indexes, Opts) ->
370-
case lists:keyfind(use_index, 1, Opts) of
371-
{use_index, []} ->
393+
case use_index(Opts) of
394+
[] ->
372395
[];
373-
{use_index, [DesignId]} ->
396+
[DesignId] ->
374397
filter_indexes(Indexes, DesignId);
375-
{use_index, [DesignId, ViewName]} ->
398+
[DesignId, ViewName] ->
376399
filter_indexes(Indexes, DesignId, ViewName)
377400
end.
378401

@@ -575,7 +598,9 @@ create_test_() ->
575598
[
576599
?TDEF_FE(t_create_regular, 10),
577600
?TDEF_FE(t_create_user_specified_index, 10),
578-
?TDEF_FE(t_create_invalid_user_specified_index, 10)
601+
?TDEF_FE(t_create_invalid_user_specified_index, 10),
602+
?TDEF_FE(t_create_invalid_user_specified_index_no_fallback, 10),
603+
?TDEF_FE(t_create_no_suitable_index_no_fallback, 10)
579604
]
580605
}.
581606

@@ -591,7 +616,7 @@ t_create_regular(_) ->
591616
filtered_indexes => sets:from_list(FilteredIndexes),
592617
indexes_of_type => sets:from_list(IndexesOfType)
593618
},
594-
Options = [{use_index, []}],
619+
Options = [{use_index, []}, {allow_fallback, true}],
595620
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
596621
meck:expect(
597622
mango_idx,
@@ -650,7 +675,7 @@ t_create_invalid_user_specified_index(_) ->
650675
filtered_indexes => sets:from_list(UsableIndexes),
651676
indexes_of_type => sets:from_list(IndexesOfType)
652677
},
653-
Options = [{use_index, [<<"foobar">>]}],
678+
Options = [{use_index, [<<"foobar">>]}, {allow_fallback, true}],
654679
meck:expect(mango_selector, normalize, [selector], meck:val(normalized_selector)),
655680
meck:expect(
656681
mango_idx,
@@ -666,6 +691,64 @@ t_create_invalid_user_specified_index(_) ->
666691
),
667692
?assertEqual(view_cursor, create(db, selector, Options, target)).
668693

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

src/mango/src/mango_error.erl

Lines changed: 28 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,
@@ -231,6 +253,12 @@ info(mango_opts, {invalid_bulk_docs, Val}) ->
231253
[Val]
232254
)
233255
};
256+
info(mango_opts, {invalid_index_name, Val}) ->
257+
{
258+
400,
259+
<<"invalid_index_name">>,
260+
fmt("Invalid index name: ~w", [Val])
261+
};
234262
info(mango_opts, {invalid_ejson, Val}) ->
235263
{
236264
400,

src/mango/src/mango_opts.erl

Lines changed: 7 additions & 1 deletion
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).
@@ -256,7 +262,7 @@ validate_bulk_docs(Else) ->
256262
?MANGO_ERROR({invalid_bulk_docs, Else}).
257263

258264
validate_use_index(IndexName) when is_binary(IndexName) ->
259-
case binary:split(IndexName, <<"/">>) of
265+
case binary:split(IndexName, <<"/">>, [global]) of
260266
[DesignId] ->
261267
{ok, [DesignId]};
262268
[<<"_design">>, DesignId] ->

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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,79 @@ def test_explain_sort_reverse(self):
211211
)
212212
self.assertEqual(resp_explain["index"]["type"], "json")
213213

214+
def test_use_index_with_invalid_name(self):
215+
for index in ["foo/bar/baz", ["foo", "bar", "baz"]]:
216+
with self.subTest(index=index):
217+
try:
218+
self.db.find({"manager": True}, use_index=index)
219+
except Exception as e:
220+
self.assertEqual(e.response.status_code, 400)
221+
else:
222+
raise AssertionError("did not fail on invalid index name")
223+
224+
def test_use_index_without_fallback(self):
225+
with self.subTest(use_index="valid", fallback_available=None):
226+
docs = self.db.find(
227+
{"manager": True}, use_index="manager", allow_fallback=False
228+
)
229+
assert len(docs) > 0
230+
231+
with self.subTest(use_index="invalid", fallback_available=True):
232+
try:
233+
self.db.find(
234+
{"manager": True}, use_index="invalid", allow_fallback=False
235+
)
236+
except Exception as e:
237+
self.assertEqual(e.response.status_code, 400)
238+
else:
239+
raise AssertionError("did not fail on invalid index for use_index")
240+
241+
with self.subTest(use_index="empty", fallback_available=True):
242+
try:
243+
docs = self.db.find(
244+
{"manager": True}, use_index=[], allow_fallback=False
245+
)
246+
assert len(docs) > 0
247+
except Exception as e:
248+
raise AssertionError(
249+
"fail due to missing use_index with suitable indexes"
250+
)
251+
252+
with self.subTest(use_index="empty", fallback_available=False):
253+
try:
254+
self.db.find({"company": "foobar"}, use_index=[], allow_fallback=False)
255+
except Exception as e:
256+
self.assertEqual(e.response.status_code, 400)
257+
else:
258+
raise AssertionError(
259+
"did not fail due to missing use_index without suitable indexes"
260+
)
261+
262+
with self.subTest(use_index="invalid", fallback_available=False):
263+
try:
264+
self.db.find(
265+
{"company": "foobar"}, use_index="invalid", allow_fallback=False
266+
)
267+
except Exception as e:
268+
self.assertEqual(e.response.status_code, 400)
269+
else:
270+
raise AssertionError("did not fail on invalid index for use_index")
271+
272+
def test_index_without_fallback(self):
273+
try:
274+
docs = self.db.find({"manager": True}, allow_fallback=False)
275+
assert len(docs) > 0
276+
except Exception as e:
277+
raise AssertionError("fail on usable indexes")
278+
279+
def test_no_index_without_fallback(self):
280+
try:
281+
self.db.find({"company": "foobar"}, allow_fallback=False)
282+
except Exception as e:
283+
self.assertEqual(e.response.status_code, 400)
284+
else:
285+
raise AssertionError("did not fail on no usable indexes")
286+
214287

215288
class JSONIndexSelectionTests(mango.UserDocsTests, IndexSelectionTests):
216289
@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)