Skip to content

Commit d7316af

Browse files
authored
PYTHON-5328 CRUD Support in Driver for Prefix/Suffix/Substring Indexes (#2521)
1 parent 7580309 commit d7316af

File tree

9 files changed

+710
-7
lines changed

9 files changed

+710
-7
lines changed

doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Changes in Version 4.15.0 (XXXX/XX/XX)
44
--------------------------------------
55
PyMongo 4.15 brings a number of changes including:
66

7+
- Added :class:`~pymongo.encryption_options.TextOpts`,
8+
:attr:`~pymongo.encryption.Algorithm.TEXTPREVIEW`,
9+
:attr:`~pymongo.encryption.QueryType.PREFIXPREVIEW`,
10+
:attr:`~pymongo.encryption.QueryType.SUFFIXPREVIEW`,
11+
:attr:`~pymongo.encryption.QueryType.SUBSTRINGPREVIEW`,
12+
as part of the experimental Queryable Encryption text queries beta.
13+
``pymongocrypt>=1.16`` is required for text query support.
714
- Added :class:`bson.decimal128.DecimalEncoder` and :class:`bson.decimal128.DecimalDecoder`
815
to support encoding and decoding of BSON Decimal128 values to decimal.Decimal values using the TypeRegistry API.
916

pymongo/asynchronous/encryption.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
from pymongo.asynchronous.pool import AsyncBaseConnection
6868
from pymongo.common import CONNECT_TIMEOUT
6969
from pymongo.daemon import _spawn_daemon
70-
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
70+
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
7171
from pymongo.errors import (
7272
ConfigurationError,
7373
EncryptedCollectionError,
@@ -516,6 +516,11 @@ class Algorithm(str, enum.Enum):
516516
517517
.. versionadded:: 4.4
518518
"""
519+
TEXTPREVIEW = "TextPreview"
520+
"""**BETA** - TextPreview.
521+
522+
.. versionadded:: 4.15
523+
"""
519524

520525

521526
class QueryType(str, enum.Enum):
@@ -541,6 +546,24 @@ class QueryType(str, enum.Enum):
541546
.. versionadded:: 4.4
542547
"""
543548

549+
PREFIXPREVIEW = "prefixPreview"
550+
"""**BETA** - Used to encrypt a value for a prefixPreview query.
551+
552+
.. versionadded:: 4.15
553+
"""
554+
555+
SUFFIXPREVIEW = "suffixPreview"
556+
"""**BETA** - Used to encrypt a value for a suffixPreview query.
557+
558+
.. versionadded:: 4.15
559+
"""
560+
561+
SUBSTRINGPREVIEW = "substringPreview"
562+
"""**BETA** - Used to encrypt a value for a substringPreview query.
563+
564+
.. versionadded:: 4.15
565+
"""
566+
544567

545568
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
546569
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
@@ -876,6 +899,7 @@ async def _encrypt_helper(
876899
contention_factor: Optional[int] = None,
877900
range_opts: Optional[RangeOpts] = None,
878901
is_expression: bool = False,
902+
text_opts: Optional[TextOpts] = None,
879903
) -> Any:
880904
self._check_closed()
881905
if isinstance(key_id, uuid.UUID):
@@ -895,6 +919,12 @@ async def _encrypt_helper(
895919
range_opts.document,
896920
codec_options=self._codec_options,
897921
)
922+
text_opts_bytes = None
923+
if text_opts:
924+
text_opts_bytes = encode(
925+
text_opts.document,
926+
codec_options=self._codec_options,
927+
)
898928
with _wrap_encryption_errors():
899929
encrypted_doc = await self._encryption.encrypt(
900930
value=doc,
@@ -905,6 +935,7 @@ async def _encrypt_helper(
905935
contention_factor=contention_factor,
906936
range_opts=range_opts_bytes,
907937
is_expression=is_expression,
938+
text_opts=text_opts_bytes,
908939
)
909940
return decode(encrypted_doc)["v"]
910941

@@ -917,6 +948,7 @@ async def encrypt(
917948
query_type: Optional[str] = None,
918949
contention_factor: Optional[int] = None,
919950
range_opts: Optional[RangeOpts] = None,
951+
text_opts: Optional[TextOpts] = None,
920952
) -> Binary:
921953
"""Encrypt a BSON value with a given key and algorithm.
922954
@@ -937,9 +969,14 @@ async def encrypt(
937969
used.
938970
:param range_opts: Index options for `range` queries. See
939971
:class:`RangeOpts` for some valid options.
972+
:param text_opts: Index options for `textPreview` queries. See
973+
:class:`TextOpts` for some valid options.
940974
941975
:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
942976
977+
.. versionchanged:: 4.9
978+
Added the `text_opts` parameter.
979+
943980
.. versionchanged:: 4.9
944981
Added the `range_opts` parameter.
945982
@@ -960,6 +997,7 @@ async def encrypt(
960997
contention_factor=contention_factor,
961998
range_opts=range_opts,
962999
is_expression=False,
1000+
text_opts=text_opts,
9631001
),
9641002
)
9651003

pymongo/encryption_options.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"""
1919
from __future__ import annotations
2020

21-
from typing import TYPE_CHECKING, Any, Mapping, Optional
21+
from typing import TYPE_CHECKING, Any, Mapping, Optional, TypedDict
2222

2323
from pymongo.uri_parser_shared import _parse_kms_tls_options
2424

@@ -295,3 +295,85 @@ def document(self) -> dict[str, Any]:
295295
if v is not None:
296296
doc[k] = v
297297
return doc
298+
299+
300+
class TextOpts:
301+
"""**BETA** Options to configure encrypted queries using the text algorithm.
302+
303+
TextOpts is currently unstable API and subject to backwards breaking changes."""
304+
305+
def __init__(
306+
self,
307+
substring: Optional[SubstringOpts] = None,
308+
prefix: Optional[PrefixOpts] = None,
309+
suffix: Optional[SuffixOpts] = None,
310+
case_sensitive: Optional[bool] = None,
311+
diacritic_sensitive: Optional[bool] = None,
312+
) -> None:
313+
"""Options to configure encrypted queries using the text algorithm.
314+
315+
:param substring: Further options to support substring queries.
316+
:param prefix: Further options to support prefix queries.
317+
:param suffix: Further options to support suffix queries.
318+
:param case_sensitive: Whether text indexes for this field are case sensitive.
319+
:param diacritic_sensitive: Whether text indexes for this field are diacritic sensitive.
320+
321+
.. versionadded:: 4.15
322+
"""
323+
self.substring = substring
324+
self.prefix = prefix
325+
self.suffix = suffix
326+
self.case_sensitive = case_sensitive
327+
self.diacritic_sensitive = diacritic_sensitive
328+
329+
@property
330+
def document(self) -> dict[str, Any]:
331+
doc = {}
332+
for k, v in [
333+
("substring", self.substring),
334+
("prefix", self.prefix),
335+
("suffix", self.suffix),
336+
("caseSensitive", self.case_sensitive),
337+
("diacriticSensitive", self.diacritic_sensitive),
338+
]:
339+
if v is not None:
340+
doc[k] = v
341+
return doc
342+
343+
344+
class SubstringOpts(TypedDict):
345+
"""**BETA** Options for substring text queries.
346+
347+
SubstringOpts is currently unstable API and subject to backwards breaking changes.
348+
"""
349+
350+
# strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
351+
strMaxLength: int
352+
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
353+
strMinQueryLength: int
354+
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
355+
strMaxQueryLength: int
356+
357+
358+
class PrefixOpts(TypedDict):
359+
"""**BETA** Options for prefix text queries.
360+
361+
PrefixOpts is currently unstable API and subject to backwards breaking changes.
362+
"""
363+
364+
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
365+
strMinQueryLength: int
366+
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
367+
strMaxQueryLength: int
368+
369+
370+
class SuffixOpts(TypedDict):
371+
"""**BETA** Options for suffix text queries.
372+
373+
SuffixOpts is currently unstable API and subject to backwards breaking changes.
374+
"""
375+
376+
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
377+
strMinQueryLength: int
378+
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
379+
strMaxQueryLength: int

pymongo/synchronous/encryption.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
from pymongo import _csot
6262
from pymongo.common import CONNECT_TIMEOUT
6363
from pymongo.daemon import _spawn_daemon
64-
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
64+
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
6565
from pymongo.errors import (
6666
ConfigurationError,
6767
EncryptedCollectionError,
@@ -513,6 +513,11 @@ class Algorithm(str, enum.Enum):
513513
514514
.. versionadded:: 4.4
515515
"""
516+
TEXTPREVIEW = "TextPreview"
517+
"""**BETA** - TextPreview.
518+
519+
.. versionadded:: 4.15
520+
"""
516521

517522

518523
class QueryType(str, enum.Enum):
@@ -538,6 +543,24 @@ class QueryType(str, enum.Enum):
538543
.. versionadded:: 4.4
539544
"""
540545

546+
PREFIXPREVIEW = "prefixPreview"
547+
"""**BETA** - Used to encrypt a value for a prefixPreview query.
548+
549+
.. versionadded:: 4.15
550+
"""
551+
552+
SUFFIXPREVIEW = "suffixPreview"
553+
"""**BETA** - Used to encrypt a value for a suffixPreview query.
554+
555+
.. versionadded:: 4.15
556+
"""
557+
558+
SUBSTRINGPREVIEW = "substringPreview"
559+
"""**BETA** - Used to encrypt a value for a substringPreview query.
560+
561+
.. versionadded:: 4.15
562+
"""
563+
541564

542565
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
543566
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
@@ -869,6 +892,7 @@ def _encrypt_helper(
869892
contention_factor: Optional[int] = None,
870893
range_opts: Optional[RangeOpts] = None,
871894
is_expression: bool = False,
895+
text_opts: Optional[TextOpts] = None,
872896
) -> Any:
873897
self._check_closed()
874898
if isinstance(key_id, uuid.UUID):
@@ -888,6 +912,12 @@ def _encrypt_helper(
888912
range_opts.document,
889913
codec_options=self._codec_options,
890914
)
915+
text_opts_bytes = None
916+
if text_opts:
917+
text_opts_bytes = encode(
918+
text_opts.document,
919+
codec_options=self._codec_options,
920+
)
891921
with _wrap_encryption_errors():
892922
encrypted_doc = self._encryption.encrypt(
893923
value=doc,
@@ -898,6 +928,7 @@ def _encrypt_helper(
898928
contention_factor=contention_factor,
899929
range_opts=range_opts_bytes,
900930
is_expression=is_expression,
931+
text_opts=text_opts_bytes,
901932
)
902933
return decode(encrypted_doc)["v"]
903934

@@ -910,6 +941,7 @@ def encrypt(
910941
query_type: Optional[str] = None,
911942
contention_factor: Optional[int] = None,
912943
range_opts: Optional[RangeOpts] = None,
944+
text_opts: Optional[TextOpts] = None,
913945
) -> Binary:
914946
"""Encrypt a BSON value with a given key and algorithm.
915947
@@ -930,9 +962,14 @@ def encrypt(
930962
used.
931963
:param range_opts: Index options for `range` queries. See
932964
:class:`RangeOpts` for some valid options.
965+
:param text_opts: Index options for `textPreview` queries. See
966+
:class:`TextOpts` for some valid options.
933967
934968
:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
935969
970+
.. versionchanged:: 4.9
971+
Added the `text_opts` parameter.
972+
936973
.. versionchanged:: 4.9
937974
Added the `range_opts` parameter.
938975
@@ -953,6 +990,7 @@ def encrypt(
953990
contention_factor=contention_factor,
954991
range_opts=range_opts,
955992
is_expression=False,
993+
text_opts=text_opts,
956994
),
957995
)
958996

test/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import warnings
3333
from inspect import iscoroutinefunction
3434

35+
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
3536
from pymongo.errors import AutoReconnect
3637
from pymongo.synchronous.uri_parser import parse_uri
3738

@@ -524,6 +525,19 @@ def require_version_max(self, *ver):
524525
"Server version must be at most %s" % str(other_version),
525526
)
526527

528+
def require_libmongocrypt_min(self, *ver):
529+
other_version = Version(*ver)
530+
if not _HAVE_PYMONGOCRYPT:
531+
version = Version.from_string("0.0.0")
532+
else:
533+
from pymongocrypt import libmongocrypt_version
534+
535+
version = Version.from_string(libmongocrypt_version())
536+
return self._require(
537+
lambda: version >= other_version,
538+
"Libmongocrypt version must be at least %s" % str(other_version),
539+
)
540+
527541
def require_auth(self, func):
528542
"""Run a test only if the server is running with auth enabled."""
529543
return self._require(

test/asynchronous/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from inspect import iscoroutinefunction
3434

3535
from pymongo.asynchronous.uri_parser import parse_uri
36+
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
3637
from pymongo.errors import AutoReconnect
3738

3839
try:
@@ -524,6 +525,19 @@ def require_version_max(self, *ver):
524525
"Server version must be at most %s" % str(other_version),
525526
)
526527

528+
def require_libmongocrypt_min(self, *ver):
529+
other_version = Version(*ver)
530+
if not _HAVE_PYMONGOCRYPT:
531+
version = Version.from_string("0.0.0")
532+
else:
533+
from pymongocrypt import libmongocrypt_version
534+
535+
version = Version.from_string(libmongocrypt_version())
536+
return self._require(
537+
lambda: version >= other_version,
538+
"Libmongocrypt version must be at least %s" % str(other_version),
539+
)
540+
527541
def require_auth(self, func):
528542
"""Run a test only if the server is running with auth enabled."""
529543
return self._require(

0 commit comments

Comments
 (0)