Skip to content

Commit ea8941e

Browse files
committed
PYTHON-1811 Deprecate running min/max queries without hint
Starting in MongoDB 4.2 a hint will be required when using min/max.
1 parent 4049b14 commit ea8941e

File tree

6 files changed

+95
-22
lines changed

6 files changed

+95
-22
lines changed

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ Changes in Version 3.8.0.dev0
8989
:meth:`pymongo.change_stream.ChangeStream.try_next` method.
9090
- Fixed a reference leak bug when splitting a batched write command based on
9191
maxWriteBatchSize or the max message size.
92+
- Deprecated running find queries that set :meth:`~pymongo.cursor.Cursor.min`
93+
and/or :meth:`~pymongo.cursor.Cursor.max` but do not also set a
94+
:meth:`~pymongo.cursor.Cursor.hint` of which index to use. The find command
95+
is expected to require a :meth:`~pymongo.cursor.Cursor.hint` when using
96+
min/max starting in MongoDB 4.2.
9297

9398
Issues Resolved
9499
...............

pymongo/collection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,11 +1368,13 @@ def find(self, *args, **kwargs):
13681368
- `min` (optional): A list of field, limit pairs specifying the
13691369
inclusive lower bound for all keys of a specific index in order.
13701370
Pass this as an alternative to calling
1371-
:meth:`~pymongo.cursor.Cursor.min` on the cursor.
1371+
:meth:`~pymongo.cursor.Cursor.min` on the cursor. ``hint`` must
1372+
also be passed to ensure the query utilizes the correct index.
13721373
- `max` (optional): A list of field, limit pairs specifying the
13731374
exclusive upper bound for all keys of a specific index in order.
13741375
Pass this as an alternative to calling
1375-
:meth:`~pymongo.cursor.Cursor.max` on the cursor.
1376+
:meth:`~pymongo.cursor.Cursor.max` on the cursor. ``hint`` must
1377+
also be passed to ensure the query utilizes the correct index.
13761378
- `comment` (optional): A string to attach to the query to help
13771379
interpret and trace the operation in the server logs and in profile
13781380
data. Pass this as an alternative to calling

pymongo/cursor.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -634,12 +634,19 @@ def max_scan(self, max_scan):
634634
return self
635635

636636
def max(self, spec):
637-
"""Adds `max` operator that specifies upper bound for specific index.
637+
"""Adds ``max`` operator that specifies upper bound for specific index.
638+
639+
When using ``max``, :meth:`~hint` should also be configured to ensure
640+
the query uses the expected index and starting in MongoDB 4.2
641+
:meth:`~hint` will be required.
638642
639643
:Parameters:
640644
- `spec`: a list of field, limit pairs specifying the exclusive
641645
upper bound for all keys of a specific index in order.
642646
647+
.. versionchanged:: 3.8
648+
Deprecated cursors that use ``max`` without a :meth:`~hint`.
649+
643650
.. versionadded:: 2.7
644651
"""
645652
if not isinstance(spec, (list, tuple)):
@@ -650,12 +657,19 @@ def max(self, spec):
650657
return self
651658

652659
def min(self, spec):
653-
"""Adds `min` operator that specifies lower bound for specific index.
660+
"""Adds ``min`` operator that specifies lower bound for specific index.
661+
662+
When using ``min``, :meth:`~hint` should also be configured to ensure
663+
the query uses the expected index and starting in MongoDB 4.2
664+
:meth:`~hint` will be required.
654665
655666
:Parameters:
656667
- `spec`: a list of field, limit pairs specifying the inclusive
657668
lower bound for all keys of a specific index in order.
658669
670+
.. versionchanged:: 3.8
671+
Deprecated cursors that use ``min`` without a :meth:`~hint`.
672+
659673
.. versionadded:: 2.7
660674
"""
661675
if not isinstance(spec, (list, tuple)):
@@ -1095,6 +1109,12 @@ def _refresh(self):
10951109
self.__session = self.__collection.database.client._ensure_session()
10961110

10971111
if self.__id is None: # Query
1112+
if (self.__min or self.__max) and not self.__hint:
1113+
warnings.warn("using a min/max query operator without "
1114+
"specifying a Cursor.hint is deprecated. A "
1115+
"hint will be required when using min/max in "
1116+
"PyMongo 4.0",
1117+
DeprecationWarning, stacklevel=3)
10981118
q = self._query_class(self.__query_flags,
10991119
self.__collection.database.name,
11001120
self.__collection.name,

test/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,12 @@ def supports_getpreverror(self):
637637
"""Does the connected server support getpreverror?"""
638638
return not (self.version.at_least(4, 1, 0) or self.is_mongos)
639639

640+
@property
641+
def requires_hint_with_min_max_queries(self):
642+
"""Does the server require a hint with min/max queries."""
643+
# Changed in SERVER-39567.
644+
return self.version.at_least(4, 1, 10)
645+
640646

641647
# Reusable client context
642648
client_context = ClientContext()

test/test_collection.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,10 +2000,13 @@ def test_min_query(self):
20002000
self.db.test.insert_many([{"x": 1}, {"x": 2}])
20012001
self.db.test.create_index("x")
20022002

2003-
self.assertEqual(1, len(list(self.db.test.find({"$min": {"x": 2},
2004-
"$query": {}}))))
2005-
self.assertEqual(2, self.db.test.find({"$min": {"x": 2},
2006-
"$query": {}})[0]["x"])
2003+
cursor = self.db.test.find({"$min": {"x": 2}, "$query": {}})
2004+
if client_context.requires_hint_with_min_max_queries:
2005+
cursor = cursor.hint("x_1")
2006+
2007+
docs = list(cursor)
2008+
self.assertEqual(1, len(docs))
2009+
self.assertEqual(2, docs[0]["x"])
20072010

20082011
def test_numerous_inserts(self):
20092012
# Ensure we don't exceed server's 1000-document batch size limit.

test/test_cursor.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sys
2222
import time
2323
import threading
24+
import warnings
2425

2526
sys.path[0:0] = [""]
2627

@@ -449,66 +450,102 @@ def test_limit(self):
449450
break
450451
self.assertRaises(InvalidOperation, a.limit, 5)
451452

453+
@ignore_deprecations # Ignore max without hint.
452454
def test_max(self):
453455
db = self.db
454456
db.test.drop()
455-
db.test.create_index([("j", ASCENDING)])
457+
j_index = [("j", ASCENDING)]
458+
db.test.create_index(j_index)
456459

457460
db.test.insert_many([{"j": j, "k": j} for j in range(10)])
458461

459-
cursor = db.test.find().max([("j", 3)])
462+
def find(max_spec, expected_index):
463+
cursor = db.test.find().max(max_spec)
464+
if client_context.requires_hint_with_min_max_queries:
465+
cursor = cursor.hint(expected_index)
466+
return cursor
467+
468+
cursor = find([("j", 3)], j_index)
460469
self.assertEqual(len(list(cursor)), 3)
461470

462471
# Tuple.
463-
cursor = db.test.find().max((("j", 3), ))
472+
cursor = find((("j", 3),), j_index)
464473
self.assertEqual(len(list(cursor)), 3)
465474

466475
# Compound index.
467-
db.test.create_index([("j", ASCENDING), ("k", ASCENDING)])
468-
cursor = db.test.find().max([("j", 3), ("k", 3)])
476+
index_keys = [("j", ASCENDING), ("k", ASCENDING)]
477+
db.test.create_index(index_keys)
478+
cursor = find([("j", 3), ("k", 3)], index_keys)
469479
self.assertEqual(len(list(cursor)), 3)
470480

471481
# Wrong order.
472-
cursor = db.test.find().max([("k", 3), ("j", 3)])
482+
cursor = find([("k", 3), ("j", 3)], index_keys)
473483
self.assertRaises(OperationFailure, list, cursor)
474484

475485
# No such index.
476-
cursor = db.test.find().max([("k", 3)])
486+
cursor = find([("k", 3)], "k")
477487
self.assertRaises(OperationFailure, list, cursor)
478488

479489
self.assertRaises(TypeError, db.test.find().max, 10)
480490
self.assertRaises(TypeError, db.test.find().max, {"j": 10})
481491

492+
@ignore_deprecations # Ignore min without hint.
482493
def test_min(self):
483494
db = self.db
484495
db.test.drop()
485-
db.test.create_index([("j", ASCENDING)])
496+
j_index = [("j", ASCENDING)]
497+
db.test.create_index(j_index)
486498

487499
db.test.insert_many([{"j": j, "k": j} for j in range(10)])
488500

489-
cursor = db.test.find().min([("j", 3)])
501+
def find(min_spec, expected_index):
502+
cursor = db.test.find().min(min_spec)
503+
if client_context.requires_hint_with_min_max_queries:
504+
cursor = cursor.hint(expected_index)
505+
return cursor
506+
507+
cursor = find([("j", 3)], j_index)
490508
self.assertEqual(len(list(cursor)), 7)
491509

492510
# Tuple.
493-
cursor = db.test.find().min((("j", 3), ))
511+
cursor = find((("j", 3),), j_index)
494512
self.assertEqual(len(list(cursor)), 7)
495513

496514
# Compound index.
497-
db.test.create_index([("j", ASCENDING), ("k", ASCENDING)])
498-
cursor = db.test.find().min([("j", 3), ("k", 3)])
515+
index_keys = [("j", ASCENDING), ("k", ASCENDING)]
516+
db.test.create_index(index_keys)
517+
cursor = find([("j", 3), ("k", 3)], index_keys)
499518
self.assertEqual(len(list(cursor)), 7)
500519

501520
# Wrong order.
502-
cursor = db.test.find().min([("k", 3), ("j", 3)])
521+
cursor = find([("k", 3), ("j", 3)], index_keys)
503522
self.assertRaises(OperationFailure, list, cursor)
504523

505524
# No such index.
506-
cursor = db.test.find().min([("k", 3)])
525+
cursor = find([("k", 3)], "k")
507526
self.assertRaises(OperationFailure, list, cursor)
508527

509528
self.assertRaises(TypeError, db.test.find().min, 10)
510529
self.assertRaises(TypeError, db.test.find().min, {"j": 10})
511530

531+
@client_context.require_version_max(4, 1, -1)
532+
def test_min_max_without_hint(self):
533+
coll = self.db.test
534+
j_index = [("j", ASCENDING)]
535+
coll.create_index(j_index)
536+
537+
with warnings.catch_warnings(record=True) as warns:
538+
warnings.simplefilter("default", DeprecationWarning)
539+
list(coll.find().min([("j", 3)]))
540+
self.assertIn('using a min/max query operator', str(warns[0]))
541+
# Ensure the warning is raised with the proper stack level.
542+
del warns[:]
543+
list(coll.find().min([("j", 3)]))
544+
self.assertIn('using a min/max query operator', str(warns[0]))
545+
del warns[:]
546+
list(coll.find().max([("j", 3)]))
547+
self.assertIn('using a min/max query operator', str(warns[0]))
548+
512549
def test_batch_size(self):
513550
db = self.db
514551
db.test.drop()

0 commit comments

Comments
 (0)