Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

PYTHON-5328 CRUD Support in Driver for Prefix/Suffix/Substring Indexes #2521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
blink1073 merged 5 commits into mongodb:master from blink1073:PYTHON-5328
Sep 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/changelog.rst
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Changes in Version 4.15.0 (XXXX/XX/XX)
--------------------------------------
PyMongo 4.15 brings a number of changes including:

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

Expand Down
40 changes: 39 additions & 1 deletion pymongo/asynchronous/encryption.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
from pymongo.asynchronous.pool import AsyncBaseConnection
from pymongo.common import CONNECT_TIMEOUT
from pymongo.daemon import _spawn_daemon
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
from pymongo.errors import (
ConfigurationError,
EncryptedCollectionError,
Expand Down Expand Up @@ -516,6 +516,11 @@ class Algorithm(str, enum.Enum):

.. versionadded:: 4.4
"""
TEXTPREVIEW = "TextPreview"
Copy link
Contributor

@aclark4life aclark4life Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we required to call this TEXTPREVIEW instead of calling it TEXT and designating "feature preview"?

Copy link
Member Author

@blink1073 blink1073 Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, following the pattern we used for RANGEPREVIEW, to ensure that if the semantics change it won't do so silently.

aclark4life reacted with thumbs up emoji
"""**BETA** - TextPreview.

.. versionadded:: 4.15
"""


class QueryType(str, enum.Enum):
Expand All @@ -541,6 +546,24 @@ class QueryType(str, enum.Enum):
.. versionadded:: 4.4
"""

PREFIXPREVIEW = "prefixPreview"
"""**BETA** - Used to encrypt a value for a prefixPreview query.

.. versionadded:: 4.15
"""

SUFFIXPREVIEW = "suffixPreview"
"""**BETA** - Used to encrypt a value for a suffixPreview query.

.. versionadded:: 4.15
"""

SUBSTRINGPREVIEW = "substringPreview"
"""**BETA** - Used to encrypt a value for a substringPreview query.

.. versionadded:: 4.15
"""


def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
Expand Down Expand Up @@ -876,6 +899,7 @@ async def _encrypt_helper(
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
is_expression: bool = False,
text_opts: Optional[TextOpts] = None,
) -> Any:
self._check_closed()
if isinstance(key_id, uuid.UUID):
Expand All @@ -895,6 +919,12 @@ async def _encrypt_helper(
range_opts.document,
codec_options=self._codec_options,
)
text_opts_bytes = None
if text_opts:
text_opts_bytes = encode(
text_opts.document,
codec_options=self._codec_options,
)
with _wrap_encryption_errors():
encrypted_doc = await self._encryption.encrypt(
value=doc,
Expand All @@ -905,6 +935,7 @@ async def _encrypt_helper(
contention_factor=contention_factor,
range_opts=range_opts_bytes,
is_expression=is_expression,
text_opts=text_opts_bytes,
Copy link
Member

@ShaneHarvey ShaneHarvey Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing text_opts even when it's None will fail on old versions of pymongocrypt. So we need to bump the min version or avoid passing text_opts by default.

)
return decode(encrypted_doc)["v"]

Expand All @@ -917,6 +948,7 @@ async def encrypt(
query_type: Optional[str] = None,
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
text_opts: Optional[TextOpts] = None,
) -> Binary:
"""Encrypt a BSON value with a given key and algorithm.

Expand All @@ -937,9 +969,14 @@ async def encrypt(
used.
:param range_opts: Index options for `range` queries. See
:class:`RangeOpts` for some valid options.
:param text_opts: Index options for `textPreview` queries. See
:class:`TextOpts` for some valid options.

:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.

.. versionchanged:: 4.9
Added the `text_opts` parameter.

.. versionchanged:: 4.9
Added the `range_opts` parameter.

Expand All @@ -960,6 +997,7 @@ async def encrypt(
contention_factor=contention_factor,
range_opts=range_opts,
is_expression=False,
text_opts=text_opts,
),
)

Expand Down
84 changes: 83 additions & 1 deletion pymongo/encryption_options.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"""
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Mapping, Optional
from typing import TYPE_CHECKING, Any, Mapping, Optional, TypedDict

from pymongo.uri_parser_shared import _parse_kms_tls_options

Expand Down Expand Up @@ -295,3 +295,85 @@ def document(self) -> dict[str, Any]:
if v is not None:
doc[k] = v
return doc


class TextOpts:
"""**BETA** Options to configure encrypted queries using the text algorithm.

TextOpts is currently unstable API and subject to backwards breaking changes."""

def __init__(
self,
substring: Optional[SubstringOpts] = None,
prefix: Optional[PrefixOpts] = None,
suffix: Optional[SuffixOpts] = None,
case_sensitive: Optional[bool] = None,
diacritic_sensitive: Optional[bool] = None,
) -> None:
"""Options to configure encrypted queries using the text algorithm.

:param substring: Further options to support substring queries.
:param prefix: Further options to support prefix queries.
:param suffix: Further options to support suffix queries.
:param case_sensitive: Whether text indexes for this field are case sensitive.
:param diacritic_sensitive: Whether text indexes for this field are diacritic sensitive.

.. versionadded:: 4.15
"""
self.substring = substring
self.prefix = prefix
self.suffix = suffix
self.case_sensitive = case_sensitive
self.diacritic_sensitive = diacritic_sensitive

@property
def document(self) -> dict[str, Any]:
doc = {}
for k, v in [
("substring", self.substring),
("prefix", self.prefix),
("suffix", self.suffix),
("caseSensitive", self.case_sensitive),
("diacriticSensitive", self.diacritic_sensitive),
]:
if v is not None:
doc[k] = v
return doc


class SubstringOpts(TypedDict):
"""**BETA** Options for substring text queries.

SubstringOpts is currently unstable API and subject to backwards breaking changes.
"""

# strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
strMaxLength: int
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
strMinQueryLength: int
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
strMaxQueryLength: int


class PrefixOpts(TypedDict):
"""**BETA** Options for prefix text queries.

PrefixOpts is currently unstable API and subject to backwards breaking changes.
"""

# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
strMinQueryLength: int
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
strMaxQueryLength: int


class SuffixOpts(TypedDict):
"""**BETA** Options for suffix text queries.

SuffixOpts is currently unstable API and subject to backwards breaking changes.
"""

# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
strMinQueryLength: int
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
strMaxQueryLength: int
40 changes: 39 additions & 1 deletion pymongo/synchronous/encryption.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
from pymongo import _csot
from pymongo.common import CONNECT_TIMEOUT
from pymongo.daemon import _spawn_daemon
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
from pymongo.errors import (
ConfigurationError,
EncryptedCollectionError,
Expand Down Expand Up @@ -513,6 +513,11 @@ class Algorithm(str, enum.Enum):

.. versionadded:: 4.4
"""
TEXTPREVIEW = "TextPreview"
"""**BETA** - TextPreview.

.. versionadded:: 4.15
"""


class QueryType(str, enum.Enum):
Expand All @@ -538,6 +543,24 @@ class QueryType(str, enum.Enum):
.. versionadded:: 4.4
"""

PREFIXPREVIEW = "prefixPreview"
"""**BETA** - Used to encrypt a value for a prefixPreview query.

.. versionadded:: 4.15
"""

SUFFIXPREVIEW = "suffixPreview"
"""**BETA** - Used to encrypt a value for a suffixPreview query.

.. versionadded:: 4.15
"""

SUBSTRINGPREVIEW = "substringPreview"
"""**BETA** - Used to encrypt a value for a substringPreview query.

.. versionadded:: 4.15
"""


def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
Expand Down Expand Up @@ -869,6 +892,7 @@ def _encrypt_helper(
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
is_expression: bool = False,
text_opts: Optional[TextOpts] = None,
) -> Any:
self._check_closed()
if isinstance(key_id, uuid.UUID):
Expand All @@ -888,6 +912,12 @@ def _encrypt_helper(
range_opts.document,
codec_options=self._codec_options,
)
text_opts_bytes = None
if text_opts:
text_opts_bytes = encode(
text_opts.document,
codec_options=self._codec_options,
)
with _wrap_encryption_errors():
encrypted_doc = self._encryption.encrypt(
value=doc,
Expand All @@ -898,6 +928,7 @@ def _encrypt_helper(
contention_factor=contention_factor,
range_opts=range_opts_bytes,
is_expression=is_expression,
text_opts=text_opts_bytes,
)
return decode(encrypted_doc)["v"]

Expand All @@ -910,6 +941,7 @@ def encrypt(
query_type: Optional[str] = None,
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
text_opts: Optional[TextOpts] = None,
) -> Binary:
"""Encrypt a BSON value with a given key and algorithm.

Expand All @@ -930,9 +962,14 @@ def encrypt(
used.
:param range_opts: Index options for `range` queries. See
:class:`RangeOpts` for some valid options.
:param text_opts: Index options for `textPreview` queries. See
:class:`TextOpts` for some valid options.

:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.

.. versionchanged:: 4.9
Added the `text_opts` parameter.

.. versionchanged:: 4.9
Added the `range_opts` parameter.

Expand All @@ -953,6 +990,7 @@ def encrypt(
contention_factor=contention_factor,
range_opts=range_opts,
is_expression=False,
text_opts=text_opts,
),
)

Expand Down
14 changes: 14 additions & 0 deletions test/__init__.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import warnings
from inspect import iscoroutinefunction

from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
from pymongo.errors import AutoReconnect
from pymongo.synchronous.uri_parser import parse_uri

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

def require_libmongocrypt_min(self, *ver):
other_version = Version(*ver)
if not _HAVE_PYMONGOCRYPT:
version = Version.from_string("0.0.0")
else:
from pymongocrypt import libmongocrypt_version

version = Version.from_string(libmongocrypt_version())
return self._require(
lambda: version >= other_version,
"Libmongocrypt version must be at least %s" % str(other_version),
)

def require_auth(self, func):
"""Run a test only if the server is running with auth enabled."""
return self._require(
Expand Down
14 changes: 14 additions & 0 deletions test/asynchronous/__init__.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from inspect import iscoroutinefunction

from pymongo.asynchronous.uri_parser import parse_uri
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
from pymongo.errors import AutoReconnect

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

def require_libmongocrypt_min(self, *ver):
other_version = Version(*ver)
if not _HAVE_PYMONGOCRYPT:
version = Version.from_string("0.0.0")
else:
from pymongocrypt import libmongocrypt_version

version = Version.from_string(libmongocrypt_version())
return self._require(
lambda: version >= other_version,
"Libmongocrypt version must be at least %s" % str(other_version),
)

def require_auth(self, func):
"""Run a test only if the server is running with auth enabled."""
return self._require(
Expand Down
Loading
Loading

AltStyle によって変換されたページ (->オリジナル) /