diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index e6b0a3436..84b6373ad 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -28,6 +28,11 @@
($value is NativeType ? BSONType : $value)
+
+
+ $options
+
+
$cmd[$option]
@@ -309,6 +314,14 @@
isInTransaction
+
+
+ (array) $index
+
+
+ $cmd[$option]
+
+
options['typeMap']]]>
@@ -389,6 +402,11 @@
isInTransaction
+
+
+ $cmd[$option]
+
+
options['typeMap']]]>
@@ -537,6 +555,11 @@
isInTransaction
+
+
+ $cmd[$option]
+
+
cursor->firstBatch]]>
diff --git a/src/Collection.php b/src/Collection.php
index 801511951..4b552e385 100644
--- a/src/Collection.php
+++ b/src/Collection.php
@@ -17,8 +17,11 @@
namespace MongoDB;
+use Countable;
+use Iterator;
use MongoDB\BSON\JavascriptInterface;
use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadConcern;
@@ -36,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;
@@ -53,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;
@@ -377,6 +384,64 @@ public function createIndexes(array $indexes, array $options = [])
return $operation->execute($server);
}
+ /**
+ * 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.
*
@@ -554,6 +619,31 @@ public function dropIndexes(array $options = [])
return $operation->execute($server);
}
+ /**
+ * 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);
+
+ try {
+ $operation->execute($server);
+ } catch (CommandException $e) {
+ // Suppress namespace not found errors for idempotency
+ if ($e->getCode() !== 26) {
+ throw $e;
+ }
+ }
+ }
+
/**
* Gets an estimated number of documents in the collection using the collection metadata.
*
@@ -928,6 +1018,24 @@ public function listIndexes(array $options = [])
return $operation->execute($server);
}
+ /**
+ * 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.
*
@@ -1088,6 +1196,25 @@ public function updateOne($filter, $update, array $options = [])
return $operation->execute($server);
}
+ /**
+ * 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..00f1ea6cb
--- /dev/null
+++ b/src/Operation/CreateSearchIndexes.php
@@ -0,0 +1,103 @@
+ $indexes List of search index specifications
+ * @param array{comment?: mixed} $options Command options
+ * @throws InvalidArgumentException for parameter parsing errors
+ */
+ public function __construct(string $databaseName, string $collectionName, array $indexes, array $options)
+ {
+ if (! array_is_list($indexes)) {
+ throw new InvalidArgumentException('$indexes is not a list');
+ }
+
+ foreach ($indexes as $i => $index) {
+ if (! is_document($index)) {
+ throw InvalidArgumentException::expectedDocumentType(sprintf('$indexes[%d]', $i), $index);
+ }
+
+ $this->indexes[] = new SearchIndexInput((array) $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,
+ ];
+
+ foreach (['comment'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = $this->options[$option];
+ }
+ }
+
+ $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..3fad9b967
--- /dev/null
+++ b/src/Operation/DropSearchIndex.php
@@ -0,0 +1,82 @@
+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,
+ ];
+
+ foreach (['comment'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = $this->options[$option];
+ }
+ }
+
+ $server->executeCommand($this->databaseName, new Command($cmd));
+ }
+}
diff --git a/src/Operation/ListSearchIndexes.php b/src/Operation/ListSearchIndexes.php
new file mode 100644
index 000000000..792428159
--- /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..fab1917e4
--- /dev/null
+++ b/src/Operation/UpdateSearchIndex.php
@@ -0,0 +1,93 @@
+databaseName = $databaseName;
+ $this->collectionName = $collectionName;
+ $this->name = $name;
+ $this->definition = document_to_array($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,
+ ];
+
+ foreach (['comment'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = $this->options[$option];
+ }
+ }
+
+ $server->executeCommand($this->databaseName, new Command($cmd));
+ }
+}
diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php
index 2a79e6b19..87e97ed3f 100644
--- a/tests/FunctionalTestCase.php
+++ b/tests/FunctionalTestCase.php
@@ -51,6 +51,8 @@
abstract class FunctionalTestCase extends TestCase
{
+ private const ATLAS_TLD = '/\.(mongodb\.net|mongodb-dev\.net)/';
+
protected Manager $manager;
private array $configuredFailPoints = [];
@@ -519,6 +521,11 @@ protected function skipIfTransactionsAreNotSupported(): void
}
}
+ public static function isAtlas(): bool
+ {
+ return preg_match(self::ATLAS_TLD, getenv('MONGODB_URI') ?: '');
+ }
+
/** @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..e9dad4f89 100644
--- a/tests/Model/IndexInputTest.php
+++ b/tests/Model/IndexInputTest.php
@@ -15,12 +15,14 @@ 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
{
$this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "key" option to have type "document"');
new IndexInput(['key' => 'foo']);
}
@@ -28,6 +30,7 @@ public function testConstructorShouldRequireKeyToBeArrayOrObject(): void
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]]);
}
@@ -39,6 +42,7 @@ public function provideInvalidFieldOrderValues()
public function testConstructorShouldRequireNameToBeString(): void
{
$this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "name" option to have type "string"');
new IndexInput(['key' => ['x' => 1], 'name' => 1]);
}
@@ -67,7 +71,7 @@ public function provideExpectedNameAndKey(): array
public function testBsonSerialization(): void
{
- $expected = [
+ $expected = (object) [
'key' => ['x' => 1],
'unique' => true,
'name' => 'x_1',
@@ -79,6 +83,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..3c6fbe142
--- /dev/null
+++ b/tests/Model/SearchIndexInputTest.php
@@ -0,0 +1,48 @@
+expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Required "definition" document is missing from search index specification');
+ new SearchIndexInput([]);
+ }
+
+ public function testConstructorIndexDefinitionMustBeADocument(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "definition" option to have type "document"');
+ new SearchIndexInput(['definition' => 'foo']);
+ }
+
+ public function testConstructorShouldRequireNameToBeString(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected "name" option to have type "string"');
+ new SearchIndexInput(['definition' => ['mapping' => ['dynamid' => true]], 'name' => 1]);
+ }
+
+ 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 5da144343..505ae9698 100644
--- a/tests/Operation/BulkWriteTest.php
+++ b/tests/Operation/BulkWriteTest.php
@@ -92,11 +92,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..678678f59
--- /dev/null
+++ b/tests/Operation/CreateSearchIndexesTest.php
@@ -0,0 +1,52 @@
+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 provideInvalidIndexSpecificationTypes */
+ public function testConstructorIndexDefinitionMustBeADocument($index): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Expected $indexes[0] to have type "document"');
+ 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]], []);
+ }
+
+ public function provideInvalidIndexSpecificationTypes(): array
+ {
+ return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
+ }
+}
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..58032b4fa
--- /dev/null
+++ b/tests/SpecTests/SearchIndexSpecTest.php
@@ -0,0 +1,188 @@
+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);
+
+ [$index] = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $this->assertSame($name, $index->name);
+
+ // Convert to JSON to compare nested associative arrays and nested objects
+ $this->assertJsonStringEqualsJsonString(
+ json_encode($mapping, JSON_THROW_ON_ERROR),
+ json_encode($index->latestDefinition, JSON_THROW_ON_ERROR),
+ );
+ }
+
+ 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));
+
+ foreach ($names as $key => $name) {
+ $index = $indexes[$key];
+ $this->assertSame($name, $index->name);
+
+ // Convert to JSON to compare nested associative arrays and nested objects
+ $this->assertJsonStringEqualsJsonString(
+ json_encode($mapping, JSON_THROW_ON_ERROR),
+ json_encode($index->latestDefinition, JSON_THROW_ON_ERROR),
+ );
+ }
+ }
+
+ 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);
+
+ $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $collection->dropSearchIndex($name);
+
+ $indexes = $this->waitForIndexes($collection, fn (array $indexes): bool => count($indexes) === 0);
+
+ $this->assertCount(0, $indexes);
+ }
+
+ 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);
+
+ $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $mapping = ['mappings' => ['dynamic' => true]];
+ $collection->updateSearchIndex($name, $mapping);
+
+ [$index] = $this->waitForIndexes($collection, fn ($indexes) => $this->allIndexesAreQueryable($indexes));
+
+ $this->assertSame($name, $index->name);
+
+ // Convert to JSON to compare nested associative arrays and nested objects
+ $this->assertJsonStringEqualsJsonString(
+ json_encode($mapping, JSON_THROW_ON_ERROR),
+ json_encode($index->latestDefinition, JSON_THROW_ON_ERROR),
+ );
+ }
+
+ 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;
+ 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 === 'READY') {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index a5d20b837..19c386f2b 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -164,21 +164,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..6bef86c32 100644
--- a/tests/UnifiedSpecTests/Operation.php
+++ b/tests/UnifiedSpecTests/Operation.php
@@ -541,6 +541,35 @@ 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;
+ }
+
+ return $collection->createSearchIndex($args['model']->definition, $options);
+
+ case 'createSearchIndexes':
+ return $collection->createSearchIndexes($args['models']);
+
+ 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':
+ return $collection->listSearchIndexes($args + (array) ($args['aggregationOptions'] ?? []));
+
default:
Assert::fail('Unsupported collection operation: ' . $this->name);
}
diff --git a/tests/UnifiedSpecTests/UnifiedSpecTest.php b/tests/UnifiedSpecTests/UnifiedSpecTest.php
index 1559ee439..f83c336a8 100644
--- a/tests/UnifiedSpecTests/UnifiedSpecTest.php
+++ b/tests/UnifiedSpecTests/UnifiedSpecTest.php
@@ -273,6 +273,23 @@ 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');
+ }
+
+ self::skipIfServerVersion('<', '7.0.0', 'A dedicated error message was introduced in 7.0.0');
+
+ 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/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"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}