PYTHON-1811 Deprecate running min/max queries without hint

Starting in MongoDB 4.2 a hint will be required when using min/max.
This commit is contained in:
Shane Harvey 2019-04-12 17:13:10 -07:00
parent 4049b1493a
commit ea8941ef5d
6 changed files with 95 additions and 22 deletions

View File

@ -89,6 +89,11 @@ Changes in Version 3.8.0.dev0
:meth:`pymongo.change_stream.ChangeStream.try_next` method.
- Fixed a reference leak bug when splitting a batched write command based on
maxWriteBatchSize or the max message size.
- Deprecated running find queries that set :meth:`~pymongo.cursor.Cursor.min`
and/or :meth:`~pymongo.cursor.Cursor.max` but do not also set a
:meth:`~pymongo.cursor.Cursor.hint` of which index to use. The find command
is expected to require a :meth:`~pymongo.cursor.Cursor.hint` when using
min/max starting in MongoDB 4.2.
Issues Resolved
...............

View File

@ -1368,11 +1368,13 @@ class Collection(common.BaseObject):
- `min` (optional): A list of field, limit pairs specifying the
inclusive lower bound for all keys of a specific index in order.
Pass this as an alternative to calling
:meth:`~pymongo.cursor.Cursor.min` on the cursor.
:meth:`~pymongo.cursor.Cursor.min` on the cursor. ``hint`` must
also be passed to ensure the query utilizes the correct index.
- `max` (optional): A list of field, limit pairs specifying the
exclusive upper bound for all keys of a specific index in order.
Pass this as an alternative to calling
:meth:`~pymongo.cursor.Cursor.max` on the cursor.
:meth:`~pymongo.cursor.Cursor.max` on the cursor. ``hint`` must
also be passed to ensure the query utilizes the correct index.
- `comment` (optional): A string to attach to the query to help
interpret and trace the operation in the server logs and in profile
data. Pass this as an alternative to calling

View File

@ -634,12 +634,19 @@ class Cursor(object):
return self
def max(self, spec):
"""Adds `max` operator that specifies upper bound for specific index.
"""Adds ``max`` operator that specifies upper bound for specific index.
When using ``max``, :meth:`~hint` should also be configured to ensure
the query uses the expected index and starting in MongoDB 4.2
:meth:`~hint` will be required.
:Parameters:
- `spec`: a list of field, limit pairs specifying the exclusive
upper bound for all keys of a specific index in order.
.. versionchanged:: 3.8
Deprecated cursors that use ``max`` without a :meth:`~hint`.
.. versionadded:: 2.7
"""
if not isinstance(spec, (list, tuple)):
@ -650,12 +657,19 @@ class Cursor(object):
return self
def min(self, spec):
"""Adds `min` operator that specifies lower bound for specific index.
"""Adds ``min`` operator that specifies lower bound for specific index.
When using ``min``, :meth:`~hint` should also be configured to ensure
the query uses the expected index and starting in MongoDB 4.2
:meth:`~hint` will be required.
:Parameters:
- `spec`: a list of field, limit pairs specifying the inclusive
lower bound for all keys of a specific index in order.
.. versionchanged:: 3.8
Deprecated cursors that use ``min`` without a :meth:`~hint`.
.. versionadded:: 2.7
"""
if not isinstance(spec, (list, tuple)):
@ -1095,6 +1109,12 @@ class Cursor(object):
self.__session = self.__collection.database.client._ensure_session()
if self.__id is None: # Query
if (self.__min or self.__max) and not self.__hint:
warnings.warn("using a min/max query operator without "
"specifying a Cursor.hint is deprecated. A "
"hint will be required when using min/max in "
"PyMongo 4.0",
DeprecationWarning, stacklevel=3)
q = self._query_class(self.__query_flags,
self.__collection.database.name,
self.__collection.name,

View File

@ -637,6 +637,12 @@ class ClientContext(object):
"""Does the connected server support getpreverror?"""
return not (self.version.at_least(4, 1, 0) or self.is_mongos)
@property
def requires_hint_with_min_max_queries(self):
"""Does the server require a hint with min/max queries."""
# Changed in SERVER-39567.
return self.version.at_least(4, 1, 10)
# Reusable client context
client_context = ClientContext()

View File

@ -2000,10 +2000,13 @@ class TestCollection(IntegrationTest):
self.db.test.insert_many([{"x": 1}, {"x": 2}])
self.db.test.create_index("x")
self.assertEqual(1, len(list(self.db.test.find({"$min": {"x": 2},
"$query": {}}))))
self.assertEqual(2, self.db.test.find({"$min": {"x": 2},
"$query": {}})[0]["x"])
cursor = self.db.test.find({"$min": {"x": 2}, "$query": {}})
if client_context.requires_hint_with_min_max_queries:
cursor = cursor.hint("x_1")
docs = list(cursor)
self.assertEqual(1, len(docs))
self.assertEqual(2, docs[0]["x"])
def test_numerous_inserts(self):
# Ensure we don't exceed server's 1000-document batch size limit.

View File

@ -21,6 +21,7 @@ import re
import sys
import time
import threading
import warnings
sys.path[0:0] = [""]
@ -449,66 +450,102 @@ class TestCursor(IntegrationTest):
break
self.assertRaises(InvalidOperation, a.limit, 5)
@ignore_deprecations # Ignore max without hint.
def test_max(self):
db = self.db
db.test.drop()
db.test.create_index([("j", ASCENDING)])
j_index = [("j", ASCENDING)]
db.test.create_index(j_index)
db.test.insert_many([{"j": j, "k": j} for j in range(10)])
cursor = db.test.find().max([("j", 3)])
def find(max_spec, expected_index):
cursor = db.test.find().max(max_spec)
if client_context.requires_hint_with_min_max_queries:
cursor = cursor.hint(expected_index)
return cursor
cursor = find([("j", 3)], j_index)
self.assertEqual(len(list(cursor)), 3)
# Tuple.
cursor = db.test.find().max((("j", 3), ))
cursor = find((("j", 3),), j_index)
self.assertEqual(len(list(cursor)), 3)
# Compound index.
db.test.create_index([("j", ASCENDING), ("k", ASCENDING)])
cursor = db.test.find().max([("j", 3), ("k", 3)])
index_keys = [("j", ASCENDING), ("k", ASCENDING)]
db.test.create_index(index_keys)
cursor = find([("j", 3), ("k", 3)], index_keys)
self.assertEqual(len(list(cursor)), 3)
# Wrong order.
cursor = db.test.find().max([("k", 3), ("j", 3)])
cursor = find([("k", 3), ("j", 3)], index_keys)
self.assertRaises(OperationFailure, list, cursor)
# No such index.
cursor = db.test.find().max([("k", 3)])
cursor = find([("k", 3)], "k")
self.assertRaises(OperationFailure, list, cursor)
self.assertRaises(TypeError, db.test.find().max, 10)
self.assertRaises(TypeError, db.test.find().max, {"j": 10})
@ignore_deprecations # Ignore min without hint.
def test_min(self):
db = self.db
db.test.drop()
db.test.create_index([("j", ASCENDING)])
j_index = [("j", ASCENDING)]
db.test.create_index(j_index)
db.test.insert_many([{"j": j, "k": j} for j in range(10)])
cursor = db.test.find().min([("j", 3)])
def find(min_spec, expected_index):
cursor = db.test.find().min(min_spec)
if client_context.requires_hint_with_min_max_queries:
cursor = cursor.hint(expected_index)
return cursor
cursor = find([("j", 3)], j_index)
self.assertEqual(len(list(cursor)), 7)
# Tuple.
cursor = db.test.find().min((("j", 3), ))
cursor = find((("j", 3),), j_index)
self.assertEqual(len(list(cursor)), 7)
# Compound index.
db.test.create_index([("j", ASCENDING), ("k", ASCENDING)])
cursor = db.test.find().min([("j", 3), ("k", 3)])
index_keys = [("j", ASCENDING), ("k", ASCENDING)]
db.test.create_index(index_keys)
cursor = find([("j", 3), ("k", 3)], index_keys)
self.assertEqual(len(list(cursor)), 7)
# Wrong order.
cursor = db.test.find().min([("k", 3), ("j", 3)])
cursor = find([("k", 3), ("j", 3)], index_keys)
self.assertRaises(OperationFailure, list, cursor)
# No such index.
cursor = db.test.find().min([("k", 3)])
cursor = find([("k", 3)], "k")
self.assertRaises(OperationFailure, list, cursor)
self.assertRaises(TypeError, db.test.find().min, 10)
self.assertRaises(TypeError, db.test.find().min, {"j": 10})
@client_context.require_version_max(4, 1, -1)
def test_min_max_without_hint(self):
coll = self.db.test
j_index = [("j", ASCENDING)]
coll.create_index(j_index)
with warnings.catch_warnings(record=True) as warns:
warnings.simplefilter("default", DeprecationWarning)
list(coll.find().min([("j", 3)]))
self.assertIn('using a min/max query operator', str(warns[0]))
# Ensure the warning is raised with the proper stack level.
del warns[:]
list(coll.find().min([("j", 3)]))
self.assertIn('using a min/max query operator', str(warns[0]))
del warns[:]
list(coll.find().max([("j", 3)]))
self.assertIn('using a min/max query operator', str(warns[0]))
def test_batch_size(self):
db = self.db
db.test.drop()