Skip to content

Commit 10ec1da

Browse files
authoredAug 26, 2024··
Gracefully handle search index exceptions if possible (#2671)
* Gracefully handle search index exceptions if possible * Handle search index exceptions on older server versions
1 parent d6a56ff commit 10ec1da

File tree

2 files changed

+147
-3
lines changed

2 files changed

+147
-3
lines changed
 

‎lib/Doctrine/ODM/MongoDB/SchemaManager.php

+40-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface;
99
use Doctrine\ODM\MongoDB\Repository\ViewRepository;
1010
use InvalidArgumentException;
11+
use MongoDB\Driver\Exception\CommandException;
1112
use MongoDB\Driver\Exception\RuntimeException;
1213
use MongoDB\Driver\Exception\ServerException;
1314
use MongoDB\Driver\WriteConcern;
@@ -32,6 +33,7 @@
3233
use function iterator_to_array;
3334
use function ksort;
3435
use function sprintf;
36+
use function str_contains;
3537

3638
/**
3739
* @psalm-import-type IndexMapping from ClassMetadata
@@ -44,6 +46,7 @@ final class SchemaManager
4446
private const GRIDFS_CHUNKS_COLLECTION_INDEX = ['filename' => 1, 'uploadDate' => 1];
4547

4648
private const CODE_SHARDING_ALREADY_INITIALIZED = 23;
49+
private const CODE_COMMAND_NOT_SUPPORTED = 115;
4750

4851
private const ALLOWED_MISSING_INDEX_OPTIONS = [
4952
'background',
@@ -408,8 +411,19 @@ public function updateDocumentSearchIndexes(string $documentName): void
408411
$searchIndexes = $class->getSearchIndexes();
409412
$collection = $this->dm->getDocumentCollection($class->name);
410413

411-
$definedNames = array_column($searchIndexes, 'name');
412-
$existingNames = array_column(iterator_to_array($collection->listSearchIndexes()), 'name');
414+
$definedNames = array_column($searchIndexes, 'name');
415+
try {
416+
$existingNames = array_column(iterator_to_array($collection->listSearchIndexes()), 'name');
417+
} catch (CommandException $e) {
418+
/* If $listSearchIndexes doesn't exist, only throw if search indexes have been defined.
419+
* If no search indexes are defined and the server doesn't support search indexes, there's
420+
* nothing for us to do here and we can safely return */
421+
if ($definedNames === [] && $this->isSearchIndexCommandException($e)) {
422+
return;
423+
}
424+
425+
throw $e;
426+
}
413427

414428
foreach (array_diff($existingNames, $definedNames) as $name) {
415429
$collection->dropSearchIndex($name);
@@ -450,7 +464,18 @@ public function deleteDocumentSearchIndexes(string $documentName): void
450464

451465
$collection = $this->dm->getDocumentCollection($class->name);
452466

453-
foreach ($collection->listSearchIndexes() as $searchIndex) {
467+
try {
468+
$searchIndexes = $collection->listSearchIndexes();
469+
} catch (CommandException $e) {
470+
// If the server does not support search indexes, there are no indexes to remove in any case
471+
if ($this->isSearchIndexCommandException($e)) {
472+
return;
473+
}
474+
475+
throw $e;
476+
}
477+
478+
foreach ($searchIndexes as $searchIndex) {
454479
$collection->dropSearchIndex($searchIndex['name']);
455480
}
456481
}
@@ -1029,4 +1054,16 @@ private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeCon
10291054

10301055
return $options;
10311056
}
1057+
1058+
private function isSearchIndexCommandException(CommandException $e): bool
1059+
{
1060+
// MongoDB 6.0.7+ and 7.0+: "Search indexes are only available on Atlas"
1061+
if ($e->getCode() === self::CODE_COMMAND_NOT_SUPPORTED && str_contains($e->getMessage(), 'Search index')) {
1062+
return true;
1063+
}
1064+
1065+
// Older server versions don't support $listSearchIndexes
1066+
// We don't check for an error code here as the code is not documented and we can't rely on it
1067+
return str_contains($e->getMessage(), 'Unrecognized pipeline stage name: \'$listSearchIndexes\'');
1068+
}
10321069
}

‎tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php

+107
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use MongoDB\Client;
2727
use MongoDB\Collection;
2828
use MongoDB\Database;
29+
use MongoDB\Driver\Exception\CommandException;
2930
use MongoDB\Driver\WriteConcern;
3031
use MongoDB\GridFS\Bucket;
3132
use MongoDB\Model\CollectionInfo;
@@ -420,6 +421,27 @@ public function testCreateDocumentSearchIndexes(): void
420421
$this->schemaManager->createDocumentSearchIndexes(CmsArticle::class);
421422
}
422423

424+
public function testCreateDocumentSearchIndexesNotSupported(): void
425+
{
426+
$exception = $this->createSearchIndexCommandException();
427+
428+
$cmsArticleCollectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection();
429+
foreach ($this->documentCollections as $collectionName => $collection) {
430+
if ($collectionName === $cmsArticleCollectionName) {
431+
$collection
432+
->expects($this->once())
433+
->method('createSearchIndexes')
434+
->with($this->anything())
435+
->willThrowException($exception);
436+
} else {
437+
$collection->expects($this->never())->method('createSearchIndexes');
438+
}
439+
}
440+
441+
$this->expectExceptionObject($exception);
442+
$this->schemaManager->createDocumentSearchIndexes(CmsArticle::class);
443+
}
444+
423445
public function testUpdateDocumentSearchIndexes(): void
424446
{
425447
$collectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection();
@@ -443,6 +465,66 @@ public function testUpdateDocumentSearchIndexes(): void
443465
$this->schemaManager->updateDocumentSearchIndexes(CmsArticle::class);
444466
}
445467

468+
public function testUpdateDocumentSearchIndexesNotSupportedForClassWithoutSearchIndexes(): void
469+
{
470+
// Class has no search indexes, so if the server doesn't support them we assume everything is fine
471+
$collectionName = $this->dm->getClassMetadata(CmsProduct::class)->getCollection();
472+
$collection = $this->documentCollections[$collectionName];
473+
$collection
474+
->expects($this->once())
475+
->method('listSearchIndexes')
476+
->willThrowException($this->createSearchIndexCommandException());
477+
$collection
478+
->expects($this->never())
479+
->method('dropSearchIndex');
480+
$collection
481+
->expects($this->never())
482+
->method('updateSearchIndex');
483+
484+
$this->schemaManager->updateDocumentSearchIndexes(CmsProduct::class);
485+
}
486+
487+
public function testUpdateDocumentSearchIndexesNotSupportedForClassWithoutSearchIndexesOnOlderServers(): void
488+
{
489+
// Class has no search indexes, so if the server doesn't support them we assume everything is fine
490+
$collectionName = $this->dm->getClassMetadata(CmsProduct::class)->getCollection();
491+
$collection = $this->documentCollections[$collectionName];
492+
$collection
493+
->expects($this->once())
494+
->method('listSearchIndexes')
495+
->willThrowException($this->createSearchIndexCommandExceptionForOlderServers());
496+
$collection
497+
->expects($this->never())
498+
->method('dropSearchIndex');
499+
$collection
500+
->expects($this->never())
501+
->method('updateSearchIndex');
502+
503+
$this->schemaManager->updateDocumentSearchIndexes(CmsProduct::class);
504+
}
505+
506+
public function testUpdateDocumentSearchIndexesNotSupportedForClassWithSearchIndexes(): void
507+
{
508+
$exception = $this->createSearchIndexCommandException();
509+
510+
// This class has search indexes, so we do expect an exception
511+
$collectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection();
512+
$collection = $this->documentCollections[$collectionName];
513+
$collection
514+
->expects($this->once())
515+
->method('listSearchIndexes')
516+
->willThrowException($exception);
517+
$collection
518+
->expects($this->never())
519+
->method('dropSearchIndex');
520+
$collection
521+
->expects($this->never())
522+
->method('updateSearchIndex');
523+
524+
$this->expectExceptionObject($exception);
525+
$this->schemaManager->updateDocumentSearchIndexes(CmsArticle::class);
526+
}
527+
446528
public function testDeleteDocumentSearchIndexes(): void
447529
{
448530
$collectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection();
@@ -459,6 +541,21 @@ public function testDeleteDocumentSearchIndexes(): void
459541
$this->schemaManager->deleteDocumentSearchIndexes(CmsArticle::class);
460542
}
461543

544+
public function testDeleteDocumentSearchIndexesNotSupported(): void
545+
{
546+
$collectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection();
547+
$collection = $this->documentCollections[$collectionName];
548+
$collection
549+
->expects($this->once())
550+
->method('listSearchIndexes')
551+
->willThrowException($this->createSearchIndexCommandException());
552+
$collection
553+
->expects($this->never())
554+
->method('dropSearchIndex');
555+
556+
$this->schemaManager->deleteDocumentSearchIndexes(CmsArticle::class);
557+
}
558+
462559
public function testUpdateValidators(): void
463560
{
464561
$dbCommands = [];
@@ -1239,4 +1336,14 @@ private function writeOptions(array $expectedWriteOptions): Constraint
12391336
return true;
12401337
});
12411338
}
1339+
1340+
private function createSearchIndexCommandException(): CommandException
1341+
{
1342+
return new CommandException('PlanExecutor error during aggregation :: caused by :: Search index commands are only supported with Atlas.', 115);
1343+
}
1344+
1345+
private function createSearchIndexCommandExceptionForOlderServers(): CommandException
1346+
{
1347+
return new CommandException('Unrecognized pipeline stage name: \'$listSearchIndexes\'', 40234);
1348+
}
12421349
}

0 commit comments

Comments
 (0)
Please sign in to comment.