diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 17571a178..f1db3e9fa 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -28,6 +28,11 @@
($value is NativeType ? BSONType : $value)
+
+
+ $options
+
+
$cmd[$option]
@@ -317,6 +322,14 @@
isInTransaction
+
+
+ $index
+
+
+
+
+
options['typeMap']]]>
@@ -397,6 +410,11 @@
isInTransaction
+
+
+
+
+
options['typeMap']]]>
@@ -576,6 +594,11 @@
isInTransaction
+
+
+
+
+
cursor->firstBatch]]>
diff --git a/src/Collection.php b/src/Collection.php
index b5a672308..0420be097 100644
--- a/src/Collection.php
+++ b/src/Collection.php
@@ -17,6 +17,7 @@
namespace MongoDB;
+use Countable;
use Iterator;
use MongoDB\BSON\JavascriptInterface;
use MongoDB\Codec\DocumentCodec;
@@ -38,12 +39,14 @@
use MongoDB\Operation\Count;
use MongoDB\Operation\CountDocuments;
use MongoDB\Operation\CreateIndexes;
+use MongoDB\Operation\CreateSearchIndexes;
use MongoDB\Operation\DeleteMany;
use MongoDB\Operation\DeleteOne;
use MongoDB\Operation\Distinct;
use MongoDB\Operation\DropCollection;
use MongoDB\Operation\DropEncryptedCollection;
use MongoDB\Operation\DropIndexes;
+use MongoDB\Operation\DropSearchIndex;
use MongoDB\Operation\EstimatedDocumentCount;
use MongoDB\Operation\Explain;
use MongoDB\Operation\Explainable;
@@ -55,11 +58,13 @@
use MongoDB\Operation\InsertMany;
use MongoDB\Operation\InsertOne;
use MongoDB\Operation\ListIndexes;
+use MongoDB\Operation\ListSearchIndexes;
use MongoDB\Operation\MapReduce;
use MongoDB\Operation\RenameCollection;
use MongoDB\Operation\ReplaceOne;
use MongoDB\Operation\UpdateMany;
use MongoDB\Operation\UpdateOne;
+use MongoDB\Operation\UpdateSearchIndex;
use MongoDB\Operation\Watch;
use function array_diff_key;
@@ -360,6 +365,64 @@ public function createIndexes(array $indexes, array $options = [])
return $operation->execute(select_server($this->manager, $options));
}
+ /**
+ * Create an Atlas Search index for the collection.
+ * Only available when used against a 7.0+ Atlas cluster.
+ *
+ * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/
+ * @see https://mongodb.com/docs/manual/reference/method/db.collection.createSearchIndex/
+ * @param array|object $definition Atlas Search index mapping definition
+ * @param array{name?: string, comment?: mixed} $options Command options
+ * @return string The name of the created search index
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function createSearchIndex($definition, array $options = []): string
+ {
+ $index = ['definition' => $definition];
+ if (isset($options['name'])) {
+ $index['name'] = $options['name'];
+ unset($options['name']);
+ }
+
+ $names = $this->createSearchIndexes([$index], $options);
+
+ return current($names);
+ }
+
+ /**
+ * Create one or more Atlas Search indexes for the collection.
+ * Only available when used against a 7.0+ Atlas cluster.
+ *
+ * Each element in the $indexes array must have "definition" document and they may have a "name" string.
+ * The name can be omitted for a single index, in which case a name will be the default.
+ * For example:
+ *
+ * $indexes = [
+ * // Create a search index with the default name, on
+ * ['definition' => ['mappings' => ['dynamic' => false, 'fields' => ['title' => ['type' => 'string']]]]],
+ * // Create a named search index on all fields
+ * ['name' => 'search_all', 'definition' => ['mappings' => ['dynamic' => true]]],
+ * ];
+ *
+ * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/
+ * @see https://mongodb.com/docs/manual/reference/method/db.collection.createSearchIndex/
+ * @param list $indexes List of search index specifications
+ * @param array{comment?: string} $options Command options
+ * @return string[] The names of the created search indexes
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function createSearchIndexes(array $indexes, array $options = []): array
+ {
+ $operation = new CreateSearchIndexes($this->databaseName, $this->collectionName, $indexes, $options);
+ $server = select_server($this->manager, $options);
+
+ return $operation->execute($server);
+ }
+
/**
* Deletes all documents matching the filter.
*
@@ -501,6 +564,24 @@ public function dropIndexes(array $options = [])
return $operation->execute(select_server($this->manager, $options));
}
+ /**
+ * Drop a single Atlas Search index in the collection.
+ * Only available when used against a 7.0+ Atlas cluster.
+ *
+ * @param string $name Search index name
+ * @param array{comment?: mixed} $options Additional options
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function dropSearchIndex(string $name, array $options = []): void
+ {
+ $operation = new DropSearchIndex($this->databaseName, $this->collectionName, $name);
+ $server = select_server($this->manager, $options);
+
+ $operation->execute($server);
+ }
+
/**
* Gets an estimated number of documents in the collection using the collection metadata.
*
@@ -812,6 +893,24 @@ public function listIndexes(array $options = [])
return $operation->execute(select_server($this->manager, $options));
}
+ /**
+ * Returns information for all Atlas Search indexes for the collection.
+ * Only available when used against a 7.0+ Atlas cluster.
+ *
+ * @param array{name?: string} $options Command options
+ * @return Countable&Iterator
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ * @see ListSearchIndexes::__construct() for supported options
+ */
+ public function listSearchIndexes(array $options = []): Iterator
+ {
+ $operation = new ListSearchIndexes($this->databaseName, $this->collectionName, $options);
+ $server = select_server($this->manager, $options);
+
+ return $operation->execute($server);
+ }
+
/**
* Executes a map-reduce aggregation on the collection.
*
@@ -946,6 +1045,25 @@ public function updateOne($filter, $update, array $options = [])
return $operation->execute(select_server($this->manager, $options));
}
+ /**
+ * Update a single Atlas Search index in the collection.
+ * Only available when used against a 7.0+ Atlas cluster.
+ *
+ * @param string $name Search index name
+ * @param array|object $definition Atlas Search index definition
+ * @param array{comment?: mixed} $options Command options
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function updateSearchIndex(string $name, $definition, array $options = []): void
+ {
+ $operation = new UpdateSearchIndex($this->databaseName, $this->collectionName, $name, $definition, $options);
+ $server = select_server($this->manager, $options);
+
+ $operation->execute($server);
+ }
+
/**
* Create a change stream for watching changes to the collection.
*
diff --git a/src/Model/IndexInput.php b/src/Model/IndexInput.php
index 1c089db9e..4a21bc6c5 100644
--- a/src/Model/IndexInput.php
+++ b/src/Model/IndexInput.php
@@ -86,9 +86,9 @@ public function __toString(): string
* @see \MongoDB\Collection::createIndexes()
* @see https://php.net/mongodb-bson-serializable.bsonserialize
*/
- public function bsonSerialize(): array
+ public function bsonSerialize(): object
{
- return $this->index;
+ return (object) $this->index;
}
/**
diff --git a/src/Model/SearchIndexInput.php b/src/Model/SearchIndexInput.php
new file mode 100644
index 000000000..991159f6a
--- /dev/null
+++ b/src/Model/SearchIndexInput.php
@@ -0,0 +1,73 @@
+index = $index;
+ }
+
+ /**
+ * Serialize the search index information to BSON for search index creation.
+ *
+ * @see \MongoDB\Collection::createSearchIndexes()
+ * @see https://php.net/mongodb-bson-serializable.bsonserialize
+ */
+ public function bsonSerialize(): object
+ {
+ return (object) $this->index;
+ }
+}
diff --git a/src/Operation/CreateSearchIndexes.php b/src/Operation/CreateSearchIndexes.php
new file mode 100644
index 000000000..96e529b08
--- /dev/null
+++ b/src/Operation/CreateSearchIndexes.php
@@ -0,0 +1,101 @@
+ $index) {
+ if (! is_array($index)) {
+ throw InvalidArgumentException::invalidType(sprintf('$indexes[%d]', $i), $index, 'array');
+ }
+
+ $this->indexes[] = new SearchIndexInput($index);
+ }
+
+ $this->databaseName = $databaseName;
+ $this->collectionName = $collectionName;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @return string[] The names of the created indexes
+ * @throws UnsupportedException if write concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server): array
+ {
+ $cmd = [
+ 'createSearchIndexes' => $this->collectionName,
+ 'indexes' => $this->indexes,
+ ];
+
+ if (isset($this->options['comment'])) {
+ $cmd['comment'] = $this->options['comment'];
+ }
+
+ $cursor = $server->executeCommand($this->databaseName, new Command($cmd));
+
+ /** @var object{indexesCreated: list} $result */
+ $result = current($cursor->toArray());
+
+ return array_column($result->indexesCreated, 'name');
+ }
+}
diff --git a/src/Operation/DropIndexes.php b/src/Operation/DropIndexes.php
index 66ded5712..0bbd08247 100644
--- a/src/Operation/DropIndexes.php
+++ b/src/Operation/DropIndexes.php
@@ -72,8 +72,6 @@ class DropIndexes implements Executable
*/
public function __construct(string $databaseName, string $collectionName, string $indexName, array $options = [])
{
- $indexName = $indexName;
-
if ($indexName === '') {
throw new InvalidArgumentException('$indexName cannot be empty');
}
diff --git a/src/Operation/DropSearchIndex.php b/src/Operation/DropSearchIndex.php
new file mode 100644
index 000000000..a8a6ff1b7
--- /dev/null
+++ b/src/Operation/DropSearchIndex.php
@@ -0,0 +1,90 @@
+databaseName = $databaseName;
+ $this->collectionName = $collectionName;
+ $this->name = $name;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @throws UnsupportedException if write concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server): void
+ {
+ $cmd = [
+ 'dropSearchIndex' => $this->collectionName,
+ 'name' => $this->name,
+ ];
+
+ if (isset($this->options['comment'])) {
+ $cmd['comment'] = $this->options['comment'];
+ }
+
+ try {
+ $server->executeCommand($this->databaseName, new Command($cmd));
+ } catch (CommandException $e) {
+ // Drop operations are idempotent. The server may return an error if the collection does not exist.
+ if ($e->getCode() !== self::ERROR_CODE_NAMESPACE_NOT_FOUND) {
+ throw $e;
+ }
+ }
+ }
+}
diff --git a/src/Operation/ListSearchIndexes.php b/src/Operation/ListSearchIndexes.php
new file mode 100644
index 000000000..875eab3af
--- /dev/null
+++ b/src/Operation/ListSearchIndexes.php
@@ -0,0 +1,95 @@
+databaseName = $databaseName;
+ $this->collectionName = $collectionName;
+ $this->listSearchIndexesOptions = array_intersect_key($options, ['name' => 1]);
+ $this->aggregateOptions = array_intersect_key($options, ['batchSize' => 1, 'collation' => 1, 'comment' => 1, 'maxTimeMS' => 1, 'readConcern' => 1, 'readPreference' => 1, 'session' => 1, 'typeMap' => 1]);
+
+ $this->aggregate = $this->createAggregate();
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @return Iterator&Countable
+ * @see Executable::execute()
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if collation or read concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server): Iterator
+ {
+ $cursor = $this->aggregate->execute($server);
+
+ return new CachingIterator($cursor);
+ }
+
+ private function createAggregate(): Aggregate
+ {
+ $pipeline = [
+ ['$listSearchIndexes' => (object) $this->listSearchIndexesOptions],
+ ];
+
+ return new Aggregate($this->databaseName, $this->collectionName, $pipeline, $this->aggregateOptions);
+ }
+}
diff --git a/src/Operation/UpdateSearchIndex.php b/src/Operation/UpdateSearchIndex.php
new file mode 100644
index 000000000..a44bc4103
--- /dev/null
+++ b/src/Operation/UpdateSearchIndex.php
@@ -0,0 +1,90 @@
+databaseName = $databaseName;
+ $this->collectionName = $collectionName;
+ $this->name = $name;
+ $this->definition = (object) $definition;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @throws UnsupportedException if write concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server): void
+ {
+ $cmd = [
+ 'updateSearchIndex' => $this->collectionName,
+ 'name' => $this->name,
+ 'definition' => $this->definition,
+ ];
+
+ if (isset($this->options['comment'])) {
+ $cmd['comment'] = $this->options['comment'];
+ }
+
+ $server->executeCommand($this->databaseName, new Command($cmd));
+ }
+}
diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php
index 2a79e6b19..c45a94367 100644
--- a/tests/FunctionalTestCase.php
+++ b/tests/FunctionalTestCase.php
@@ -27,6 +27,7 @@
use function filter_var;
use function getenv;
use function implode;
+use function in_array;
use function is_array;
use function is_callable;
use function is_executable;
@@ -51,12 +52,14 @@
abstract class FunctionalTestCase extends TestCase
{
+ private const ATLAS_TLD = '/\.(mongodb\.net|mongodb-dev\.net)/';
+
protected Manager $manager;
private array $configuredFailPoints = [];
/** @var array{int,{Collection,array}} */
- private $collectionsToCleanup = [];
+ private array $collectionsToCleanup = [];
public function setUp(): void
{
@@ -519,6 +522,25 @@ protected function skipIfTransactionsAreNotSupported(): void
}
}
+ protected function isEnterprise(): bool
+ {
+ $buildInfo = $this->getPrimaryServer()->executeCommand(
+ $this->getDatabaseName(),
+ new Command(['buildInfo' => 1]),
+ )->toArray()[0];
+
+ if (isset($buildInfo->modules) && is_array($buildInfo->modules)) {
+ return in_array('enterprise', $buildInfo->modules);
+ }
+
+ throw new UnexpectedValueException('Could not determine server modules');
+ }
+
+ public static function isAtlas(?string $uri = null): bool
+ {
+ return preg_match(self::ATLAS_TLD, $uri ?? static::getUri());
+ }
+
/** @see https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/shared-library/ */
public static function isCryptSharedLibAvailable(): bool
{
diff --git a/tests/Model/IndexInputTest.php b/tests/Model/IndexInputTest.php
index b9653afda..3f5e814e5 100644
--- a/tests/Model/IndexInputTest.php
+++ b/tests/Model/IndexInputTest.php
@@ -15,19 +15,23 @@ class IndexInputTest extends TestCase
public function testConstructorShouldRequireKey(): void
{
$this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Required "key" document is missing from index specification');
new IndexInput([]);
}
- public function testConstructorShouldRequireKeyToBeArrayOrObject(): void
+ /** @dataProvider provideInvalidDocumentValues */
+ public function testConstructorShouldRequireKeyToBeArrayOrObject($key): void
{
$this->expectException(InvalidArgumentException::class);
- new IndexInput(['key' => 'foo']);
+ $this->expectExceptionMessage('Expected "key" option to have type "document"');
+ new IndexInput(['key' => $key]);
}
/** @dataProvider provideInvalidFieldOrderValues */
public function testConstructorShouldRequireKeyFieldOrderToBeNumericOrString($order): void
{
$this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected order value for "x" field within "key" option to have type "numeric or string"');
new IndexInput(['key' => ['x' => $order]]);
}
@@ -36,10 +40,12 @@ public function provideInvalidFieldOrderValues()
return $this->wrapValuesForDataProvider([true, [], new stdClass()]);
}
- public function testConstructorShouldRequireNameToBeString(): void
+ /** @dataProvider provideInvalidStringValues */
+ public function testConstructorShouldRequireNameToBeString($name): void
{
$this->expectException(InvalidArgumentException::class);
- new IndexInput(['key' => ['x' => 1], 'name' => 1]);
+ $this->expectExceptionMessage('Expected "name" option to have type "string"');
+ new IndexInput(['key' => ['x' => 1], 'name' => $name]);
}
/**
@@ -67,7 +73,7 @@ public function provideExpectedNameAndKey(): array
public function testBsonSerialization(): void
{
- $expected = [
+ $expected = (object) [
'key' => ['x' => 1],
'unique' => true,
'name' => 'x_1',
@@ -79,6 +85,6 @@ public function testBsonSerialization(): void
]);
$this->assertInstanceOf(Serializable::class, $indexInput);
- $this->assertSame($expected, $indexInput->bsonSerialize());
+ $this->assertEquals($expected, $indexInput->bsonSerialize());
}
}
diff --git a/tests/Model/SearchIndexInputTest.php b/tests/Model/SearchIndexInputTest.php
new file mode 100644
index 000000000..0126aaec6
--- /dev/null
+++ b/tests/Model/SearchIndexInputTest.php
@@ -0,0 +1,50 @@
+expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Required "definition" document is missing from search index specification');
+ new SearchIndexInput([]);
+ }
+
+ /** @dataProvider provideInvalidDocumentValues */
+ public function testConstructorIndexDefinitionMustBeADocument($definition): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "definition" option to have type "document"');
+ new SearchIndexInput(['definition' => $definition]);
+ }
+
+ /** @dataProvider provideInvalidStringValues */
+ public function testConstructorShouldRequireNameToBeString($name): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "name" option to have type "string"');
+ new SearchIndexInput(['definition' => ['mapping' => ['dynamic' => true]], 'name' => $name]);
+ }
+
+ public function testBsonSerialization(): void
+ {
+ $expected = (object) [
+ 'name' => 'my_search',
+ 'definition' => ['mapping' => ['dynamic' => true]],
+ ];
+
+ $indexInput = new SearchIndexInput([
+ 'name' => 'my_search',
+ 'definition' => ['mapping' => ['dynamic' => true]],
+ ]);
+
+ $this->assertInstanceOf(Serializable::class, $indexInput);
+ $this->assertEquals($expected, $indexInput->bsonSerialize());
+ }
+}
diff --git a/tests/Operation/BulkWriteTest.php b/tests/Operation/BulkWriteTest.php
index a307f5b01..2c255bec5 100644
--- a/tests/Operation/BulkWriteTest.php
+++ b/tests/Operation/BulkWriteTest.php
@@ -106,11 +106,6 @@ public function testDeleteManyCollationOptionTypeCheck($collation): void
]);
}
- public function provideInvalidDocumentValues()
- {
- return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
- }
-
public function testDeleteOneFilterArgumentMissing(): void
{
$this->expectException(InvalidArgumentException::class);
diff --git a/tests/Operation/CreateIndexesTest.php b/tests/Operation/CreateIndexesTest.php
index 0ece84f43..04e0e86ba 100644
--- a/tests/Operation/CreateIndexesTest.php
+++ b/tests/Operation/CreateIndexesTest.php
@@ -98,9 +98,4 @@ public function testConstructorRequiresIndexSpecificationNameToBeString($name):
$this->expectExceptionMessage('Expected "name" option to have type "string"');
new CreateIndexes($this->getDatabaseName(), $this->getCollectionName(), [['key' => ['x' => 1], 'name' => $name]]);
}
-
- public function provideInvalidStringValues()
- {
- return $this->wrapValuesForDataProvider($this->getInvalidStringValues());
- }
}
diff --git a/tests/Operation/CreateSearchIndexesTest.php b/tests/Operation/CreateSearchIndexesTest.php
new file mode 100644
index 000000000..e2dcb1e65
--- /dev/null
+++ b/tests/Operation/CreateSearchIndexesTest.php
@@ -0,0 +1,47 @@
+expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('$indexes is not a list');
+ new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [1 => ['name' => 'index name', 'definition' => ['mappings' => ['dynamic' => true]]]], []);
+ }
+
+ /** @dataProvider provideInvalidArrayValues */
+ public function testConstructorIndexDefinitionMustBeADocument($index): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected $indexes[0] to have type "array"');
+ new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [$index], []);
+ }
+
+ /** @dataProvider provideInvalidStringValues */
+ public function testConstructorIndexNameMustBeAString($name): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "name" option to have type "string"');
+ new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [['name' => $name, 'definition' => ['mappings' => ['dynamic' => true]]]], []);
+ }
+
+ public function testConstructorIndexDefinitionMustBeDefined(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Required "definition" document is missing from search index specification');
+ new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [['name' => 'index name']], []);
+ }
+
+ /** @dataProvider provideInvalidDocumentValues */
+ public function testConstructorIndexDefinitionMustBeAnArray($definition): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "definition" option to have type "document"');
+ new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [['definition' => $definition]], []);
+ }
+}
diff --git a/tests/Operation/DropSearchIndexTest.php b/tests/Operation/DropSearchIndexTest.php
new file mode 100644
index 000000000..06be34e59
--- /dev/null
+++ b/tests/Operation/DropSearchIndexTest.php
@@ -0,0 +1,15 @@
+expectException(InvalidArgumentException::class);
+ new DropSearchIndex($this->getDatabaseName(), $this->getCollectionName(), '');
+ }
+}
diff --git a/tests/Operation/ListSearchIndexesTest.php b/tests/Operation/ListSearchIndexesTest.php
new file mode 100644
index 000000000..65d020e68
--- /dev/null
+++ b/tests/Operation/ListSearchIndexesTest.php
@@ -0,0 +1,33 @@
+expectException(InvalidArgumentException::class);
+ new ListSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), ['name' => '']);
+ }
+
+ /** @dataProvider provideInvalidConstructorOptions */
+ public function testConstructorOptionTypeChecks(array $options): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new ListSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), $options);
+ }
+
+ public function provideInvalidConstructorOptions(): array
+ {
+ $options = [];
+
+ foreach ($this->getInvalidIntegerValues() as $value) {
+ $options[][] = ['batchSize' => $value];
+ }
+
+ return $options;
+ }
+}
diff --git a/tests/Operation/UpdateSearchIndexTest.php b/tests/Operation/UpdateSearchIndexTest.php
new file mode 100644
index 000000000..90c623fc3
--- /dev/null
+++ b/tests/Operation/UpdateSearchIndexTest.php
@@ -0,0 +1,23 @@
+expectException(InvalidArgumentException::class);
+ new UpdateSearchIndex($this->getDatabaseName(), $this->getCollectionName(), '', []);
+ }
+
+ /** @dataProvider provideInvalidDocumentValues */
+ public function testConstructorIndexDefinitionMustBeADocument($definition): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected $definition to have type "document"');
+ new UpdateSearchIndex($this->getDatabaseName(), $this->getCollectionName(), 'index name', $definition);
+ }
+}
diff --git a/tests/SpecTests/SearchIndexSpecTest.php b/tests/SpecTests/SearchIndexSpecTest.php
new file mode 100644
index 000000000..d5ed619ef
--- /dev/null
+++ b/tests/SpecTests/SearchIndexSpecTest.php
@@ -0,0 +1,204 @@
+skipIfServerVersion('<', '7.0', 'Search Indexes are only supported on MongoDB Atlas 7.0+');
+ }
+
+ /**
+ * Case 1: Driver can successfully create and list search indexes
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.rst#case-1-driver-can-successfully-create-and-list-search-indexes
+ */
+ public function testCreateAndListSearchIndexes(): void
+ {
+ $collection = $this->createCollection($this->getDatabaseName(), $this->getCollectionName());
+ $name = 'test-search-index';
+ $mapping = ['mappings' => ['dynamic' => false]];
+
+ $createdName = $collection->createSearchIndex(
+ $mapping,
+ ['name' => $name, 'comment' => 'Index creation test'],
+ );
+ $this->assertSame($name, $createdName);
+
+ $indexes = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $this->assertCount(1, $indexes);
+ $this->assertSame($name, $indexes[0]->name);
+ $this->assertSameDocument($mapping, $indexes[0]->latestDefinition);
+ }
+
+ /**
+ * Case 2: Driver can successfully create multiple indexes in batch
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.rst#case-2-driver-can-successfully-create-multiple-indexes-in-batch
+ */
+ public function testCreateMultipleIndexesInBatch(): void
+ {
+ $collection = $this->createCollection($this->getDatabaseName(), $this->getCollectionName());
+ $names = ['test-search-index-1', 'test-search-index-2'];
+ $mapping = ['mappings' => ['dynamic' => false]];
+
+ $createdNames = $collection->createSearchIndexes([
+ ['name' => $names[0], 'definition' => $mapping],
+ ['name' => $names[1], 'definition' => $mapping],
+ ]);
+ $this->assertSame($names, $createdNames);
+
+ $indexes = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $this->assertCount(2, $indexes);
+ foreach ($names as $key => $name) {
+ $index = $indexes[$key];
+ $this->assertSame($name, $index->name);
+ $this->assertSameDocument($mapping, $index->latestDefinition);
+ }
+ }
+
+ /**
+ * Case 3: Driver can successfully drop search indexes
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.rst#case-3-driver-can-successfully-drop-search-indexes
+ */
+ public function testDropSearchIndexes(): void
+ {
+ $collection = $this->createCollection($this->getDatabaseName(), $this->getCollectionName());
+ $name = 'test-search-index';
+ $mapping = ['mappings' => ['dynamic' => false]];
+
+ $createdName = $collection->createSearchIndex(
+ $mapping,
+ ['name' => $name],
+ );
+ $this->assertSame($name, $createdName);
+
+ $indexes = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+ $this->assertCount(1, $indexes);
+
+ $collection->dropSearchIndex($name);
+
+ $indexes = $this->waitForIndexes($collection, fn (array $indexes): bool => count($indexes) === 0);
+ $this->assertCount(0, $indexes);
+ }
+
+ /**
+ * Case 4: Driver can update a search index
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.rst#case-4-driver-can-update-a-search-index
+ */
+ public function testUpdateSearchIndex(): void
+ {
+ $collection = $this->createCollection($this->getDatabaseName(), $this->getCollectionName());
+ $name = 'test-search-index';
+ $mapping = ['mappings' => ['dynamic' => false]];
+
+ $createdName = $collection->createSearchIndex(
+ $mapping,
+ ['name' => $name],
+ );
+ $this->assertSame($name, $createdName);
+
+ $indexes = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+ $this->assertCount(1, $indexes);
+
+ $mapping = ['mappings' => ['dynamic' => true]];
+ $collection->updateSearchIndex($name, $mapping);
+
+ $indexes = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $this->assertCount(1, $indexes);
+ $this->assertSame($name, $indexes[0]->name);
+ $this->assertSameDocument($mapping, $indexes[0]->latestDefinition);
+ }
+
+ /**
+ * Case 5: dropSearchIndex suppresses namespace not found errors
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.rst#case-5-dropsearchindex-suppresses-namespace-not-found-errors
+ */
+ public function testDropSearchIndexSuppressNamespaceNotFoundError(): void
+ {
+ $collection = $this->dropCollection($this->getDatabaseName(), $this->getCollectionName());
+
+ $collection->dropSearchIndex('test-seach-index');
+
+ $this->expectNotToPerformAssertions();
+ }
+
+ /**
+ * Randomize the collection name to avoid duplicate index names when running tests concurrently.
+ * Search index operations are asynchronous and can take up to a few minutes.
+ */
+ protected function getCollectionName(): string
+ {
+ return sprintf('%s.%s', parent::getCollectionName(), bin2hex(random_bytes(5)));
+ }
+
+ private function waitForIndexes(Collection $collection, Closure $callback): array
+ {
+ $timeout = hrtime()[0] + self::WAIT_TIMEOUT_SEC;
+ while (hrtime()[0] < $timeout) {
+ sleep(5);
+ $result = $collection->listSearchIndexes();
+ $this->assertInstanceOf(CachingIterator::class, $result);
+ $result = iterator_to_array($result);
+ if ($callback($result)) {
+ return $result;
+ }
+ }
+
+ $this->fail('Operation did not complete in time');
+ }
+
+ private function allIndexesAreQueryable(array $indexes): bool
+ {
+ if (count($indexes) === 0) {
+ return false;
+ }
+
+ foreach ($indexes as $index) {
+ if (! $index->queryable) {
+ return false;
+ }
+
+ if (! $index->status === self::STATUS_READY) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 9b2f69982..4ecfe8cda 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -165,21 +165,26 @@ public function dataDescription(): string
return is_string($dataName) ? $dataName : '';
}
- public function provideInvalidArrayValues()
+ public function provideInvalidArrayValues(): array
{
return $this->wrapValuesForDataProvider($this->getInvalidArrayValues());
}
- public function provideInvalidDocumentValues()
+ public function provideInvalidDocumentValues(): array
{
return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
}
- public function provideInvalidIntegerValues()
+ public function provideInvalidIntegerValues(): array
{
return $this->wrapValuesForDataProvider($this->getInvalidIntegerValues());
}
+ public function provideInvalidStringValues(): array
+ {
+ return $this->wrapValuesForDataProvider($this->getInvalidStringValues());
+ }
+
protected function assertDeprecated(callable $execution): void
{
$errors = [];
diff --git a/tests/UnifiedSpecTests/Operation.php b/tests/UnifiedSpecTests/Operation.php
index 2eadc7eb0..224fd903c 100644
--- a/tests/UnifiedSpecTests/Operation.php
+++ b/tests/UnifiedSpecTests/Operation.php
@@ -541,6 +541,47 @@ private function executeForCollection(Collection $collection)
array_diff_key($args, ['to' => 1]),
);
+ case 'createSearchIndex':
+ $options = [];
+ if (isset($args['model']->name)) {
+ assertIsString($args['model']->name);
+ $options['name'] = $args['model']->name;
+ }
+
+ assertInstanceOf(stdClass::class, $args['model']->definition);
+
+ return $collection->createSearchIndex($args['model']->definition, $options);
+
+ case 'createSearchIndexes':
+ $indexes = array_map(function ($index) {
+ $index = (array) $index;
+ assertInstanceOf(stdClass::class, $index['definition']);
+
+ return $index;
+ }, $args['models']);
+
+ return $collection->createSearchIndexes($indexes);
+
+ case 'dropSearchIndex':
+ assertArrayHasKey('name', $args);
+ assertIsString($args['name']);
+
+ return $collection->dropSearchIndex($args['name']);
+
+ case 'updateSearchIndex':
+ assertArrayHasKey('name', $args);
+ assertArrayHasKey('definition', $args);
+ assertIsString($args['name']);
+ assertInstanceOf(stdClass::class, $args['definition']);
+
+ return $collection->updateSearchIndex($args['name'], $args['definition']);
+
+ case 'listSearchIndexes':
+ $args += (array) ($args['aggregationOptions'] ?? []);
+ unset($args['aggregationOptions']);
+
+ return $collection->listSearchIndexes($args);
+
default:
Assert::fail('Unsupported collection operation: ' . $this->name);
}
diff --git a/tests/UnifiedSpecTests/UnifiedSpecTest.php b/tests/UnifiedSpecTests/UnifiedSpecTest.php
index 1559ee439..9da22cf76 100644
--- a/tests/UnifiedSpecTests/UnifiedSpecTest.php
+++ b/tests/UnifiedSpecTests/UnifiedSpecTest.php
@@ -273,6 +273,25 @@ public function provideFailingTests()
yield from $this->provideTests(__DIR__ . '/valid-fail/*.json');
}
+ /** @dataProvider provideIndexManagementTests */
+ public function testIndexManagement(UnifiedTestCase $test): void
+ {
+ if (self::isAtlas()) {
+ self::markTestSkipped('Search Indexes tests must run on a non-Atlas cluster');
+ }
+
+ if (! self::isEnterprise()) {
+ self::markTestSkipped('Specific Atlas error messages are only available on Enterprise server');
+ }
+
+ self::$runner->run($test);
+ }
+
+ public function provideIndexManagementTests()
+ {
+ yield from $this->provideTests(__DIR__ . '/index-management/*.json');
+ }
+
private function provideTests(string $pattern): Generator
{
foreach (glob($pattern) as $filename) {
diff --git a/tests/UnifiedSpecTests/UnifiedTestRunner.php b/tests/UnifiedSpecTests/UnifiedTestRunner.php
index 35a0ec456..dc34f3752 100644
--- a/tests/UnifiedSpecTests/UnifiedTestRunner.php
+++ b/tests/UnifiedSpecTests/UnifiedTestRunner.php
@@ -49,8 +49,6 @@
*/
final class UnifiedTestRunner
{
- public const ATLAS_TLD = 'mongodb.net';
-
public const SERVER_ERROR_INTERRUPTED = 11601;
public const SERVER_ERROR_UNAUTHORIZED = 13;
@@ -83,7 +81,7 @@ public function __construct(string $internalClientUri)
/* Atlas prohibits killAllSessions. Inspect the connection string to
* determine if we should avoid calling killAllSessions(). This does
* mean that lingering transactions could block test execution. */
- if ($this->isServerless() || strpos($internalClientUri, self::ATLAS_TLD) !== false) {
+ if ($this->isServerless() || FunctionalTestCase::isAtlas($internalClientUri)) {
$this->allowKillAllSessions = false;
}
diff --git a/tests/UnifiedSpecTests/Util.php b/tests/UnifiedSpecTests/Util.php
index 3ddb8fcee..b7b44f11a 100644
--- a/tests/UnifiedSpecTests/Util.php
+++ b/tests/UnifiedSpecTests/Util.php
@@ -88,6 +88,8 @@ final class Util
'createChangeStream' => ['pipeline', 'session', 'fullDocument', 'fullDocumentBeforeChange', 'resumeAfter', 'startAfter', 'startAtOperationTime', 'batchSize', 'collation', 'maxAwaitTimeMS', 'comment', 'showExpandedEvents'],
'createFindCursor' => ['filter', 'session', 'allowDiskUse', 'allowPartialResults', 'batchSize', 'collation', 'comment', 'cursorType', 'hint', 'limit', 'max', 'maxAwaitTimeMS', 'maxScan', 'maxTimeMS', 'min', 'modifiers', 'noCursorTimeout', 'oplogReplay', 'projection', 'returnKey', 'showRecordId', 'skip', 'snapshot', 'sort'],
'createIndex' => ['keys', 'comment', 'commitQuorum', 'maxTimeMS', 'name', 'session', 'unique'],
+ 'createSearchIndex' => ['model'],
+ 'createSearchIndexes' => ['models'],
'dropIndex' => ['name', 'session', 'maxTimeMS', 'comment'],
'count' => ['filter', 'session', 'collation', 'hint', 'limit', 'maxTimeMS', 'skip', 'comment'],
'countDocuments' => ['filter', 'session', 'limit', 'skip', 'collation', 'hint', 'maxTimeMS', 'comment'],
@@ -97,6 +99,7 @@ final class Util
'findOneAndDelete' => ['let', 'filter', 'session', 'projection', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'maxTimeMS', 'new', 'sort', 'update', 'upsert', 'comment'],
'distinct' => ['fieldName', 'filter', 'session', 'collation', 'maxTimeMS', 'comment'],
'drop' => ['session', 'comment'],
+ 'dropSearchIndex' => ['name'],
'find' => ['let', 'filter', 'session', 'allowDiskUse', 'allowPartialResults', 'batchSize', 'collation', 'comment', 'cursorType', 'hint', 'limit', 'max', 'maxAwaitTimeMS', 'maxScan', 'maxTimeMS', 'min', 'modifiers', 'noCursorTimeout', 'oplogReplay', 'projection', 'returnKey', 'showRecordId', 'skip', 'snapshot', 'sort'],
'findOne' => ['let', 'filter', 'session', 'allowDiskUse', 'allowPartialResults', 'batchSize', 'collation', 'comment', 'cursorType', 'hint', 'max', 'maxAwaitTimeMS', 'maxScan', 'maxTimeMS', 'min', 'modifiers', 'noCursorTimeout', 'oplogReplay', 'projection', 'returnKey', 'showRecordId', 'skip', 'snapshot', 'sort'],
'findOneAndReplace' => ['let', 'returnDocument', 'filter', 'replacement', 'session', 'projection', 'returnDocument', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'maxTimeMS', 'new', 'remove', 'sort', 'comment'],
@@ -105,9 +108,11 @@ final class Util
'findOneAndUpdate' => ['let', 'returnDocument', 'filter', 'update', 'session', 'upsert', 'projection', 'remove', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'maxTimeMS', 'sort', 'comment'],
'updateMany' => ['let', 'filter', 'update', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment'],
'updateOne' => ['let', 'filter', 'update', 'session', 'upsert', 'arrayFilters', 'bypassDocumentValidation', 'collation', 'hint', 'comment'],
+ 'updateSearchIndex' => ['name', 'definition'],
'insertMany' => ['documents', 'session', 'ordered', 'bypassDocumentValidation', 'comment'],
'insertOne' => ['document', 'session', 'bypassDocumentValidation', 'comment'],
'listIndexes' => ['session', 'maxTimeMS', 'comment'],
+ 'listSearchIndexes' => ['name', 'aggregationOptions'],
'mapReduce' => ['map', 'reduce', 'out', 'session', 'bypassDocumentValidation', 'collation', 'finalize', 'jsMode', 'limit', 'maxTimeMS', 'query', 'scope', 'sort', 'verbose', 'comment'],
],
ChangeStream::class => [
diff --git a/tests/UnifiedSpecTests/index-management/createSearchIndex.json b/tests/UnifiedSpecTests/index-management/createSearchIndex.json
new file mode 100644
index 000000000..04cffbe9c
--- /dev/null
+++ b/tests/UnifiedSpecTests/index-management/createSearchIndex.json
@@ -0,0 +1,136 @@
+{
+ "description": "createSearchIndex",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "tests": [
+ {
+ "description": "no name provided for an index definition",
+ "operations": [
+ {
+ "name": "createSearchIndex",
+ "object": "collection0",
+ "arguments": {
+ "model": {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "createSearchIndexes": "collection0",
+ "indexes": [
+ {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ }
+ }
+ ],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "name provided for an index definition",
+ "operations": [
+ {
+ "name": "createSearchIndex",
+ "object": "collection0",
+ "arguments": {
+ "model": {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ },
+ "name": "test index"
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "createSearchIndexes": "collection0",
+ "indexes": [
+ {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ },
+ "name": "test index"
+ }
+ ],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/UnifiedSpecTests/index-management/createSearchIndexes.json b/tests/UnifiedSpecTests/index-management/createSearchIndexes.json
new file mode 100644
index 000000000..95dbedde7
--- /dev/null
+++ b/tests/UnifiedSpecTests/index-management/createSearchIndexes.json
@@ -0,0 +1,172 @@
+{
+ "description": "createSearchIndexes",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "tests": [
+ {
+ "description": "empty index definition array",
+ "operations": [
+ {
+ "name": "createSearchIndexes",
+ "object": "collection0",
+ "arguments": {
+ "models": []
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "createSearchIndexes": "collection0",
+ "indexes": [],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "no name provided for an index definition",
+ "operations": [
+ {
+ "name": "createSearchIndexes",
+ "object": "collection0",
+ "arguments": {
+ "models": [
+ {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ }
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "createSearchIndexes": "collection0",
+ "indexes": [
+ {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ }
+ }
+ ],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "name provided for an index definition",
+ "operations": [
+ {
+ "name": "createSearchIndexes",
+ "object": "collection0",
+ "arguments": {
+ "models": [
+ {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ },
+ "name": "test index"
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "createSearchIndexes": "collection0",
+ "indexes": [
+ {
+ "definition": {
+ "mappings": {
+ "dynamic": true
+ }
+ },
+ "name": "test index"
+ }
+ ],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/UnifiedSpecTests/index-management/dropSearchIndex.json b/tests/UnifiedSpecTests/index-management/dropSearchIndex.json
new file mode 100644
index 000000000..0f21a5b68
--- /dev/null
+++ b/tests/UnifiedSpecTests/index-management/dropSearchIndex.json
@@ -0,0 +1,74 @@
+{
+ "description": "dropSearchIndex",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "tests": [
+ {
+ "description": "sends the correct command",
+ "operations": [
+ {
+ "name": "dropSearchIndex",
+ "object": "collection0",
+ "arguments": {
+ "name": "test index"
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "dropSearchIndex": "collection0",
+ "name": "test index",
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/UnifiedSpecTests/index-management/listSearchIndexes.json b/tests/UnifiedSpecTests/index-management/listSearchIndexes.json
new file mode 100644
index 000000000..24c51ad88
--- /dev/null
+++ b/tests/UnifiedSpecTests/index-management/listSearchIndexes.json
@@ -0,0 +1,156 @@
+{
+ "description": "listSearchIndexes",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "tests": [
+ {
+ "description": "when no name is provided, it does not populate the filter",
+ "operations": [
+ {
+ "name": "listSearchIndexes",
+ "object": "collection0",
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$listSearchIndexes": {}
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "when a name is provided, it is present in the filter",
+ "operations": [
+ {
+ "name": "listSearchIndexes",
+ "object": "collection0",
+ "arguments": {
+ "name": "test index"
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$listSearchIndexes": {
+ "name": "test index"
+ }
+ }
+ ],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "aggregation cursor options are supported",
+ "operations": [
+ {
+ "name": "listSearchIndexes",
+ "object": "collection0",
+ "arguments": {
+ "name": "test index",
+ "aggregationOptions": {
+ "batchSize": 10
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {
+ "batchSize": 10
+ },
+ "pipeline": [
+ {
+ "$listSearchIndexes": {
+ "name": "test index"
+ }
+ }
+ ],
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/UnifiedSpecTests/index-management/updateSearchIndex.json b/tests/UnifiedSpecTests/index-management/updateSearchIndex.json
new file mode 100644
index 000000000..88a46a306
--- /dev/null
+++ b/tests/UnifiedSpecTests/index-management/updateSearchIndex.json
@@ -0,0 +1,76 @@
+{
+ "description": "updateSearchIndex",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "tests": [
+ {
+ "description": "sends the correct command",
+ "operations": [
+ {
+ "name": "updateSearchIndex",
+ "object": "collection0",
+ "arguments": {
+ "name": "test index",
+ "definition": {}
+ },
+ "expectError": {
+ "isError": true,
+ "errorContains": "Search index commands are only supported with Atlas"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "updateSearchIndex": "collection0",
+ "name": "test index",
+ "definition": {},
+ "$db": "database0"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}