Skip to content

Commit 6935327

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. (cherry picked from commit ea8941e)
1 parent 50b905e commit 6935327

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
@@ -47,6 +47,11 @@ Changes in Version 3.8.0.dev0
4747
:meth:`pymongo.change_stream.ChangeStream.try_next` method.
4848
- Fixed a reference leak bug when splitting a batched write command based on
4949
maxWriteBatchSize or the max message size.
50+
- Deprecated running find queries that set :meth:`~pymongo.cursor.Cursor.min`
51+
and/or :meth:`~pymongo.cursor.Cursor.max` but do not also set a
52+
:meth:`~pymongo.cursor.Cursor.hint` of which index to use. The find command
53+
is expected to require a :meth:`~pymongo.cursor.Cursor.hint` when using
54+
min/max starting in MongoDB 4.2.
5055

5156
Issues Resolved
5257
...............

pymongo/collection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,11 +1367,13 @@ def find(self, *args, **kwargs):
13671367
- `min` (optional): A list of field, limit pairs specifying the
13681368
inclusive lower bound for all keys of a specific index in order.
13691369
Pass this as an alternative to calling
1370-
:meth:`~pymongo.cursor.Cursor.min` on the cursor.
1370+
:meth:`~pymongo.cursor.Cursor.min` on the cursor. ``hint`` must
1371+
also be passed to ensure the query utilizes the correct index.
13711372
- `max` (optional): A list of field, limit pairs specifying the
13721373
exclusive upper bound for all keys of a specific index in order.
13731374
Pass this as an alternative to calling
1374-
:meth:`~pymongo.cursor.Cursor.max` on the cursor.
1375+
:meth:`~pymongo.cursor.Cursor.max` on the cursor. ``hint`` must
1376+
also be passed to ensure the query utilizes the correct index.
13751377
- `comment` (optional): A string to attach to the query to help
13761378
interpret and trace the operation in the server logs and in profile
13771379
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
@@ -604,6 +604,12 @@ def supports_getpreverror(self):
604604
"""Does the connected server support getpreverror?"""
605605
return not (self.version.at_least(4, 1, 0) or self.is_mongos)
606606

607+
@property
608+
def requires_hint_with_min_max_queries(self):
609+
"""Does the server require a hint with min/max queries."""
610+
# Changed in SERVER-39567.
611+
return self.version.at_least(4, 1, 10)
612+
607613

608614
# Reusable client context
609615
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)