diff --git a/pymongo/collection.py b/pymongo/collection.py index b43e06c2a..afaef480c 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -117,7 +117,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]): read_concern: Optional["ReadConcern"] = None, session: Optional["ClientSession"] = None, timeout: Optional[float] = None, - encrypted_fields: Optional[Mapping[str, Any]] = None, **kwargs: Any, ) -> None: """Get / create a Mongo collection. @@ -159,13 +158,11 @@ class Collection(common.BaseObject, Generic[_DocumentType]): - `session` (optional): a :class:`~pymongo.client_session.ClientSession` that is used with the create collection command - - `encrypted_fields`: **(BETA)** Document that describes the encrypted fields for - Queryable Encryption. If provided it will be passed to the create collection command. - `**kwargs` (optional): additional keyword arguments will be passed as options for the create collection command .. versionchanged:: 4.2 - Added ``encrypted_fields`` parameter. + Added the ``clusteredIndex`` and ``encryptedFields`` parameters. .. versionchanged:: 4.0 Removed the reindex, map_reduce, inline_map_reduce, @@ -222,6 +219,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): self.__database: Database[_DocumentType] = database self.__name = name self.__full_name = "%s.%s" % (self.__database.name, self.__name) + encrypted_fields = kwargs.pop("encryptedFields", None) if create or kwargs or collation: if encrypted_fields: common.validate_is_mapping("encrypted_fields", encrypted_fields) diff --git a/pymongo/database.py b/pymongo/database.py index f764ade52..d3746b0c5 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -304,7 +304,6 @@ class Database(common.BaseObject, Generic[_DocumentType]): read_concern: Optional["ReadConcern"] = None, session: Optional["ClientSession"] = None, timeout: Optional[float] = None, - encrypted_fields: Optional[Mapping[str, Any]] = None, **kwargs: Any, ) -> Collection[_DocumentType]: """Create a new :class:`~pymongo.collection.Collection` in this @@ -336,28 +335,6 @@ class Database(common.BaseObject, Generic[_DocumentType]): :class:`~pymongo.collation.Collation`. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. - - `encrypted_fields`: **(BETA)** Document that describes the encrypted fields for - Queryable Encryption. For example:: - - { - "escCollection": "enxcol_.encryptedCollection.esc", - "eccCollection": "enxcol_.encryptedCollection.ecc", - "ecocCollection": "enxcol_.encryptedCollection.ecoc", - "fields": [ - { - "path": "firstName", - "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), - "bsonType": "string", - "queries": {"queryType": "equality"} - }, - { - "path": "ssn", - "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), - "bsonType": "string" - } - ] - - } } - `**kwargs` (optional): additional keyword arguments will be passed as options for the `create collection command`_ @@ -389,11 +366,42 @@ class Database(common.BaseObject, Generic[_DocumentType]): - ``pipeline`` (list): a list of aggregation pipeline stages - ``comment`` (str): a user-provided comment to attach to this command. This option is only supported on MongoDB >= 4.4. + - ``encryptedFields`` (dict): **(BETA)** Document that describes the encrypted fields for + Queryable Encryption. For example:: + + { + "escCollection": "enxcol_.encryptedCollection.esc", + "eccCollection": "enxcol_.encryptedCollection.ecc", + "ecocCollection": "enxcol_.encryptedCollection.ecoc", + "fields": [ + { + "path": "firstName", + "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), + "bsonType": "string", + "queries": {"queryType": "equality"} + }, + { + "path": "ssn", + "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), + "bsonType": "string" + } + ] + } + - ``clusteredIndex`` (dict): Document that specifies the clustered index + configuration. It must have the following form:: + + { + // key pattern must be {_id: 1} + key: , // required + unique: , // required, must be ‘true’ + name: , // optional, otherwise automatically generated + v: , // optional, must be ‘2’ if provided + } - ``changeStreamPreAndPostImages`` (dict): a document with a boolean field ``enabled`` for enabling pre- and post-images. .. versionchanged:: 4.2 - Added ``encrypted_fields`` parameter. + Added the ``clusteredIndex`` and ``encryptedFields`` parameters. .. versionchanged:: 3.11 This method is now supported inside multi-document transactions @@ -411,6 +419,7 @@ class Database(common.BaseObject, Generic[_DocumentType]): .. _create collection command: https://mongodb.com/docs/manual/reference/command/create """ + encrypted_fields = kwargs.get("encryptedFields") if ( not encrypted_fields and self.client.options.auto_encryption_opts @@ -419,8 +428,14 @@ class Database(common.BaseObject, Generic[_DocumentType]): encrypted_fields = self.client.options.auto_encryption_opts._encrypted_fields_map.get( "%s.%s" % (self.name, name) ) + kwargs["encryptedFields"] = encrypted_fields + if encrypted_fields: - common.validate_is_mapping("encrypted_fields", encrypted_fields) + common.validate_is_mapping("encryptedFields", encrypted_fields) + + clustered_index = kwargs.get("clusteredIndex") + if clustered_index: + common.validate_is_mapping("clusteredIndex", clustered_index) with self.__client._tmp_session(session) as s: # Skip this check in a transaction where listCollections is not @@ -439,7 +454,6 @@ class Database(common.BaseObject, Generic[_DocumentType]): read_concern, session=s, timeout=timeout, - encrypted_fields=encrypted_fields, **kwargs, ) diff --git a/test/collection_management/clustered-indexes.json b/test/collection_management/clustered-indexes.json new file mode 100644 index 000000000..739d0fd8b --- /dev/null +++ b/test/collection_management/clustered-indexes.json @@ -0,0 +1,177 @@ +{ + "description": "clustered-indexes", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "5.3", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "ts-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "ts-tests", + "documents": [] + } + ], + "tests": [ + { + "description": "createCollection with clusteredIndex", + "operations": [ + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "collection": "test" + } + }, + { + "name": "createCollection", + "object": "database0", + "arguments": { + "collection": "test", + "clusteredIndex": { + "key": { + "_id": 1 + }, + "unique": true, + "name": "test index" + } + } + }, + { + "name": "assertCollectionExists", + "object": "testRunner", + "arguments": { + "databaseName": "ts-tests", + "collectionName": "test" + } + } + ] + }, + { + "description": "listCollections includes clusteredIndex", + "operations": [ + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "collection": "test" + } + }, + { + "name": "createCollection", + "object": "database0", + "arguments": { + "collection": "test", + "clusteredIndex": { + "key": { + "_id": 1 + }, + "unique": true, + "name": "test index" + } + } + }, + { + "name": "listCollections", + "object": "database0", + "arguments": { + "filter": { + "name": { + "$eq": "test" + } + } + }, + "expectResult": [ + { + "name": "test", + "options": { + "clusteredIndex": { + "key": { + "_id": 1 + }, + "unique": true, + "name": "test index", + "v": { + "$$type": [ + "int", + "long" + ] + } + } + } + } + ] + } + ] + }, + { + "description": "listIndexes returns the index", + "operations": [ + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "collection": "test" + } + }, + { + "name": "createCollection", + "object": "database0", + "arguments": { + "collection": "test", + "clusteredIndex": { + "key": { + "_id": 1 + }, + "unique": true, + "name": "test index" + } + } + }, + { + "name": "listIndexes", + "object": "collection0", + "expectResult": [ + { + "key": { + "_id": 1 + }, + "name": "test index", + "clustered": true, + "unique": true, + "v": { + "$$type": [ + "int", + "long" + ] + } + } + ] + } + ] + } + ] +} diff --git a/test/test_encryption.py b/test/test_encryption.py index e5a9666d2..209308aba 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -658,7 +658,9 @@ class TestSpec(SpecRunner): kwargs["codec_options"] = OPTS if not data: kwargs["write_concern"] = wc - db.create_collection(coll_name, **kwargs, encrypted_fields=encrypted_fields) + if encrypted_fields: + kwargs["encryptedFields"] = encrypted_fields + db.create_collection(coll_name, **kwargs) coll = db[coll_name] if data: # Load data. diff --git a/test/unified_format.py b/test/unified_format.py index cb69882b2..001af4434 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -996,7 +996,7 @@ class UnifiedSpecTestMixinV1(IntegrationTest): def _collectionOperation_listIndexes(self, target, *args, **kwargs): if "batch_size" in kwargs: self.skipTest("PyMongo does not support batch_size for list_indexes") - return target.list_indexes(*args, **kwargs) + return list(target.list_indexes(*args, **kwargs)) def _collectionOperation_listIndexNames(self, target, *args, **kwargs): self.skipTest("PyMongo does not support list_index_names") diff --git a/test/utils.py b/test/utils.py index 1aeb7571a..7071764b1 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1002,8 +1002,6 @@ def parse_spec_options(opts): if "maxCommitTimeMS" in opts: opts["max_commit_time_ms"] = opts.pop("maxCommitTimeMS") - if "encryptedFields" in opts: - opts["encrypted_fields"] = opts.pop("encryptedFields") if "hint" in opts: hint = opts.pop("hint") if not isinstance(hint, str):