Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/atlas-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: "Atlas CI"

on:
pull_request:
branches:
- "*.x"
- "feature/*"
push:

jobs:
atlas-local:
runs-on: "${{ matrix.os }}"
name: "PHP/${{ matrix.php-version }} Symfony/${{ matrix.symfony }} Proxy/${{ matrix.proxy }}"
strategy:
fail-fast: false
matrix:
os:
- "ubuntu-latest"
php-version:
- "8.2"
- "8.3"
- "8.4"
symfony:
- "stable"
- "6.4"
proxy:
- "lazy-ghost"
include:
# Test with ProxyManager
- php-version: "8.2"
symfony: "stable"
proxy: "proxy-manager"
os: "ubuntu-latest"

steps:
- name: "Checkout"
uses: "actions/checkout@v5"
with:
fetch-depth: 2

- name: "Create MongoDB Atlas Local"
run: |
docker run --name mongodb -p 27017:27017 --detach mongodb/mongodb-atlas-local:8.2
until docker exec --tty mongodb mongosh --eval "db.runCommand({ ping: 1 })"; do
sleep 1
done
until docker exec --tty mongodb mongosh --eval "db.createCollection('connection_test') && db.getCollection('connection_test').createSearchIndex({mappings:{dynamic: true}})"; do
sleep 1
done
- name: "Show MongoDB server status"
run: |
docker exec --tty mongodb mongosh --eval "db.runCommand({ serverStatus: 1 })"
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-version }}
extensions: "mongodb, bcmath"
key: "extcache-v1"

- name: Cache extensions
uses: actions/cache@v4
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
tools: "pecl"
extensions: "mongodb, bcmath"
coverage: "none"
ini-values: "zend.assertions=1"

- name: "Show driver information"
run: "php --ri mongodb"

# Not used, skip transient dependencies
- name: "Remove phpbench/phpbench"
run: composer remove --no-update --dev phpbench/phpbench

- name: "Configure Symfony ${{ matrix.symfony }}"
if: "${{ matrix.symfony != 'stable' }}"
run: |
composer config minimum-stability dev
# update symfony deps
composer require --no-update symfony/console:^${{ matrix.symfony }}
composer require --no-update symfony/var-dumper:^${{ matrix.symfony }}
composer require --no-update --dev symfony/cache:^${{ matrix.symfony }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"
composer-options: "--prefer-dist"

- name: "Run PHPUnit with Atlas Local"
run: "vendor/bin/phpunit"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted that this will run the entire test suite with Atlas Local. How do you intend to trigger the atlas-ci.yml jobs? Presumably we don't need this entire matrix run on each PR. Would it make more sense to initially restrict test execution to the "atlas" group to compensate for the tests that will be exluded by continuous-integration.yml?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal was to run all the tests with Atlas, but that's not possible due to the lack of support of failPoints. So I simplified the test matrix for Atlas.

env:
DOCTRINE_MONGODB_SERVER: "mongodb://127.0.0.1:27017/?directConnection=true"
USE_LAZY_GHOST_OBJECTS: ${{ matrix.proxy == 'lazy-ghost' && '1' || '0' }}
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ jobs:
topology: ${{ matrix.topology }}

- name: "Run PHPUnit"
run: "vendor/bin/phpunit"
run: "vendor/bin/phpunit --exclude-group=atlas"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted that this will skip the tests with sleep(2).

env:
DOCTRINE_MONGODB_SERVER: ${{ steps.setup-mongodb.outputs.cluster-uri }}
USE_LAZY_GHOST_OBJECTS: ${{ matrix.proxy == 'lazy-ghost' && '1' || '0' }}"
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: '3.8'

services:
mongodb-atlas-local:
image: mongodb/mongodb-atlas-local
ports:
- "27018:27017"
2 changes: 1 addition & 1 deletion lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@

public const VECTOR_SIMILARITY_EUCLIDEAN = 'euclidean';
public const VECTOR_SIMILARITY_COSINE = 'cosine';
public const VECTOR_SIMILARITY_DOT_PRODUCT = 'dot_product';
public const VECTOR_SIMILARITY_DOT_PRODUCT = 'dotProduct';
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant value was changed from 'dot_product' to 'dotProduct'. This could be a breaking change if existing code relies on the snake_case format. Please verify this matches the expected MongoDB Atlas format and ensure backward compatibility is maintained.

Suggested change
public const VECTOR_SIMILARITY_DOT_PRODUCT = 'dotProduct';
public const VECTOR_SIMILARITY_DOT_PRODUCT = 'dotProduct';
public const VECTOR_SIMILARITY_DOT_PRODUCT_SNAKE_CASE = 'dot_product';

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GromNaN was this not actually a BC break because the constant was introduced in 9a0e994. Here, it looks like you're making the value consistent with what you already had in the documentation from the earlier commit.

Feel free to respond and resolve. I just wanted to confirm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is not released yet. This is a bug fix as dotProduct is the value expected by MongoDB: https://www.mongodb.com/docs/php-library/current/indexes/atlas-search-index/#create-a-search-index

public const VECTOR_QUANTIZATION_NONE = 'none';
public const VECTOR_QUANTIZATION_SCALAR = 'scalar';
public const VECTOR_QUANTIZATION_BINARY = 'binary';
Expand Down
42 changes: 39 additions & 3 deletions tests/Doctrine/ODM/MongoDB/Tests/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use MongoDB\Client;
use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\Server;
use MongoDB\Model\DatabaseInfo;
Expand Down Expand Up @@ -45,23 +46,34 @@ abstract class BaseTestCase extends TestCase
protected static bool $allowsTransactions = true;
protected ?DocumentManager $dm;
protected UnitOfWork $uow;
private bool $disableFailPoints = false;

public function setUp(): void
protected function setUp(): void
{
$this->dm = static::createTestDocumentManager();
$this->uow = $this->dm->getUnitOfWork();
}

public function tearDown(): void
protected function tearDown(): void
{
if (! $this->dm) {
return;
}

$client = $this->dm->getClient();

// Remove any fail points that may have been set
if ($this->disableFailPoints) {
$client->getDatabase('admin')->command([
'configureFailPoint' => 'failCommand',
'mode' => 'off',
]);
$this->disableFailPoints = false;
}

// Check if the database exists. Calling listCollections on a non-existing
// database in a sharded setup will cause an invalid command cursor to be
// returned
$client = $this->dm->getClient();
$databases = iterator_to_array($client->listDatabases());
$databaseNames = array_map(static fn (DatabaseInfo $database) => $database->getName(), $databases);
if (! in_array(DOCTRINE_MONGODB_DATABASE, $databaseNames)) {
Expand Down Expand Up @@ -294,4 +306,28 @@ private static function detectTransactionSupport(): bool

return $manager->selectServer()->getType() !== Server::TYPE_STANDALONE;
}

protected function createFailPoint(string $failCommand, bool $transient = false, int $times = 1): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it was adapted from what we have in PHPLIB, although this method is only useful for the failCommand fail point. That assumption also exists in the tearDown() method.

Just wanted to confirm that this is perfectly suitable for ODM's present needs. In the future, you may want to make this more consistent with PHPLIB's implementation, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot about the PHPLIB implementation, this is definitely a simpler approach as we only need to add 1 fail point at a time.

{
try {
$this->dm->getClient()->getManager()->executeCommand(
'admin',
new Command([
'configureFailPoint' => 'failCommand',
'mode' => ['times' => $times],
'data' => [
'errorCode' => 192, // FailPointEnabled
'errorLabels' => $transient ? ['TransientTransactionError'] : [],
'failCommands' => [$failCommand],
],
]),
);
$this->disableFailPoints = true;
} catch (CommandException $exception) {
// no such command: 'configureFailPoint'
if ($exception->getCode() === 59) {
self::markTestSkipped('Test skipped because the server does not support fail points');
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,6 @@ public function setUp(): void
$this->skipTestIfTransactionalFlushDisabled();
}

public function tearDown(): void
{
$this->dm->getClient()->getDatabase('admin')->command([
'configureFailPoint' => 'failCommand',
'mode' => 'off',
]);

parent::tearDown();
}

public function testPersistEvents(): void
{
$root = new RootEventDocument();
Expand All @@ -41,7 +31,7 @@ public function testPersistEvents(): void
$root->embedded = new EmbeddedEventDocument();
$root->embedded->name = 'embedded';

$this->createFailPoint('insert');
$this->createFailPoint('insert', transient: true);

$this->dm->persist($root);
$this->dm->flush();
Expand All @@ -61,7 +51,7 @@ public function testUpdateEvents(): void
$this->dm->persist($root);
$this->dm->flush();

$this->createFailPoint('update');
$this->createFailPoint('update', transient: true);

$root->name = 'updated';
$root->embedded->name = 'updated';
Expand All @@ -85,7 +75,7 @@ public function testUpdateEventsRootOnly(): void
$this->dm->persist($root);
$this->dm->flush();

$this->createFailPoint('update');
$this->createFailPoint('update', transient: true);

$root->name = 'updated';

Expand All @@ -108,7 +98,7 @@ public function testUpdateEventsEmbeddedOnly(): void
$this->dm->persist($root);
$this->dm->flush();

$this->createFailPoint('update');
$this->createFailPoint('update', transient: true);

$root->embedded->name = 'updated';

Expand Down Expand Up @@ -136,7 +126,7 @@ public function testUpdateEventsWithNewEmbeddedDocument(): void
$this->dm->persist($root);
$this->dm->flush();

$this->createFailPoint('update');
$this->createFailPoint('update', transient: true);

$root->name = 'updated';
$root->embedded = $secondEmbedded;
Expand Down Expand Up @@ -168,7 +158,7 @@ public function testRemoveEvents(): void
$this->dm->persist($root);
$this->dm->flush();

$this->createFailPoint('delete');
$this->createFailPoint('delete', transient: true);

$this->dm->remove($root);
$this->dm->flush();
Expand All @@ -185,19 +175,6 @@ protected static function createTestDocumentManager(): DocumentManager

return DocumentManager::create($client, $config);
}

private function createFailPoint(string $failCommand): void
{
$this->dm->getClient()->getDatabase('admin')->command([
'configureFailPoint' => 'failCommand',
'mode' => ['times' => 1],
'data' => [
'errorCode' => 192, // FailPointEnabled
'errorLabels' => ['TransientTransactionError'],
'failCommands' => [$failCommand],
],
]);
}
}

#[ODM\MappedSuperclass]
Expand Down
Loading