Skip to content

Commit d3117ce

Browse files
authored
PYTHON-3280 Support for Range Indexes (#1140)
1 parent ec07401 commit d3117ce

17 files changed

+759
-23
lines changed

pymongo/encryption.py

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from pymongo import _csot
4242
from pymongo.cursor import Cursor
4343
from pymongo.daemon import _spawn_daemon
44-
from pymongo.encryption_options import AutoEncryptionOpts
44+
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
4545
from pymongo.errors import (
4646
ConfigurationError,
4747
EncryptionError,
@@ -416,6 +416,14 @@ class Algorithm(str, enum.Enum):
416416
417417
.. versionadded:: 4.2
418418
"""
419+
RANGEPREVIEW = "RangePreview"
420+
"""RangePreview.
421+
422+
.. note:: Support for Range queries is in beta.
423+
Backwards-breaking changes may be made before the final release.
424+
425+
.. versionadded:: 4.4
426+
"""
419427

420428

421429
class QueryType(str, enum.Enum):
@@ -430,6 +438,9 @@ class QueryType(str, enum.Enum):
430438
EQUALITY = "equality"
431439
"""Used to encrypt a value for an equality query."""
432440

441+
RANGEPREVIEW = "rangePreview"
442+
"""Used to encrypt a value for a range query."""
443+
433444

434445
class ClientEncryption(Generic[_DocumentType]):
435446
"""Explicit client-side field level encryption."""
@@ -627,6 +638,45 @@ def create_data_key(
627638
key_material=key_material,
628639
)
629640

641+
def _encrypt_helper(
642+
self,
643+
value,
644+
algorithm,
645+
key_id=None,
646+
key_alt_name=None,
647+
query_type=None,
648+
contention_factor=None,
649+
range_opts=None,
650+
is_expression=False,
651+
):
652+
self._check_closed()
653+
if key_id is not None and not (
654+
isinstance(key_id, Binary) and key_id.subtype == UUID_SUBTYPE
655+
):
656+
raise TypeError("key_id must be a bson.binary.Binary with subtype 4")
657+
658+
doc = encode(
659+
{"v": value},
660+
codec_options=self._codec_options,
661+
)
662+
if range_opts:
663+
range_opts = encode(
664+
range_opts.document,
665+
codec_options=self._codec_options,
666+
)
667+
with _wrap_encryption_errors():
668+
encrypted_doc = self._encryption.encrypt(
669+
value=doc,
670+
algorithm=algorithm,
671+
key_id=key_id,
672+
key_alt_name=key_alt_name,
673+
query_type=query_type,
674+
contention_factor=contention_factor,
675+
range_opts=range_opts,
676+
is_expression=is_expression,
677+
)
678+
return decode(encrypted_doc)["v"] # type: ignore[index]
679+
630680
def encrypt(
631681
self,
632682
value: Any,
@@ -635,6 +685,7 @@ def encrypt(
635685
key_alt_name: Optional[str] = None,
636686
query_type: Optional[str] = None,
637687
contention_factor: Optional[int] = None,
688+
range_opts: Optional[RangeOpts] = None,
638689
) -> Binary:
639690
"""Encrypt a BSON value with a given key and algorithm.
640691
@@ -655,10 +706,10 @@ def encrypt(
655706
when the algorithm is :attr:`Algorithm.INDEXED`. An integer value
656707
*must* be given when the :attr:`Algorithm.INDEXED` algorithm is
657708
used.
709+
- `range_opts`: **(BETA)** An instance of RangeOpts.
658710
659-
.. note:: `query_type` and `contention_factor` are part of the
660-
Queryable Encryption beta. Backwards-breaking changes may be made before the
661-
final release.
711+
.. note:: `query_type`, `contention_factor` and `range_opts` are part of the Queryable Encryption beta.
712+
Backwards-breaking changes may be made before the final release.
662713
663714
:Returns:
664715
The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
@@ -667,23 +718,66 @@ def encrypt(
667718
Added the `query_type` and `contention_factor` parameters.
668719
669720
"""
670-
self._check_closed()
671-
if key_id is not None and not (
672-
isinstance(key_id, Binary) and key_id.subtype == UUID_SUBTYPE
673-
):
674-
raise TypeError("key_id must be a bson.binary.Binary with subtype 4")
721+
return self._encrypt_helper(
722+
value=value,
723+
algorithm=algorithm,
724+
key_id=key_id,
725+
key_alt_name=key_alt_name,
726+
query_type=query_type,
727+
contention_factor=contention_factor,
728+
range_opts=range_opts,
729+
is_expression=False,
730+
)
675731

676-
doc = encode({"v": value}, codec_options=self._codec_options)
677-
with _wrap_encryption_errors():
678-
encrypted_doc = self._encryption.encrypt(
679-
doc,
680-
algorithm,
681-
key_id=key_id,
682-
key_alt_name=key_alt_name,
683-
query_type=query_type,
684-
contention_factor=contention_factor,
685-
)
686-
return decode(encrypted_doc)["v"] # type: ignore[index]
732+
def encrypt_expression(
733+
self,
734+
expression: Mapping[str, Any],
735+
algorithm: str,
736+
key_id: Optional[Binary] = None,
737+
key_alt_name: Optional[str] = None,
738+
query_type: Optional[str] = None,
739+
contention_factor: Optional[int] = None,
740+
range_opts: Optional[RangeOpts] = None,
741+
) -> RawBSONDocument:
742+
"""Encrypt a BSON expression with a given key and algorithm.
743+
744+
Note that exactly one of ``key_id`` or ``key_alt_name`` must be
745+
provided.
746+
747+
:Parameters:
748+
- `expression`: **(BETA)** The BSON aggregate or match expression to encrypt.
749+
- `algorithm` (string): The encryption algorithm to use. See
750+
:class:`Algorithm` for some valid options.
751+
- `key_id`: Identifies a data key by ``_id`` which must be a
752+
:class:`~bson.binary.Binary` with subtype 4 (
753+
:attr:`~bson.binary.UUID_SUBTYPE`).
754+
- `key_alt_name`: Identifies a key vault document by 'keyAltName'.
755+
- `query_type` (str): **(BETA)** The query type to execute. See
756+
:class:`QueryType` for valid options.
757+
- `contention_factor` (int): **(BETA)** The contention factor to use
758+
when the algorithm is :attr:`Algorithm.INDEXED`. An integer value
759+
*must* be given when the :attr:`Algorithm.INDEXED` algorithm is
760+
used.
761+
- `range_opts`: **(BETA)** An instance of RangeOpts.
762+
763+
.. note:: Support for range queries is in beta.
764+
Backwards-breaking changes may be made before the final release.
765+
766+
:Returns:
767+
The encrypted expression, a :class:`~bson.RawBSONDocument`.
768+
769+
.. versionadded:: 4.4
770+
"""
771+
return self._encrypt_helper(
772+
value=expression,
773+
algorithm=algorithm,
774+
key_id=key_id,
775+
key_alt_name=key_alt_name,
776+
query_type=query_type,
777+
contention_factor=contention_factor,
778+
range_opts=range_opts,
779+
is_expression=True,
780+
)
687781

688782
def decrypt(self, value: Binary) -> Any:
689783
"""Decrypt an encrypted value.

pymongo/encryption_options.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
_HAVE_PYMONGOCRYPT = True
2323
except ImportError:
2424
_HAVE_PYMONGOCRYPT = False
25-
25+
from bson import int64
2626
from pymongo.common import validate_is_mapping
2727
from pymongo.errors import ConfigurationError
2828
from pymongo.uri_parser import _parse_kms_tls_options
@@ -219,3 +219,45 @@ def __init__(
219219
# Maps KMS provider name to a SSLContext.
220220
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)
221221
self._bypass_query_analysis = bypass_query_analysis
222+
223+
224+
class RangeOpts:
225+
"""Options to configure encrypted queries using the rangePreview algorithm."""
226+
227+
def __init__(
228+
self,
229+
sparsity: int,
230+
min: Optional[Any] = None,
231+
max: Optional[Any] = None,
232+
precision: Optional[int] = None,
233+
) -> None:
234+
"""Options to configure encrypted queries using the rangePreview algorithm.
235+
236+
.. note:: Support for Range queries is in beta.
237+
Backwards-breaking changes may be made before the final release.
238+
239+
:Parameters:
240+
- `sparsity`: An integer.
241+
- `min`: A BSON scalar value corresponding to the type being queried.
242+
- `max`: A BSON scalar value corresponding to the type being queried.
243+
- `precision`: An integer, may only be set for double or decimal128 types.
244+
245+
.. versionadded:: 4.4
246+
"""
247+
self.min = min
248+
self.max = max
249+
self.sparsity = sparsity
250+
self.precision = precision
251+
252+
@property
253+
def document(self) -> Mapping[str, Any]:
254+
doc = {}
255+
for k, v in [
256+
("sparsity", int64.Int64(self.sparsity)),
257+
("precision", self.precision),
258+
("min", self.min),
259+
("max", self.max),
260+
]:
261+
if v is not None:
262+
doc[k] = v
263+
return doc
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"escCollection": "enxcol_.default.esc",
3+
"eccCollection": "enxcol_.default.ecc",
4+
"ecocCollection": "enxcol_.default.ecoc",
5+
"fields": [
6+
{
7+
"keyId": {
8+
"$binary": {
9+
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
10+
"subType": "04"
11+
}
12+
},
13+
"path": "encryptedDate",
14+
"bsonType": "date",
15+
"queries": {
16+
"queryType": "rangePreview",
17+
"contention": {
18+
"$numberLong": "0"
19+
},
20+
"sparsity": {
21+
"$numberLong": "1"
22+
},
23+
"min": {
24+
"$date": {
25+
"$numberLong": "0"
26+
}
27+
},
28+
"max": {
29+
"$date": {
30+
"$numberLong": "200"
31+
}
32+
}
33+
}
34+
}
35+
]
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"escCollection": "enxcol_.default.esc",
3+
"eccCollection": "enxcol_.default.ecc",
4+
"ecocCollection": "enxcol_.default.ecoc",
5+
"fields": [
6+
{
7+
"keyId": {
8+
"$binary": {
9+
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
10+
"subType": "04"
11+
}
12+
},
13+
"path": "encryptedDecimal",
14+
"bsonType": "decimal",
15+
"queries": {
16+
"queryType": "rangePreview",
17+
"contention": {
18+
"$numberLong": "0"
19+
},
20+
"sparsity": {
21+
"$numberLong": "1"
22+
}
23+
}
24+
}
25+
]
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"escCollection": "enxcol_.default.esc",
3+
"eccCollection": "enxcol_.default.ecc",
4+
"ecocCollection": "enxcol_.default.ecoc",
5+
"fields": [
6+
{
7+
"keyId": {
8+
"$binary": {
9+
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
10+
"subType": "04"
11+
}
12+
},
13+
"path": "encryptedDecimalPrecision",
14+
"bsonType": "decimal",
15+
"queries": {
16+
"queryType": "rangePreview",
17+
"contention": {
18+
"$numberLong": "0"
19+
},
20+
"sparsity": {
21+
"$numberLong": "1"
22+
},
23+
"min": {
24+
"$numberDecimal": "0.0"
25+
},
26+
"max": {
27+
"$numberDecimal": "200.0"
28+
},
29+
"precision": {
30+
"$numberInt": "2"
31+
}
32+
}
33+
}
34+
]
35+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"escCollection": "enxcol_.default.esc",
3+
"eccCollection": "enxcol_.default.ecc",
4+
"ecocCollection": "enxcol_.default.ecoc",
5+
"fields": [
6+
{
7+
"keyId": {
8+
"$binary": {
9+
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
10+
"subType": "04"
11+
}
12+
},
13+
"path": "encryptedDouble",
14+
"bsonType": "double",
15+
"queries": {
16+
"queryType": "rangePreview",
17+
"contention": {
18+
"$numberLong": "0"
19+
},
20+
"sparsity": {
21+
"$numberLong": "1"
22+
}
23+
}
24+
}
25+
]
26+
}

0 commit comments

Comments
 (0)