From cadfba01030a71e388cfa63f98eaab367f92e35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 5 Nov 2025 14:11:18 -0500 Subject: [PATCH 1/2] PHPLIB-1689 Make the Atlas Search exception more specific --- src/Exception/SearchNotSupportedException.php | 31 +++++++++++++++++++ src/Operation/Aggregate.php | 12 ++++++- src/Operation/CreateSearchIndexes.php | 12 ++++++- tests/Collection/CollectionFunctionalTest.php | 27 ++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/Exception/SearchNotSupportedException.php diff --git a/src/Exception/SearchNotSupportedException.php b/src/Exception/SearchNotSupportedException.php new file mode 100644 index 000000000..2c1312b31 --- /dev/null +++ b/src/Exception/SearchNotSupportedException.php @@ -0,0 +1,31 @@ +getCode(), $e); + } + + public static function isSearchNotSupportedError(Throwable $e): bool + { + if (! $e instanceof ServerException) { + return false; + } + + return in_array($e->getCode(), [ + 59, // MongoDB 4 to 6, 7-community: no such command: 'createSearchIndexes' + 40324, // MongoDB 4 to 6: Unrecognized pipeline stage name: '$listSearchIndexes' + 115, // MongoDB 7-ent: Search index commands are only supported with Atlas. + 6047401, // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas + 31082, // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. + ], true); + } +} diff --git a/src/Operation/Aggregate.php b/src/Operation/Aggregate.php index b5da6470c..4d120f9c4 100644 --- a/src/Operation/Aggregate.php +++ b/src/Operation/Aggregate.php @@ -21,12 +21,14 @@ use MongoDB\Driver\Command; use MongoDB\Driver\CursorInterface; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; +use MongoDB\Driver\Exception\ServerException; use MongoDB\Driver\ReadConcern; use MongoDB\Driver\ReadPreference; use MongoDB\Driver\Server; use MongoDB\Driver\Session; use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Exception\SearchNotSupportedException; use MongoDB\Exception\UnexpectedValueException; use MongoDB\Exception\UnsupportedException; use MongoDB\Model\CodecCursor; @@ -233,7 +235,15 @@ public function execute(Server $server): CursorInterface $this->createCommandOptions(), ); - $cursor = $this->executeCommand($server, $command); + try { + $cursor = $this->executeCommand($server, $command); + } catch (ServerException $exception) { + if (SearchNotSupportedException::isSearchNotSupportedError($exception)) { + throw SearchNotSupportedException::create($exception); + } + + throw $exception; + } if (isset($this->options['codec'])) { return CodecCursor::fromCursor($cursor, $this->options['codec']); diff --git a/src/Operation/CreateSearchIndexes.php b/src/Operation/CreateSearchIndexes.php index d21ed9428..0b5b59573 100644 --- a/src/Operation/CreateSearchIndexes.php +++ b/src/Operation/CreateSearchIndexes.php @@ -19,8 +19,10 @@ use MongoDB\Driver\Command; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; +use MongoDB\Driver\Exception\ServerException; use MongoDB\Driver\Server; use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Exception\SearchNotSupportedException; use MongoDB\Exception\UnsupportedException; use MongoDB\Model\SearchIndexInput; @@ -83,7 +85,15 @@ public function execute(Server $server): array $cmd['comment'] = $this->options['comment']; } - $cursor = $server->executeCommand($this->databaseName, new Command($cmd)); + try { + $cursor = $server->executeCommand($this->databaseName, new Command($cmd)); + } catch (ServerException $exception) { + if (SearchNotSupportedException::isSearchNotSupportedError($exception)) { + throw SearchNotSupportedException::create($exception); + } + + throw $exception; + } /** @var object{indexesCreated: list} $result */ $result = current($cursor->toArray()); diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index 95659381c..31d965a99 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -13,6 +13,7 @@ use MongoDB\Driver\ReadPreference; use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Exception\SearchNotSupportedException; use MongoDB\Exception\UnsupportedException; use MongoDB\Operation\Count; use MongoDB\Tests\CommandObserver; @@ -807,6 +808,32 @@ public function testListSearchIndexesInheritTypeMap(): void $this->assertIsArray($indexes[0]); } + public function testListSearchIndexesNotSupportedException(): void + { + if (self::isAtlas()) { + self::markTestSkipped('Atlas Search is supported on Atlas'); + } + + $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName()); + + $this->expectException(SearchNotSupportedException::class); + + $collection->listSearchIndexes(); + } + + public function testCreateSearchIndexNotSupportedException(): void + { + if (self::isAtlas()) { + self::markTestSkipped('Atlas Search is supported on Atlas'); + } + + $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName()); + + $this->expectException(SearchNotSupportedException::class); + + $collection->createSearchIndex(['mappings' => ['dynamic' => false]], ['name' => 'test-search-index']); + } + /** * Create data fixtures. */ From 50fbc84e5ab382661f9c9bed96b9269c6e5b75d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 18 Nov 2025 18:00:53 +0100 Subject: [PATCH 2/2] Use error message when error code is not specific to atlas search on old server versions --- phpunit.xml.dist | 1 + .../AtlasSearchNotSupportedException.php | 40 ++++++++++++ src/Exception/SearchNotSupportedException.php | 31 ---------- src/Operation/Aggregate.php | 6 +- src/Operation/CreateSearchIndexes.php | 6 +- tests/Collection/CollectionFunctionalTest.php | 27 -------- tests/ExamplesTest.php | 4 +- .../AtlasSearchNotSupportedExceptionTest.php | 62 +++++++++++++++++++ tests/FunctionalTestCase.php | 4 +- 9 files changed, 111 insertions(+), 70 deletions(-) create mode 100644 src/Exception/AtlasSearchNotSupportedException.php delete mode 100644 src/Exception/SearchNotSupportedException.php create mode 100644 tests/Exception/AtlasSearchNotSupportedExceptionTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b3b46625b..be9b7cd48 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,6 +16,7 @@ + diff --git a/src/Exception/AtlasSearchNotSupportedException.php b/src/Exception/AtlasSearchNotSupportedException.php new file mode 100644 index 000000000..b65b1620b --- /dev/null +++ b/src/Exception/AtlasSearchNotSupportedException.php @@ -0,0 +1,40 @@ +getCode() === 31082 ? $e->getMessage() : 'Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. Please connect to Atlas or an AtlasCLI local deployment to enable. For more information on how to connect, see https://dochub.mongodb.org/core/atlas-cli-deploy-local-reqs'; + + return new self($message, $e->getCode(), $e); + } + + /** @internal */ + public static function isAtlasSearchNotSupportedError(Throwable $e): bool + { + if (! $e instanceof ServerException) { + return false; + } + + return match ($e->getCode()) { + // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. + 31082 => true, + // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas + 6047401 => true, + // MongoDB 7-ent: Search index commands are only supported with Atlas. + 115 => true, + // MongoDB 4 to 6, 7-community + 59 => 'no such command: \'createSearchIndexes\'' === $e->getMessage(), + // MongoDB 4 to 6 + 40324 => 'Unrecognized pipeline stage name: \'$listSearchIndexes\'' === $e->getMessage(), + // Not an Atlas Search error + default => false, + }; + } +} diff --git a/src/Exception/SearchNotSupportedException.php b/src/Exception/SearchNotSupportedException.php deleted file mode 100644 index 2c1312b31..000000000 --- a/src/Exception/SearchNotSupportedException.php +++ /dev/null @@ -1,31 +0,0 @@ -getCode(), $e); - } - - public static function isSearchNotSupportedError(Throwable $e): bool - { - if (! $e instanceof ServerException) { - return false; - } - - return in_array($e->getCode(), [ - 59, // MongoDB 4 to 6, 7-community: no such command: 'createSearchIndexes' - 40324, // MongoDB 4 to 6: Unrecognized pipeline stage name: '$listSearchIndexes' - 115, // MongoDB 7-ent: Search index commands are only supported with Atlas. - 6047401, // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas - 31082, // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. - ], true); - } -} diff --git a/src/Operation/Aggregate.php b/src/Operation/Aggregate.php index 4d120f9c4..6827bb07f 100644 --- a/src/Operation/Aggregate.php +++ b/src/Operation/Aggregate.php @@ -27,8 +27,8 @@ use MongoDB\Driver\Server; use MongoDB\Driver\Session; use MongoDB\Driver\WriteConcern; +use MongoDB\Exception\AtlasSearchNotSupportedException; use MongoDB\Exception\InvalidArgumentException; -use MongoDB\Exception\SearchNotSupportedException; use MongoDB\Exception\UnexpectedValueException; use MongoDB\Exception\UnsupportedException; use MongoDB\Model\CodecCursor; @@ -238,8 +238,8 @@ public function execute(Server $server): CursorInterface try { $cursor = $this->executeCommand($server, $command); } catch (ServerException $exception) { - if (SearchNotSupportedException::isSearchNotSupportedError($exception)) { - throw SearchNotSupportedException::create($exception); + if (AtlasSearchNotSupportedException::isAtlasSearchNotSupportedError($exception)) { + throw AtlasSearchNotSupportedException::create($exception); } throw $exception; diff --git a/src/Operation/CreateSearchIndexes.php b/src/Operation/CreateSearchIndexes.php index 0b5b59573..2738b7291 100644 --- a/src/Operation/CreateSearchIndexes.php +++ b/src/Operation/CreateSearchIndexes.php @@ -21,8 +21,8 @@ use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Exception\ServerException; use MongoDB\Driver\Server; +use MongoDB\Exception\AtlasSearchNotSupportedException; use MongoDB\Exception\InvalidArgumentException; -use MongoDB\Exception\SearchNotSupportedException; use MongoDB\Exception\UnsupportedException; use MongoDB\Model\SearchIndexInput; @@ -88,8 +88,8 @@ public function execute(Server $server): array try { $cursor = $server->executeCommand($this->databaseName, new Command($cmd)); } catch (ServerException $exception) { - if (SearchNotSupportedException::isSearchNotSupportedError($exception)) { - throw SearchNotSupportedException::create($exception); + if (AtlasSearchNotSupportedException::isAtlasSearchNotSupportedError($exception)) { + throw AtlasSearchNotSupportedException::create($exception); } throw $exception; diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index 31d965a99..95659381c 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -13,7 +13,6 @@ use MongoDB\Driver\ReadPreference; use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; -use MongoDB\Exception\SearchNotSupportedException; use MongoDB\Exception\UnsupportedException; use MongoDB\Operation\Count; use MongoDB\Tests\CommandObserver; @@ -808,32 +807,6 @@ public function testListSearchIndexesInheritTypeMap(): void $this->assertIsArray($indexes[0]); } - public function testListSearchIndexesNotSupportedException(): void - { - if (self::isAtlas()) { - self::markTestSkipped('Atlas Search is supported on Atlas'); - } - - $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName()); - - $this->expectException(SearchNotSupportedException::class); - - $collection->listSearchIndexes(); - } - - public function testCreateSearchIndexNotSupportedException(): void - { - if (self::isAtlas()) { - self::markTestSkipped('Atlas Search is supported on Atlas'); - } - - $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName()); - - $this->expectException(SearchNotSupportedException::class); - - $collection->createSearchIndex(['mappings' => ['dynamic' => false]], ['name' => 'test-search-index']); - } - /** * Create data fixtures. */ diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 78ade169c..642798a97 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use function bin2hex; -use function getenv; use function putenv; use function random_bytes; use function sprintf; @@ -230,8 +229,7 @@ public static function provideExamples(): Generator #[Group('atlas')] public function testAtlasSearch(): void { - $uri = getenv('MONGODB_URI') ?? ''; - if (! self::isAtlas($uri)) { + if (! self::isAtlas()) { $this->markTestSkipped('Atlas Search examples are only supported on MongoDB Atlas'); } diff --git a/tests/Exception/AtlasSearchNotSupportedExceptionTest.php b/tests/Exception/AtlasSearchNotSupportedExceptionTest.php new file mode 100644 index 000000000..5d8d99498 --- /dev/null +++ b/tests/Exception/AtlasSearchNotSupportedExceptionTest.php @@ -0,0 +1,62 @@ +manager, $this->getDatabaseName(), $this->getCollectionName()); + + $this->expectException(AtlasSearchNotSupportedException::class); + + $collection->listSearchIndexes(); + } + + public function testCreateSearchIndexNotSupportedException(): void + { + if (self::isAtlas()) { + self::markTestSkipped('Atlas Search is supported on Atlas'); + } + + $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName()); + + $this->expectException(AtlasSearchNotSupportedException::class); + + $collection->createSearchIndex(['mappings' => ['dynamic' => false]], ['name' => 'test-search-index']); + } + + public function testOtherStageNotFound(): void + { + $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName()); + + try { + $collection->aggregate([ + ['$searchStageNotExisting' => ['text' => ['query' => 'test', 'path' => 'field']]], + ]); + self::fail('Expected ServerException was not thrown'); + } catch (ServerException $exception) { + self::assertNotInstanceOf(AtlasSearchNotSupportedException::class, $exception, $exception); + } + } + + public function testOtherCommandNotFound(): void + { + try { + $this->manager->executeCommand($this->getDatabaseName(), new Command(['nonExistingCommand' => 1])); + self::fail('Expected ServerException was not thrown'); + } catch (ServerException $exception) { + self::assertFalse(AtlasSearchNotSupportedException::isAtlasSearchNotSupportedError($exception)); + } + } +} diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php index 156d80149..ea71accec 100644 --- a/tests/FunctionalTestCase.php +++ b/tests/FunctionalTestCase.php @@ -50,8 +50,6 @@ abstract class FunctionalTestCase extends TestCase { - private const ATLAS_TLD = '/\.(mongodb\.net|mongodb-dev\.net)/'; - protected Manager $manager; private array $configuredFailPoints = []; @@ -520,7 +518,7 @@ protected function isEnterprise(): bool public static function isAtlas(?string $uri = null): bool { - return preg_match(self::ATLAS_TLD, $uri ?? static::getUri()); + return (bool) getenv('ATLAS_SUPPORTED'); } /** @see https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/shared-library/ */