From 83ef4321ad62da57ec2194df29f04916b1e21a80 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 3 Mar 2025 17:01:26 -0500 Subject: [PATCH 01/38] BulkWriteCommandBuilder --- src/BulkWriteCommandBuilder.php | 230 ++++++++++++++++++++++++++++++++ src/Collection.php | 10 ++ 2 files changed, 240 insertions(+) create mode 100644 src/BulkWriteCommandBuilder.php diff --git a/src/BulkWriteCommandBuilder.php b/src/BulkWriteCommandBuilder.php new file mode 100644 index 000000000..f6b9fbc2a --- /dev/null +++ b/src/BulkWriteCommandBuilder.php @@ -0,0 +1,230 @@ + true]; + + if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { + throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean'); + } + + if (isset($options['let']) && ! is_document($options['let'])) { + throw InvalidArgumentException::expectedDocumentType('"let" option', $options['let']); + } + + if (! is_bool($options['ordered'])) { + throw InvalidArgumentException::invalidType('"ordered" option', $options['ordered'], 'boolean'); + } + + if (isset($options['verboseResults']) && ! is_bool($options['verboseResults'])) { + throw InvalidArgumentException::invalidType('"verboseResults" option', $options['verboseResults'], 'boolean'); + } + + $this->bulkWriteCommand = new BulkWriteCommand($options); + } + + public static function createWithCollection(Collection $collection, array $options): self + { + return new self( + $collection->getNamespace(), + $collection->getBuilderEncoder(), + $collection->getCodec(), + $options, + ); + } + + public function withCollection(Collection $collection): self + { + $this->namespace = $collection->getNamespace(); + $this->builderEncoder = $collection->getBuilderEncoder(); + $this->codec = $collection->getCodec(); + + return $this; + } + + public function deleteOne(array|object $filter, ?array $options = null): self + { + $filter = $this->builderEncoder->encodeIfSupported($filter); + + if (isset($options['collation']) && ! is_document($options['collation'])) { + throw InvalidArgumentException::expectedDocumentType('"collation" option', $options['collation']); + } + + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); + } + + $this->bulkWriteCommand->deleteOne($this->namespace, $filter, $options); + + return $this; + } + + public function deleteMany(array|object $filter, ?array $options = null): self + { + $filter = $this->builderEncoder->encodeIfSupported($filter); + + if (isset($options['collation']) && ! is_document($options['collation'])) { + throw InvalidArgumentException::expectedDocumentType('"collation" option', $options['collation']); + } + + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); + } + + $this->bulkWriteCommand->deleteMany($this->namespace, $filter, $options); + + return $this; + } + + public function insertOne(array|object $document, mixed &$id = null): self + { + if ($this->codec) { + $document = $this->codec->encode($document); + } + + // Capture the document's _id, which may have been generated, in an optional output variable + $id = $this->bulkWriteCommand->insertOne($this->namespace, $document); + + return $this; + } + + public function replaceOne(array|object $filter, array|object $replacement, ?array $options = null): self + { + $filter = $this->builderEncoder->encodeIfSupported($filter); + + if ($this->codec) { + $replacement = $this->codec->encode($replacement); + } + + // Treat empty arrays as replacement documents for BC + if ($replacement === []) { + $replacement = (object) $replacement; + } + + if (is_first_key_operator($replacement)) { + throw new InvalidArgumentException('First key in $replacement is an update operator'); + } + + if (is_pipeline($replacement, true)) { + throw new InvalidArgumentException('$replacement is an update pipeline'); + } + + if (isset($options['collation']) && ! is_document($options['collation'])) { + throw InvalidArgumentException::expectedDocumentType('"collation" option', $options['collation']); + } + + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); + } + + if (isset($options['sort']) && ! is_document($options['sort'])) { + throw InvalidArgumentException::expectedDocumentType('"sort" option', $options['sort']); + } + + if (isset($options['upsert']) && ! is_bool($options['upsert'])) { + throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean'); + } + + $this->bulkWriteCommand->replaceOne($this->namespace, $filter, $replacement, $options); + + return $this; + } + + public function updateOne(array|object $filter, array|object $update, ?array $options = null): self + { + $filter = $this->builderEncoder->encodeIfSupported($filter); + $update = $this->builderEncoder->encodeIfSupported($update); + + if (! is_first_key_operator($update) && ! is_pipeline($update)) { + throw new InvalidArgumentException('Expected update operator(s) or non-empty pipeline for $update'); + } + + if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) { + throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array'); + } + + if (isset($options['collation']) && ! is_document($options['collation'])) { + throw InvalidArgumentException::expectedDocumentType('"collation" option', $options['collation']); + } + + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); + } + + if (isset($options['sort']) && ! is_document($options['sort'])) { + throw InvalidArgumentException::expectedDocumentType('"sort" option', $options['sort']); + } + + if (isset($options['upsert']) && ! is_bool($options['upsert'])) { + throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean'); + } + + $this->bulkWriteCommand->updateOne($this->namespace, $filter, $update, $options); + + return $this; + } + + public function updateMany(array|object $filter, array|object $update, ?array $options = null): self + { + $filter = $this->builderEncoder->encodeIfSupported($filter); + $update = $this->builderEncoder->encodeIfSupported($update); + + if (! is_first_key_operator($update) && ! is_pipeline($update)) { + throw new InvalidArgumentException('Expected update operator(s) or non-empty pipeline for $update'); + } + + if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) { + throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array'); + } + + if (isset($options['collation']) && ! is_document($options['collation'])) { + throw InvalidArgumentException::expectedDocumentType('"collation" option', $options['collation']); + } + + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_document($options['hint'])) { + throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); + } + + if (isset($options['upsert']) && ! is_bool($options['upsert'])) { + throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean'); + } + + $this->bulkWriteCommand->updateMany($this->namespace, $filter, $update, $options); + + return $this; + } +} diff --git a/src/Collection.php b/src/Collection.php index 34d82544a..54d2a0adc 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -753,6 +753,16 @@ public function findOneAndUpdate(array|object $filter, array|object $update, arr return $operation->execute(select_server_for_write($this->manager, $options)); } + public function getBuilderEncoder(): BuilderEncoder + { + return $this->builderEncoder; + } + + public function getCodec(): ?DocumentCodec + { + return $this->codec; + } + /** * Return the collection name. */ From e9afb05ec67da090c7e0a90c252afdc9f5512358 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 4 Mar 2025 15:31:57 -0500 Subject: [PATCH 02/38] Test against mongodb/mongo-php-driver#1790 --- .../generated/build/build-extension.yml | 20 +++++++++++++++---- .../templates/build/build-extension.yml | 5 ++++- .github/workflows/coding-standards.yml | 6 +++++- .github/workflows/generator.yml | 6 +++++- .github/workflows/static-analysis.yml | 6 +++++- .github/workflows/tests.yml | 6 +++++- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/.evergreen/config/generated/build/build-extension.yml b/.evergreen/config/generated/build/build-extension.yml index 04860628e..db9b19010 100644 --- a/.evergreen/config/generated/build/build-extension.yml +++ b/.evergreen/config/generated/build/build-extension.yml @@ -36,7 +36,10 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged + # EXTENSION_BRANCH: "v2.x" + EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" + EXTENSION_BRANCH: "2.x-bulkwrite" - func: "upload extension" - name: "build-php-8.3" tags: ["build", "php8.3", "stable", "pr", "tag"] @@ -74,7 +77,10 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged + # EXTENSION_BRANCH: "v2.x" + EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" + EXTENSION_BRANCH: "2.x-bulkwrite" - func: "upload extension" - name: "build-php-8.2" tags: ["build", "php8.2", "stable", "pr", "tag"] @@ -112,7 +118,10 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged + # EXTENSION_BRANCH: "v2.x" + EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" + EXTENSION_BRANCH: "2.x-bulkwrite" - func: "upload extension" - name: "build-php-8.1" tags: ["build", "php8.1", "stable", "pr", "tag"] @@ -150,5 +159,8 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged + # EXTENSION_BRANCH: "v2.x" + EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" + EXTENSION_BRANCH: "2.x-bulkwrite" - func: "upload extension" diff --git a/.evergreen/config/templates/build/build-extension.yml b/.evergreen/config/templates/build/build-extension.yml index 08d4ecc2a..4f76c7889 100644 --- a/.evergreen/config/templates/build/build-extension.yml +++ b/.evergreen/config/templates/build/build-extension.yml @@ -34,5 +34,8 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged + # EXTENSION_BRANCH: "v2.x" + EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" + EXTENSION_BRANCH: "2.x-bulkwrite" - func: "upload extension" diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index bb6091805..a64982b99 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -13,7 +13,11 @@ on: env: PHP_VERSION: "8.2" - DRIVER_VERSION: "stable" + # TODO: change to "stable" once 2.0.0 is released + # DRIVER_VERSION: "stable" + # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged + # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" jobs: phpcs: diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index e64e8feeb..a780bce76 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -13,7 +13,11 @@ on: env: PHP_VERSION: "8.2" - DRIVER_VERSION: "stable" + # TODO: change to "stable" once 2.0.0 is released + # DRIVER_VERSION: "stable" + # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged + # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" jobs: diff: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 3447142a3..e145fe802 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -19,7 +19,11 @@ on: env: PHP_VERSION: "8.2" - DRIVER_VERSION: "stable" + # TODO: change to "stable" once 2.0.0 is released + # DRIVER_VERSION: "stable" + # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged + # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" jobs: psalm: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 956e0ec61..e4880354e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,11 @@ on: - "feature/*" env: - DRIVER_VERSION: "stable" + # TODO: change to "stable" once 2.0.0 is released + # DRIVER_VERSION: "stable" + # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged + # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" jobs: phpunit: From 31ca0896b16bfb94941867870bbb9ee74a1bb0a1 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 4 Mar 2025 16:13:28 -0500 Subject: [PATCH 03/38] Psalm stubs for PHPC BulkWriteCommand classes --- psalm.xml.dist | 3 + stubs/Driver/BulkWriteCommand.stub.php | 40 +++++++++++++ .../Driver/BulkWriteCommandException.stub.php | 14 +++++ stubs/Driver/BulkWriteCommandResult.stub.php | 60 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 stubs/Driver/BulkWriteCommand.stub.php create mode 100644 stubs/Driver/BulkWriteCommandException.stub.php create mode 100644 stubs/Driver/BulkWriteCommandResult.stub.php diff --git a/psalm.xml.dist b/psalm.xml.dist index 57b29cd71..28efbec86 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -20,6 +20,9 @@ + + + diff --git a/stubs/Driver/BulkWriteCommand.stub.php b/stubs/Driver/BulkWriteCommand.stub.php new file mode 100644 index 000000000..464b3f1ae --- /dev/null +++ b/stubs/Driver/BulkWriteCommand.stub.php @@ -0,0 +1,40 @@ + Date: Fri, 7 Mar 2025 14:53:27 -0500 Subject: [PATCH 04/38] BulkWriteCommandBuilder::withCollection returns a new instance --- src/BulkWriteCommandBuilder.php | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/BulkWriteCommandBuilder.php b/src/BulkWriteCommandBuilder.php index f6b9fbc2a..8186e0efa 100644 --- a/src/BulkWriteCommandBuilder.php +++ b/src/BulkWriteCommandBuilder.php @@ -26,16 +26,18 @@ use function is_bool; use function is_string; -class BulkWriteCommandBuilder +readonly class BulkWriteCommandBuilder { - private BulkWriteCommand $bulkWriteCommand; - private function __construct( + public BulkWriteCommand $bulkWriteCommand, private string $namespace, private Encoder $builderEncoder, private ?DocumentCodec $codec, - array $options, ) { + } + + public static function createWithCollection(Collection $collection, array $options): self + { $options += ['ordered' => true]; if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { @@ -54,26 +56,22 @@ private function __construct( throw InvalidArgumentException::invalidType('"verboseResults" option', $options['verboseResults'], 'boolean'); } - $this->bulkWriteCommand = new BulkWriteCommand($options); - } - - public static function createWithCollection(Collection $collection, array $options): self - { return new self( + new BulkWriteCommand($options), $collection->getNamespace(), $collection->getBuilderEncoder(), $collection->getCodec(), - $options, ); } public function withCollection(Collection $collection): self { - $this->namespace = $collection->getNamespace(); - $this->builderEncoder = $collection->getBuilderEncoder(); - $this->codec = $collection->getCodec(); - - return $this; + return new self( + $this->bulkWriteCommand, + $collection->getNamespace(), + $collection->getBuilderEncoder(), + $collection->getCodec(), + ); } public function deleteOne(array|object $filter, ?array $options = null): self From a4a504581bd79f8305101f8563a12f4b0dee2b7e Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 7 Mar 2025 14:55:27 -0500 Subject: [PATCH 05/38] Sanity check Manager association in BulkWriteCommandBuilder --- src/BulkWriteCommandBuilder.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/BulkWriteCommandBuilder.php b/src/BulkWriteCommandBuilder.php index 8186e0efa..18ad144a2 100644 --- a/src/BulkWriteCommandBuilder.php +++ b/src/BulkWriteCommandBuilder.php @@ -20,6 +20,7 @@ use MongoDB\Codec\DocumentCodec; use MongoDB\Codec\Encoder; use MongoDB\Driver\BulkWriteCommand; +use MongoDB\Driver\Manager; use MongoDB\Exception\InvalidArgumentException; use function is_array; @@ -30,6 +31,7 @@ { private function __construct( public BulkWriteCommand $bulkWriteCommand, + private Manager $manager, private string $namespace, private Encoder $builderEncoder, private ?DocumentCodec $codec, @@ -58,6 +60,7 @@ public static function createWithCollection(Collection $collection, array $optio return new self( new BulkWriteCommand($options), + $collection->getManager(), $collection->getNamespace(), $collection->getBuilderEncoder(), $collection->getCodec(), @@ -66,8 +69,18 @@ public static function createWithCollection(Collection $collection, array $optio public function withCollection(Collection $collection): self { + /* Prohibit mixing Collections associated with different Manager + * objects. This is not technically necessary, since the Collection is + * only used to derive a namespace and encoding options; however, it + * may prevent a user from inadvertently mixing writes destined for + * different deployments. */ + if ($this->manager !== $collection->getManager()) { + throw new InvalidArgumentException('$collection is associated with a different MongoDB\Driver\Manager'); + } + return new self( $this->bulkWriteCommand, + $this->manager, $collection->getNamespace(), $collection->getBuilderEncoder(), $collection->getCodec(), From 56d76c712034b0577304d722e2467a12bdd88e35 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 7 Mar 2025 14:56:37 -0500 Subject: [PATCH 06/38] Client::bulkWrite() and ClientBulkWrite operation --- src/Client.php | 29 ++++++++++ src/Operation/ClientBulkWrite.php | 92 +++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/Operation/ClientBulkWrite.php diff --git a/src/Client.php b/src/Client.php index a6b96462b..af7cbe53a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -24,6 +24,8 @@ use MongoDB\Builder\BuilderEncoder; use MongoDB\Builder\Pipeline; use MongoDB\Codec\Encoder; +use MongoDB\Driver\BulkWriteCommand; +use MongoDB\Driver\BulkWriteCommandResult; use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; @@ -39,6 +41,7 @@ use MongoDB\Model\BSONArray; use MongoDB\Model\BSONDocument; use MongoDB\Model\DatabaseInfo; +use MongoDB\Operation\ClientBulkWrite; use MongoDB\Operation\DropDatabase; use MongoDB\Operation\ListDatabaseNames; use MongoDB\Operation\ListDatabases; @@ -189,6 +192,32 @@ final public function addSubscriber(Subscriber $subscriber): void $this->manager->addSubscriber($subscriber); } + /** + * Executes multiple write operations. + * + * @see ClientBulkWrite::__construct() for supported options + * @param string $databaseName Database name + * @param array $options Additional options + * @throws UnsupportedException if options are unsupported on the selected server + * @throws InvalidArgumentException for parameter/option parsing errors + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function bulkWrite(BulkWriteCommand|BulkWriteCommandBuilder $bulk, array $options = []): ?BulkWriteCommandResult + { + if (! isset($options['writeConcern']) && ! is_in_transaction($options)) { + $options['writeConcern'] = $this->writeConcern; + } + + if ($bulk instanceof BulkWriteCommandBuilder) { + $bulk = $bulk->bulkWriteCommand; + } + + $operation = new ClientBulkWrite($bulk, $options); + $server = select_server_for_write($this->manager, $options); + + return $operation->execute($server); + } + /** * Returns a ClientEncryption instance for explicit encryption and decryption * diff --git a/src/Operation/ClientBulkWrite.php b/src/Operation/ClientBulkWrite.php new file mode 100644 index 000000000..635805930 --- /dev/null +++ b/src/Operation/ClientBulkWrite.php @@ -0,0 +1,92 @@ +isDefault()) { + unset($options['writeConcern']); + } + } + + /** + * Execute the operation. + * + * @throws UnsupportedException if write concern is used and unsupported + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function execute(Server $server): ?BulkWriteCommandResult + { + $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); + if ($inTransaction && isset($this->options['writeConcern'])) { + throw UnsupportedException::writeConcernNotSupportedInTransaction(); + } + + $options = array_filter($this->options, fn ($value) => isset($value)); + + return $server->executeBulkWriteCommand($this->bulkWriteCommand, $options); + } +} From 3e970824d9044a22574fc4dc6745dd54e214b8c1 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 14 Mar 2025 15:53:09 -0400 Subject: [PATCH 07/38] Spec tests for Client::bulkWrite() --- tests/UnifiedSpecTests/Constraint/Matches.php | 11 +- tests/UnifiedSpecTests/ExpectedError.php | 113 ++++++++++++++++-- tests/UnifiedSpecTests/ExpectedResult.php | 32 ++++- tests/UnifiedSpecTests/Operation.php | 95 ++++++++++++++- tests/UnifiedSpecTests/UnifiedTestRunner.php | 7 +- tests/UnifiedSpecTests/Util.php | 1 + 6 files changed, 240 insertions(+), 19 deletions(-) diff --git a/tests/UnifiedSpecTests/Constraint/Matches.php b/tests/UnifiedSpecTests/Constraint/Matches.php index 9a527dcc7..a6bda1420 100644 --- a/tests/UnifiedSpecTests/Constraint/Matches.php +++ b/tests/UnifiedSpecTests/Constraint/Matches.php @@ -5,6 +5,7 @@ use LogicException; use MongoDB\BSON\Document; use MongoDB\BSON\Int64; +use MongoDB\BSON\PackedArray; use MongoDB\BSON\Serializable; use MongoDB\BSON\Type; use MongoDB\Model\BSONArray; @@ -457,8 +458,14 @@ private static function prepare(mixed $bson): mixed return self::prepare($bson->bsonSerialize()); } - /* Serializable has already been handled, so any remaining instances of - * Type will not serialize as BSON arrays or objects */ + // Recurse on the PHP representation of Document and PackedArray types + if ($bson instanceof Document || $bson instanceof PackedArray) { + return self::prepare($bson->toPHP()); + } + + /* Serializable, Document, and PackedArray have already been handled. + * Any remaining Type instances will not serialize as BSON arrays or + * objects. */ if ($bson instanceof Type) { return $bson; } diff --git a/tests/UnifiedSpecTests/ExpectedError.php b/tests/UnifiedSpecTests/ExpectedError.php index c7ace6d7f..092701479 100644 --- a/tests/UnifiedSpecTests/ExpectedError.php +++ b/tests/UnifiedSpecTests/ExpectedError.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\UnifiedSpecTests; +use MongoDB\Driver\Exception\BulkWriteCommandException; use MongoDB\Driver\Exception\BulkWriteException; use MongoDB\Driver\Exception\CommandException; use MongoDB\Driver\Exception\ExecutionTimeoutException; @@ -12,6 +13,7 @@ use stdClass; use Throwable; +use function count; use function PHPUnit\Framework\assertArrayHasKey; use function PHPUnit\Framework\assertContainsOnly; use function PHPUnit\Framework\assertCount; @@ -56,7 +58,7 @@ final class ExpectedError private ?string $codeName = null; - private ?Matches $matchesResultDocument = null; + private ?Matches $matchesErrorResponse = null; private array $includedLabels = []; @@ -64,6 +66,10 @@ final class ExpectedError private ?ExpectedResult $expectedResult = null; + private ?array $writeErrors = null; + + private ?array $writeConcernErrors = null; + public function __construct(?stdClass $o, EntityMap $entityMap) { if ($o === null) { @@ -102,7 +108,7 @@ public function __construct(?stdClass $o, EntityMap $entityMap) if (isset($o->errorResponse)) { assertIsObject($o->errorResponse); - $this->matchesResultDocument = new Matches($o->errorResponse, $entityMap); + $this->matchesErrorResponse = new Matches($o->errorResponse, $entityMap); } if (isset($o->errorLabelsContain)) { @@ -120,6 +126,24 @@ public function __construct(?stdClass $o, EntityMap $entityMap) if (property_exists($o, 'expectResult')) { $this->expectedResult = new ExpectedResult($o, $entityMap); } + + if (isset($o->writeErrors)) { + assertIsObject($o->writeErrors); + assertContainsOnly('object', (array) $o->writeErrors); + + foreach ($o->writeErrors as $i => $writeError) { + $this->writeErrors[$i] = new Matches($writeError, $entityMap); + } + } + + if (isset($o->writeConcernErrors)) { + assertIsArray($o->writeConcernErrors); + assertContainsOnly('object', $o->writeConcernErrors); + + foreach ($o->writeConcernErrors as $i => $writeConcernError) { + $this->writeConcernErrors[$i] = new Matches($writeConcernError, $entityMap); + } + } } /** @@ -159,15 +183,21 @@ public function assert(?Throwable $e = null): void $this->assertCodeName($e); } - if (isset($this->matchesResultDocument)) { - assertThat($e, logicalOr(isInstanceOf(CommandException::class), isInstanceOf(BulkWriteException::class))); + if (isset($this->matchesErrorResponse)) { + assertThat($e, logicalOr( + isInstanceOf(CommandException::class), + isInstanceOf(BulkWriteException::class), + isInstanceOf(BulkWriteCommandException::class), + )); if ($e instanceof CommandException) { - assertThat($e->getResultDocument(), $this->matchesResultDocument, 'CommandException result document matches'); + assertThat($e->getResultDocument(), $this->matchesErrorResponse, 'CommandException result document matches expected errorResponse'); + } elseif ($e instanceof BulkWriteCommandException) { + assertThat($e->getErrorReply(), $this->matchesErrorResponse, 'BulkWriteCommandException error reply matches expected errorResponse'); } elseif ($e instanceof BulkWriteException) { $writeErrors = $e->getWriteResult()->getErrorReplies(); assertCount(1, $writeErrors); - assertThat($writeErrors[0], $this->matchesResultDocument, 'BulkWriteException result document matches'); + assertThat($writeErrors[0], $this->matchesErrorResponse, 'BulkWriteException first error reply matches expected errorResponse'); } } @@ -184,16 +214,34 @@ public function assert(?Throwable $e = null): void } if (isset($this->expectedResult)) { - assertInstanceOf(BulkWriteException::class, $e); - $this->expectedResult->assert($e->getWriteResult()); + assertThat($e, logicalOr( + isInstanceOf(BulkWriteException::class), + isInstanceOf(BulkWriteCommandException::class), + )); + + if ($e instanceof BulkWriteCommandException) { + $this->expectedResult->assert($e->getPartialResult()); + } elseif ($e instanceof BulkWriteException) { + $this->expectedResult->assert($e->getWriteResult()); + } + } + + if (isset($this->writeErrors)) { + assertInstanceOf(BulkWriteCommandException::class, $e); + $this->assertWriteErrors($e->getWriteErrors()); + } + + if (isset($this->writeConcernErrors)) { + assertInstanceOf(BulkWriteCommandException::class, $e); + $this->assertWriteConcernErrors($e->getWriteConcernErrors()); } } private function assertIsClientError(Throwable $e): void { - /* Note: BulkWriteException may proxy a previous exception. Unwrap it - * to check the original error. */ - if ($e instanceof BulkWriteException && $e->getPrevious() !== null) { + /* Note: BulkWriteException and BulkWriteCommandException may proxy a + * previous exception. Unwrap it to check the original error. */ + if (($e instanceof BulkWriteException || $e instanceof BulkWriteCommandException) && $e->getPrevious() !== null) { $e = $e->getPrevious(); } @@ -230,4 +278,47 @@ private function assertCodeName(ServerException $e): void assertObjectHasProperty('codeName', $result); assertSame($this->codeName, $result->codeName); } + + private function assertWriteErrors(array $writeErrors): void + { + assertCount(count($this->writeErrors), $writeErrors); + + foreach ($this->writeErrors as $i => $matchesWriteError) { + assertArrayHasKey($i, $writeErrors); + $writeError = $writeErrors[$i]; + + // Not required by the spec test, but asserts PHPC correctness + assertSame((int) $i, $writeError->getIndex()); + + /* Convert the WriteError into a document for matching. These + * field names are derived from the CRUD spec. */ + $writeErrorDocument = [ + 'code' => $writeError->getCode(), + 'message' => $writeError->getMessage(), + 'details' => $writeError->getInfo(), + ]; + + assertThat($writeErrorDocument, $matchesWriteError); + } + } + + private function assertWriteConcernErrors(array $writeConcernErrors): void + { + assertCount(count($this->writeConcernErrors), $writeConcernErrors); + + foreach ($this->writeConcernErrors as $i => $matchesWriteConcernError) { + assertArrayHasKey($i, $writeConcernErrors); + $writeConcernError = $writeConcernErrors[$i]; + + /* Convert the WriteConcernError into a document for matching. + * These field names are derived from the CRUD spec. */ + $writeConcernErrorDocument = [ + 'code' => $writeConcernError->getCode(), + 'message' => $writeConcernError->getMessage(), + 'details' => $writeConcernError->getInfo(), + ]; + + assertThat($writeConcernErrorDocument, $matchesWriteConcernError); + } + } } diff --git a/tests/UnifiedSpecTests/ExpectedResult.php b/tests/UnifiedSpecTests/ExpectedResult.php index 5edc6e3ce..52a7127f2 100644 --- a/tests/UnifiedSpecTests/ExpectedResult.php +++ b/tests/UnifiedSpecTests/ExpectedResult.php @@ -4,6 +4,7 @@ use MongoDB\BulkWriteResult; use MongoDB\DeleteResult; +use MongoDB\Driver\BulkWriteCommandResult; use MongoDB\Driver\WriteResult; use MongoDB\InsertManyResult; use MongoDB\InsertOneResult; @@ -11,6 +12,7 @@ use MongoDB\UpdateResult; use stdClass; +use function array_filter; use function is_object; use function PHPUnit\Framework\assertThat; use function property_exists; @@ -57,6 +59,10 @@ private static function prepare($value) return $value; } + if ($value instanceof BulkWriteCommandResult) { + return self::prepareBulkWriteCommandResult($value); + } + if ( $value instanceof BulkWriteResult || $value instanceof WriteResult || @@ -71,7 +77,31 @@ private static function prepare($value) return $value; } - private static function prepareWriteResult($value) + private static function prepareBulkWriteCommandResult(BulkWriteCommandResult $result): array + { + $retval = [ + 'deletedCount' => $result->getDeletedCount(), + 'insertedCount' => $result->getInsertedCount(), + 'matchedCount' => $result->getMatchedCount(), + 'modifiedCount' => $result->getModifiedCount(), + 'upsertedCount' => $result->getUpsertedCount(), + ]; + + /* Tests use $$unsetOrMatches to expect either no key or an empty + * document when verboseResults=false, so filter out null values. */ + $retval += array_filter( + [ + 'deleteResults' => $result->getDeleteResults()?->toPHP(), + 'insertResults' => $result->getInsertResults()?->toPHP(), + 'updateResults' => $result->getUpdateResults()?->toPHP(), + ], + fn ($value) => $value !== null, + ); + + return $retval; + } + + private static function prepareWriteResult($value): array { $result = ['acknowledged' => $value->isAcknowledged()]; diff --git a/tests/UnifiedSpecTests/Operation.php b/tests/UnifiedSpecTests/Operation.php index 96e1703ce..d83b71fae 100644 --- a/tests/UnifiedSpecTests/Operation.php +++ b/tests/UnifiedSpecTests/Operation.php @@ -7,6 +7,7 @@ use MongoDB\Client; use MongoDB\Collection; use MongoDB\Database; +use MongoDB\Driver\BulkWriteCommand; use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\Cursor; use MongoDB\Driver\Server; @@ -87,10 +88,7 @@ final class Operation 'assertNumberConnectionsCheckedOut' => 'PHP does not implement CMAP', 'createEntities' => 'createEntities is not implemented (PHPC-1760)', ], - Client::class => [ - 'clientBulkWrite' => 'clientBulkWrite is not implemented (PHPLIB-847)', - 'listDatabaseObjects' => 'listDatabaseObjects is not implemented', - ], + Client::class => ['listDatabaseObjects' => 'listDatabaseObjects is not implemented'], Cursor::class => ['iterateOnce' => 'iterateOnce is not implemented (PHPC-1760)'], Database::class => [ 'createCommandCursor' => 'commandCursor API is not yet implemented (PHPLIB-1077)', @@ -257,6 +255,18 @@ private function executeForClient(Client $client) Util::assertArgumentsBySchema(Client::class, $this->name, $args); switch ($this->name) { + case 'clientBulkWrite': + assertArrayHasKey('models', $args); + assertIsArray($args['models']); + + // Options for BulkWriteCommand and Server::executeBulkWriteCommand() will be mixed + $options = array_diff_key($args, ['models' => 1]); + + return $client->bulkWrite( + self::prepareBulkWriteCommand($args['models'], $options), + $options, + ); + case 'createChangeStream': assertArrayHasKey('pipeline', $args); assertIsArray($args['pipeline']); @@ -1001,6 +1011,82 @@ private function skipIfOperationIsNotSupported(string $executingObjectName): voi Assert::markTestSkipped($skipReason); } + private static function prepareBulkWriteCommand(array $models, array $options): BulkWriteCommand + { + $bulk = new BulkWriteCommand($options); + + foreach ($models as $model) { + $model = (array) $model; + assertCount(1, $model); + + $type = key($model); + $args = current($model); + assertIsObject($args); + $args = (array) $args; + + assertArrayHasKey('namespace', $args); + assertIsString($args['namespace']); + + switch ($type) { + case 'deleteMany': + case 'deleteOne': + assertArrayHasKey('filter', $args); + assertInstanceOf(stdClass::class, $args['filter']); + + $bulk->{$type}( + $args['namespace'], + $args['filter'], + array_diff_key($args, ['namespace' => 1, 'filter' => 1]), + ); + break; + + case 'insertOne': + assertArrayHasKey('document', $args); + assertInstanceOf(stdClass::class, $args['document']); + + $bulk->insertOne( + $args['namespace'], + $args['document'], + ); + break; + + case 'replaceOne': + assertArrayHasKey('filter', $args); + assertArrayHasKey('replacement', $args); + assertInstanceOf(stdClass::class, $args['filter']); + assertInstanceOf(stdClass::class, $args['replacement']); + + $bulk->replaceOne( + $args['namespace'], + $args['filter'], + $args['replacement'], + array_diff_key($args, ['namespace' => 1, 'filter' => 1, 'replacement' => 1]), + ); + break; + + case 'updateMany': + case 'updateOne': + assertArrayHasKey('filter', $args); + assertArrayHasKey('update', $args); + assertInstanceOf(stdClass::class, $args['filter']); + assertThat($args['update'], logicalOr(new IsType('array'), new IsType('object'))); + + $bulk->{$type}( + $args['namespace'], + $args['filter'], + $args['update'], + array_diff_key($args, ['namespace' => 1, 'filter' => 1, 'update' => 1]), + ); + break; + + default: + Assert::fail('Unsupported bulk write model: ' . $type); + } + } + + return $bulk; + } + private static function prepareBulkWriteRequest(stdClass $request): array { $request = (array) $request; @@ -1026,6 +1112,7 @@ private static function prepareBulkWriteRequest(stdClass $request): array case 'insertOne': assertArrayHasKey('document', $args); + assertInstanceOf(stdClass::class, $args['document']); return ['insertOne' => [$args['document']]]; diff --git a/tests/UnifiedSpecTests/UnifiedTestRunner.php b/tests/UnifiedSpecTests/UnifiedTestRunner.php index 78e5772a5..6b700f49e 100644 --- a/tests/UnifiedSpecTests/UnifiedTestRunner.php +++ b/tests/UnifiedSpecTests/UnifiedTestRunner.php @@ -63,8 +63,13 @@ final class UnifiedTestRunner * - 1.11: Not implemented, but CMAP is not applicable * - 1.13: Only $$matchAsDocument and $$matchAsRoot is implemented * - 1.14: Not implemented + * - 1.16: Not implemented + * - 1.17: Not implemented + * - 1.18: Not implemented + * - 1.19: Not implemented + * - 1.20: Not implemented */ - public const MAX_SCHEMA_VERSION = '1.15'; + public const MAX_SCHEMA_VERSION = '1.21'; private Client $internalClient; diff --git a/tests/UnifiedSpecTests/Util.php b/tests/UnifiedSpecTests/Util.php index 12cca41df..e134cb109 100644 --- a/tests/UnifiedSpecTests/Util.php +++ b/tests/UnifiedSpecTests/Util.php @@ -58,6 +58,7 @@ final class Util 'loop' => ['operations', 'storeErrorsAsEntity', 'storeFailuresAsEntity', 'storeSuccessesAsEntity', 'storeIterationsAsEntity'], ], Client::class => [ + 'clientBulkWrite' => ['models', 'bypassDocumentValidation', 'comment', 'let', 'ordered', 'session', 'verboseResults', 'writeConcern'], 'createChangeStream' => ['pipeline', 'session', 'fullDocument', 'resumeAfter', 'startAfter', 'startAtOperationTime', 'batchSize', 'collation', 'maxAwaitTimeMS', 'showExpandedEvents'], 'listDatabaseNames' => ['authorizedDatabases', 'filter', 'maxTimeMS', 'session'], 'listDatabases' => ['authorizedDatabases', 'filter', 'maxTimeMS', 'session'], From 59417c2e973bf020e29adb0dd66306f3f948fd89 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 14 Mar 2025 16:04:01 -0400 Subject: [PATCH 08/38] Revise error messages for readConcern and writeConcern in transactions The transaction spec requires certain language, and this is now expected in spec test for clientBulkWrite. --- src/Exception/UnsupportedException.php | 4 ++-- tests/Collection/CollectionFunctionalTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Exception/UnsupportedException.php b/src/Exception/UnsupportedException.php index 1a5273715..a2204b886 100644 --- a/src/Exception/UnsupportedException.php +++ b/src/Exception/UnsupportedException.php @@ -47,7 +47,7 @@ public static function hintNotSupported(): self */ public static function readConcernNotSupportedInTransaction(): self { - return new self('The "readConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.'); + return new self('Cannot set read concern after starting a transaction. Instead, specify the "readConcern" option when starting the transaction.'); } /** @@ -57,6 +57,6 @@ public static function readConcernNotSupportedInTransaction(): self */ public static function writeConcernNotSupportedInTransaction(): self { - return new self('The "writeConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.'); + return new self('Cannot set write concern after starting a transaction. Instead, specify the "writeConcern" option when starting the transaction.'); } } diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index 581041888..90614f9a8 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -718,7 +718,7 @@ public function testMethodInTransactionWithWriteConcernOption($method): void $session->startTransaction(); $this->expectException(UnsupportedException::class); - $this->expectExceptionMessage('"writeConcern" option cannot be specified within a transaction'); + $this->expectExceptionMessage('Cannot set write concern after starting a transaction'); try { call_user_func($method, $this->collection, $session, ['writeConcern' => new WriteConcern(1)]); @@ -738,7 +738,7 @@ public function testMethodInTransactionWithReadConcernOption($method): void $session->startTransaction(); $this->expectException(UnsupportedException::class); - $this->expectExceptionMessage('"readConcern" option cannot be specified within a transaction'); + $this->expectExceptionMessage('Cannot set read concern after starting a transaction'); try { call_user_func($method, $this->collection, $session, ['readConcern' => new ReadConcern(ReadConcern::LOCAL)]); From 8aa313e1c0ff4a31b2fd2c835223eca9d957eef8 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 14 Mar 2025 16:13:40 -0400 Subject: [PATCH 09/38] BulkWriteCommandBuilder is final --- src/BulkWriteCommandBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BulkWriteCommandBuilder.php b/src/BulkWriteCommandBuilder.php index 18ad144a2..e6e6ee31d 100644 --- a/src/BulkWriteCommandBuilder.php +++ b/src/BulkWriteCommandBuilder.php @@ -27,7 +27,7 @@ use function is_bool; use function is_string; -readonly class BulkWriteCommandBuilder +final readonly class BulkWriteCommandBuilder { private function __construct( public BulkWriteCommand $bulkWriteCommand, From 3123d59ad59634ce64c470ccfb5aabe9730b6d67 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 19 Mar 2025 13:21:28 -0400 Subject: [PATCH 10/38] Re-order BulkWriteCommandBuilder methods to satisfy PedantryTest --- src/BulkWriteCommandBuilder.php | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/BulkWriteCommandBuilder.php b/src/BulkWriteCommandBuilder.php index e6e6ee31d..791176d5c 100644 --- a/src/BulkWriteCommandBuilder.php +++ b/src/BulkWriteCommandBuilder.php @@ -67,27 +67,7 @@ public static function createWithCollection(Collection $collection, array $optio ); } - public function withCollection(Collection $collection): self - { - /* Prohibit mixing Collections associated with different Manager - * objects. This is not technically necessary, since the Collection is - * only used to derive a namespace and encoding options; however, it - * may prevent a user from inadvertently mixing writes destined for - * different deployments. */ - if ($this->manager !== $collection->getManager()) { - throw new InvalidArgumentException('$collection is associated with a different MongoDB\Driver\Manager'); - } - - return new self( - $this->bulkWriteCommand, - $this->manager, - $collection->getNamespace(), - $collection->getBuilderEncoder(), - $collection->getCodec(), - ); - } - - public function deleteOne(array|object $filter, ?array $options = null): self + public function deleteMany(array|object $filter, ?array $options = null): self { $filter = $this->builderEncoder->encodeIfSupported($filter); @@ -99,12 +79,12 @@ public function deleteOne(array|object $filter, ?array $options = null): self throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); } - $this->bulkWriteCommand->deleteOne($this->namespace, $filter, $options); + $this->bulkWriteCommand->deleteMany($this->namespace, $filter, $options); return $this; } - public function deleteMany(array|object $filter, ?array $options = null): self + public function deleteOne(array|object $filter, ?array $options = null): self { $filter = $this->builderEncoder->encodeIfSupported($filter); @@ -116,7 +96,7 @@ public function deleteMany(array|object $filter, ?array $options = null): self throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); } - $this->bulkWriteCommand->deleteMany($this->namespace, $filter, $options); + $this->bulkWriteCommand->deleteOne($this->namespace, $filter, $options); return $this; } @@ -175,7 +155,7 @@ public function replaceOne(array|object $filter, array|object $replacement, ?arr return $this; } - public function updateOne(array|object $filter, array|object $update, ?array $options = null): self + public function updateMany(array|object $filter, array|object $update, ?array $options = null): self { $filter = $this->builderEncoder->encodeIfSupported($filter); $update = $this->builderEncoder->encodeIfSupported($update); @@ -196,20 +176,16 @@ public function updateOne(array|object $filter, array|object $update, ?array $op throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); } - if (isset($options['sort']) && ! is_document($options['sort'])) { - throw InvalidArgumentException::expectedDocumentType('"sort" option', $options['sort']); - } - if (isset($options['upsert']) && ! is_bool($options['upsert'])) { throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean'); } - $this->bulkWriteCommand->updateOne($this->namespace, $filter, $update, $options); + $this->bulkWriteCommand->updateMany($this->namespace, $filter, $update, $options); return $this; } - public function updateMany(array|object $filter, array|object $update, ?array $options = null): self + public function updateOne(array|object $filter, array|object $update, ?array $options = null): self { $filter = $this->builderEncoder->encodeIfSupported($filter); $update = $this->builderEncoder->encodeIfSupported($update); @@ -230,12 +206,36 @@ public function updateMany(array|object $filter, array|object $update, ?array $o throw InvalidArgumentException::expectedDocumentOrStringType('"hint" option', $options['hint']); } + if (isset($options['sort']) && ! is_document($options['sort'])) { + throw InvalidArgumentException::expectedDocumentType('"sort" option', $options['sort']); + } + if (isset($options['upsert']) && ! is_bool($options['upsert'])) { throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean'); } - $this->bulkWriteCommand->updateMany($this->namespace, $filter, $update, $options); + $this->bulkWriteCommand->updateOne($this->namespace, $filter, $update, $options); return $this; } + + public function withCollection(Collection $collection): self + { + /* Prohibit mixing Collections associated with different Manager + * objects. This is not technically necessary, since the Collection is + * only used to derive a namespace and encoding options; however, it + * may prevent a user from inadvertently mixing writes destined for + * different deployments. */ + if ($this->manager !== $collection->getManager()) { + throw new InvalidArgumentException('$collection is associated with a different MongoDB\Driver\Manager'); + } + + return new self( + $this->bulkWriteCommand, + $this->manager, + $collection->getNamespace(), + $collection->getBuilderEncoder(), + $collection->getCodec(), + ); + } } From cf40774bcf90d29cb95168f89192cd54073fea32 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 19 Mar 2025 15:04:49 -0400 Subject: [PATCH 11/38] Ignore order of non-public constructors in PedantryTest --- tests/PedantryTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/PedantryTest.php b/tests/PedantryTest.php index 3d89f259a..4a5cd7096 100644 --- a/tests/PedantryTest.php +++ b/tests/PedantryTest.php @@ -12,6 +12,7 @@ use function array_filter; use function array_map; +use function array_values; use function in_array; use function realpath; use function str_contains; @@ -39,11 +40,12 @@ public function testMethodsAreOrderedAlphabeticallyByVisibility($className): voi $class = new ReflectionClass($className); $methods = $class->getMethods(); - $methods = array_filter( + $methods = array_values(array_filter( $methods, fn (ReflectionMethod $method) => $method->getDeclaringClass() == $class // Exclude inherited methods - && $method->getFileName() === $class->getFileName(), // Exclude methods inherited from traits - ); + && $method->getFileName() === $class->getFileName() // Exclude methods inherited from traits + && ! ($method->isConstructor() && ! $method->isPublic()), // Exclude non-public constructors + )); $getSortValue = function (ReflectionMethod $method) { $prefix = $method->isPrivate() ? '2' : ($method->isProtected() ? '1' : '0'); From 54da642c20d1a952aebce6262e0e6a2fac206ee6 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 19 Mar 2025 16:12:18 -0400 Subject: [PATCH 12/38] Fix preparation of unacknowledged BulkWriteCommandResults --- tests/UnifiedSpecTests/ExpectedResult.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/UnifiedSpecTests/ExpectedResult.php b/tests/UnifiedSpecTests/ExpectedResult.php index 52a7127f2..29871c289 100644 --- a/tests/UnifiedSpecTests/ExpectedResult.php +++ b/tests/UnifiedSpecTests/ExpectedResult.php @@ -79,6 +79,12 @@ private static function prepare($value) private static function prepareBulkWriteCommandResult(BulkWriteCommandResult $result): array { + $retval = ['acknowledged' => $result->isAcknowledged()]; + + if (! $retval['acknowledged']) { + return $retval; + } + $retval = [ 'deletedCount' => $result->getDeletedCount(), 'insertedCount' => $result->getInsertedCount(), From 41ee8b2a83bddac305196d6b31da7e449a9562ba Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 19 Mar 2025 16:17:21 -0400 Subject: [PATCH 13/38] Skip CSFLE namedKMS tests that require schema 1.18 --- tests/UnifiedSpecTests/UnifiedSpecTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/UnifiedSpecTests/UnifiedSpecTest.php b/tests/UnifiedSpecTests/UnifiedSpecTest.php index 3552699e9..d057c5420 100644 --- a/tests/UnifiedSpecTests/UnifiedSpecTest.php +++ b/tests/UnifiedSpecTests/UnifiedSpecTest.php @@ -29,6 +29,8 @@ class UnifiedSpecTest extends FunctionalTestCase * @var array */ private static array $incompleteTestGroups = [ + // Spec tests for named KMS providers depends on unimplemented functionality from UTF schema 1.18 + 'client-side-encryption/namedKMS' => 'UTF schema 1.18 is not supported (PHPLIB-1328)', // Many load balancer tests use CMAP events and/or assertNumberConnectionsCheckedOut 'load-balancers/cursors are correctly pinned to connections for load-balanced clusters' => 'PHPC does not implement CMAP', 'load-balancers/transactions are correctly pinned to connections for load-balanced clusters' => 'PHPC does not implement CMAP', From b1b6368d480696eeeec7cbb7545b5fbc8909ea96 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Thu, 20 Mar 2025 11:32:53 -0400 Subject: [PATCH 14/38] Test Collection::getBuilderEncoder() and getCodec() Also fixes the return type for getBuilderEncoder() --- src/Collection.php | 2 +- tests/Collection/CollectionFunctionalTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Collection.php b/src/Collection.php index 54d2a0adc..52936d8eb 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -753,7 +753,7 @@ public function findOneAndUpdate(array|object $filter, array|object $update, arr return $operation->execute(select_server_for_write($this->manager, $options)); } - public function getBuilderEncoder(): BuilderEncoder + public function getBuilderEncoder(): Encoder { return $this->builderEncoder; } diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index 90614f9a8..7172e915c 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -77,6 +77,22 @@ public static function provideInvalidConstructorOptions(): array ]); } + public function testGetBuilderEncoder(): void + { + $collectionOptions = ['builderEncoder' => $this->createMock(Encoder::class)]; + $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName(), $collectionOptions); + + $this->assertSame($collectionOptions['builderEncoder'], $collection->getBuilderEncoder()); + } + + public function testGetCodec(): void + { + $collectionOptions = ['codec' => $this->createMock(DocumentCodec::class)]; + $collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName(), $collectionOptions); + + $this->assertSame($collectionOptions['codec'], $collection->getCodec()); + } + public function testGetManager(): void { $this->assertSame($this->manager, $this->collection->getManager()); From b86eceb30e3512804dc8e9a20004d4148fa373e0 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 21 Mar 2025 20:42:29 -0400 Subject: [PATCH 15/38] Default BulkWriteCommandBuilder options to empty arrays This is actually required for the union assignment in createWithCollection(). The nullable arrays were copied from the extension, but are inconsistent with other PHPLIB APIs. --- src/BulkWriteCommandBuilder.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BulkWriteCommandBuilder.php b/src/BulkWriteCommandBuilder.php index 791176d5c..22d3bf42f 100644 --- a/src/BulkWriteCommandBuilder.php +++ b/src/BulkWriteCommandBuilder.php @@ -38,7 +38,7 @@ private function __construct( ) { } - public static function createWithCollection(Collection $collection, array $options): self + public static function createWithCollection(Collection $collection, array $options = []): self { $options += ['ordered' => true]; @@ -67,7 +67,7 @@ public static function createWithCollection(Collection $collection, array $optio ); } - public function deleteMany(array|object $filter, ?array $options = null): self + public function deleteMany(array|object $filter, array $options = []): self { $filter = $this->builderEncoder->encodeIfSupported($filter); @@ -84,7 +84,7 @@ public function deleteMany(array|object $filter, ?array $options = null): self return $this; } - public function deleteOne(array|object $filter, ?array $options = null): self + public function deleteOne(array|object $filter, array $options = []): self { $filter = $this->builderEncoder->encodeIfSupported($filter); @@ -113,7 +113,7 @@ public function insertOne(array|object $document, mixed &$id = null): self return $this; } - public function replaceOne(array|object $filter, array|object $replacement, ?array $options = null): self + public function replaceOne(array|object $filter, array|object $replacement, array $options = []): self { $filter = $this->builderEncoder->encodeIfSupported($filter); @@ -155,7 +155,7 @@ public function replaceOne(array|object $filter, array|object $replacement, ?arr return $this; } - public function updateMany(array|object $filter, array|object $update, ?array $options = null): self + public function updateMany(array|object $filter, array|object $update, array $options = []): self { $filter = $this->builderEncoder->encodeIfSupported($filter); $update = $this->builderEncoder->encodeIfSupported($update); @@ -185,7 +185,7 @@ public function updateMany(array|object $filter, array|object $update, ?array $o return $this; } - public function updateOne(array|object $filter, array|object $update, ?array $options = null): self + public function updateOne(array|object $filter, array|object $update, array $options = []): self { $filter = $this->builderEncoder->encodeIfSupported($filter); $update = $this->builderEncoder->encodeIfSupported($update); From 3163a7cd5175bff5ccbcc071512a7b3b5621975b Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 21 Mar 2025 20:41:37 -0400 Subject: [PATCH 16/38] CRUD prose tests 3 and 4 --- ...BulkWriteSplitsOnMaxWriteBatchSizeTest.php | 71 +++++++++++++++++ ...lkWriteSplitsOnMaxMessageSizeBytesTest.php | 77 +++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php create mode 100644 tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php diff --git a/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php b/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php new file mode 100644 index 000000000..4b3066fe3 --- /dev/null +++ b/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php @@ -0,0 +1,71 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $client = self::createTestClient(); + + $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; + self::assertIsInt($maxWriteBatchSize); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection); + + for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { + $bulkWrite->insertOne(['a' => 'b']); + } + + $subscriber = new class implements CommandSubscriber { + public array $commandStartedEvents = []; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'bulkWrite') { + $this->commandStartedEvents[] = $event; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + $result = $client->bulkWrite($bulkWrite); + + self::assertSame($maxWriteBatchSize + 1, $result->getInsertedCount()); + self::assertCount(2, $subscriber->commandStartedEvents); + [$firstEvent, $secondEvent] = $subscriber->commandStartedEvents; + self::assertIsArray($firstCommandOps = $firstEvent->getCommand()->ops ?? null); + self::assertCount($maxWriteBatchSize, $firstCommandOps); + self::assertIsArray($secondCommandOps = $secondEvent->getCommand()->ops ?? null); + self::assertCount(1, $secondCommandOps); + self::assertEquals($firstEvent->getOperationId(), $secondEvent->getOperationId()); + } +} diff --git a/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php b/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php new file mode 100644 index 000000000..8e16765cc --- /dev/null +++ b/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php @@ -0,0 +1,77 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $client = self::createTestClient(); + + $hello = $this->getPrimaryServer()->getInfo(); + self::assertIsInt($maxBsonObjectSize = $hello['maxBsonObjectSize'] ?? null); + self::assertIsInt($maxMessageSizeBytes = $hello['maxMessageSizeBytes'] ?? null); + + $numModels = (int) ($maxMessageSizeBytes / $maxBsonObjectSize + 1); + $document = ['a' => str_repeat('b', $maxBsonObjectSize - 500)]; + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection); + + for ($i = 0; $i < $numModels; ++$i) { + $bulkWrite->insertOne($document); + } + + $subscriber = new class implements CommandSubscriber { + public array $commandStartedEvents = []; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'bulkWrite') { + $this->commandStartedEvents[] = $event; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + $result = $client->bulkWrite($bulkWrite); + + self::assertSame($numModels, $result->getInsertedCount()); + self::assertCount(2, $subscriber->commandStartedEvents); + [$firstEvent, $secondEvent] = $subscriber->commandStartedEvents; + self::assertIsArray($firstCommandOps = $firstEvent->getCommand()->ops ?? null); + self::assertCount($numModels - 1, $firstCommandOps); + self::assertIsArray($secondCommandOps = $secondEvent->getCommand()->ops ?? null); + self::assertCount(1, $secondCommandOps); + self::assertEquals($firstEvent->getOperationId(), $secondEvent->getOperationId()); + } +} From 36e08f16e2e3d257b1aa692bfabe8dd39b6c1693 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 31 Mar 2025 16:21:10 -0400 Subject: [PATCH 17/38] Update Psalm stubs for PHPC BulkWriteCommand API --- .../Driver/BulkWriteCommandException.stub.php | 25 ++++++++++++++++--- stubs/Driver/BulkWriteCommandResult.stub.php | 12 --------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/stubs/Driver/BulkWriteCommandException.stub.php b/stubs/Driver/BulkWriteCommandException.stub.php index 381331036..3145f9ad0 100644 --- a/stubs/Driver/BulkWriteCommandException.stub.php +++ b/stubs/Driver/BulkWriteCommandException.stub.php @@ -2,13 +2,32 @@ namespace MongoDB\Driver\Exception; +use MongoDB\BSON\Document; use MongoDB\Driver\BulkWriteCommandResult; -class BulkWriteCommandException extends ServerException +final class BulkWriteCommandException extends ServerException { - protected BulkWriteCommandResult $bulkWriteCommandResult; + private ?Document $errorReply = null; - final public function getBulkWriteCommandResult(): BulkWriteCommandResult + private ?BulkWriteCommandResult $partialResult = null; + + private array $writeErrors = []; + + private array $writeConcernErrors = []; + + final public function getErrorReply(): ?Document + { + } + + final public function getPartialResult(): ?BulkWriteCommandResult + { + } + + final public function getWriteErrors(): array + { + } + + final public function getWriteConcernErrors(): array { } } diff --git a/stubs/Driver/BulkWriteCommandResult.stub.php b/stubs/Driver/BulkWriteCommandResult.stub.php index 28ed50dd6..1c4a0092c 100644 --- a/stubs/Driver/BulkWriteCommandResult.stub.php +++ b/stubs/Driver/BulkWriteCommandResult.stub.php @@ -42,18 +42,6 @@ final public function getDeleteResults(): ?Document { } - final public function getWriteErrors(): array - { - } - - final public function getWriteConcernErrors(): array - { - } - - final public function getErrorReply(): ?Document - { - } - final public function isAcknowledged(): bool { } From 21af35081db0c0eaac422baf2b599a7ecd4bac5f Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 31 Mar 2025 14:49:03 -0400 Subject: [PATCH 18/38] CRUD prose tests 5-9 --- ...ctsWriteConcernErrorsAcrossBatchesTest.php | 84 +++++++++++++ ...iteHandlesWriteErrorsAcrossBatchesTest.php | 110 ++++++++++++++++++ ...WriteHandlesCursorRequiringGetMoreTest.php | 76 ++++++++++++ ...rRequiringGetMoreWithinTransactionTest.php | 82 +++++++++++++ ...rose9_BulkWriteHandlesGetMoreErrorTest.php | 104 +++++++++++++++++ 5 files changed, 456 insertions(+) create mode 100644 tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php create mode 100644 tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php create mode 100644 tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php create mode 100644 tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php create mode 100644 tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php diff --git a/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php b/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php new file mode 100644 index 000000000..eb864ad66 --- /dev/null +++ b/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php @@ -0,0 +1,84 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $client = self::createTestClient(null, ['retryWrites' => false]); + + $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; + self::assertIsInt($maxWriteBatchSize); + + $this->configureFailPoint([ + 'configureFailPoint' => 'failCommand', + 'mode' => ['times' => 2], + 'data' => [ + 'failCommands' => ['bulkWrite'], + 'writeConcernError' => [ + 'code' => 91, // ShutdownInProgress + 'errmsg' => 'Replication is being shut down', + ], + ], + ]); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection); + + for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { + $bulkWrite->insertOne(['a' => 'b']); + } + + $subscriber = new class implements CommandSubscriber { + public int $numBulkWriteObserved = 0; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'bulkWrite') { + ++$this->numBulkWriteObserved; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + try { + $client->bulkWrite($bulkWrite); + self::fail('BulkWriteCommandException was not thrown'); + } catch (BulkWriteCommandException $e) { + self::assertCount(2, $e->getWriteConcernErrors()); + $partialResult = $e->getPartialResult(); + self::assertNotNull($partialResult); + self::assertSame($maxWriteBatchSize + 1, $partialResult->getInsertedCount()); + self::assertSame(2, $subscriber->numBulkWriteObserved); + } + } +} diff --git a/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php b/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php new file mode 100644 index 000000000..2876c5c3c --- /dev/null +++ b/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php @@ -0,0 +1,110 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + } + + public function testOrdered(): void + { + $client = self::createTestClient(); + + $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; + self::assertIsInt($maxWriteBatchSize); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $collection->drop(); + $collection->insertOne(['_id' => 1]); + + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['ordered' => true]); + + for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { + $bulkWrite->insertOne(['_id' => 1]); + } + + $subscriber = $this->createSubscriber(); + $client->addSubscriber($subscriber); + + try { + $client->bulkWrite($bulkWrite); + self::fail('BulkWriteCommandException was not thrown'); + } catch (BulkWriteCommandException $e) { + self::assertCount(1, $e->getWriteErrors()); + self::assertSame(1, $subscriber->numBulkWriteObserved); + } + } + + public function testUnordered(): void + { + $client = self::createTestClient(); + + $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; + self::assertIsInt($maxWriteBatchSize); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $collection->drop(); + $collection->insertOne(['_id' => 1]); + + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['ordered' => false]); + + for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { + $bulkWrite->insertOne(['_id' => 1]); + } + + $subscriber = $this->createSubscriber(); + $client->addSubscriber($subscriber); + + try { + $client->bulkWrite($bulkWrite); + self::fail('BulkWriteCommandException was not thrown'); + } catch (BulkWriteCommandException $e) { + self::assertCount($maxWriteBatchSize + 1, $e->getWriteErrors()); + self::assertSame(2, $subscriber->numBulkWriteObserved); + } + } + + private function createSubscriber(): CommandSubscriber + { + return new class implements CommandSubscriber { + public int $numBulkWriteObserved = 0; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'bulkWrite') { + ++$this->numBulkWriteObserved; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + } +} diff --git a/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php b/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php new file mode 100644 index 000000000..202418224 --- /dev/null +++ b/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php @@ -0,0 +1,76 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $client = self::createTestClient(); + + $maxBsonObjectSize = $this->getPrimaryServer()->getInfo()['maxBsonObjectSize'] ?? null; + self::assertIsInt($maxBsonObjectSize); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $collection->drop(); + + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['verboseResults' => true]); + $bulkWrite->updateOne( + ['_id' => str_repeat('a', (int) ($maxBsonObjectSize / 2))], + ['$set' => ['x' => 1]], + ['upsert' => true], + ); + $bulkWrite->updateOne( + ['_id' => str_repeat('b', (int) ($maxBsonObjectSize / 2))], + ['$set' => ['x' => 1]], + ['upsert' => true], + ); + + $subscriber = new class implements CommandSubscriber { + public int $numGetMoreObserved = 0; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'getMore') { + ++$this->numGetMoreObserved; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + $result = $client->bulkWrite($bulkWrite); + + self::assertSame(2, $result->getUpsertedCount()); + self::assertCount(2, $result->getUpdateResults()); + self::assertSame(1, $subscriber->numGetMoreObserved); + } +} diff --git a/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php b/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php new file mode 100644 index 000000000..a26b7bf43 --- /dev/null +++ b/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php @@ -0,0 +1,82 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + $this->skipIfTransactionsAreNotSupported(); + + $client = self::createTestClient(); + + $maxBsonObjectSize = $this->getPrimaryServer()->getInfo()['maxBsonObjectSize'] ?? null; + self::assertIsInt($maxBsonObjectSize); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $collection->drop(); + + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['verboseResults' => true]); + $bulkWrite->updateOne( + ['_id' => str_repeat('a', (int) ($maxBsonObjectSize / 2))], + ['$set' => ['x' => 1]], + ['upsert' => true], + ); + $bulkWrite->updateOne( + ['_id' => str_repeat('b', (int) ($maxBsonObjectSize / 2))], + ['$set' => ['x' => 1]], + ['upsert' => true], + ); + + $subscriber = new class implements CommandSubscriber { + public int $numGetMoreObserved = 0; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'getMore') { + ++$this->numGetMoreObserved; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + $session = $client->startSession(); + $session->startTransaction(); + + /* Note: the prose test does not call for committing the transaction. + * The transaction will be aborted when the Session object is freed. */ + $result = $client->bulkWrite($bulkWrite, ['session' => $session]); + + self::assertSame(2, $result->getUpsertedCount()); + self::assertCount(2, $result->getUpdateResults()); + self::assertSame(1, $subscriber->numGetMoreObserved); + } +} diff --git a/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php b/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php new file mode 100644 index 000000000..ba04c984e --- /dev/null +++ b/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php @@ -0,0 +1,104 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $client = self::createTestClient(); + + $maxBsonObjectSize = $this->getPrimaryServer()->getInfo()['maxBsonObjectSize'] ?? null; + self::assertIsInt($maxBsonObjectSize); + + $this->configureFailPoint([ + 'configureFailPoint' => 'failCommand', + 'mode' => ['times' => 1], + 'data' => [ + 'failCommands' => ['getMore'], + 'errorCode' => self::UNKNOWN_ERROR, + ], + ]); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $collection->drop(); + + $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['verboseResults' => true]); + $bulkWrite->updateOne( + ['_id' => str_repeat('a', (int) ($maxBsonObjectSize / 2))], + ['$set' => ['x' => 1]], + ['upsert' => true], + ); + $bulkWrite->updateOne( + ['_id' => str_repeat('b', (int) ($maxBsonObjectSize / 2))], + ['$set' => ['x' => 1]], + ['upsert' => true], + ); + + $subscriber = new class implements CommandSubscriber { + public int $numGetMoreObserved = 0; + public int $numKillCursorsObserved = 0; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'getMore') { + ++$this->numGetMoreObserved; + } elseif ($event->getCommandName() === 'killCursors') { + ++$this->numKillCursorsObserved; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + try { + $client->bulkWrite($bulkWrite); + self::fail('BulkWriteCommandException was not thrown'); + } catch (BulkWriteCommandException $e) { + $errorReply = $e->getErrorReply(); + $this->assertNotNull($errorReply); + $this->assertSame(self::UNKNOWN_ERROR, $errorReply['code'] ?? null); + + // PHPC will also apply the top-level error code to BulkWriteCommandException + $this->assertSame(self::UNKNOWN_ERROR, $e->getCode()); + + $partialResult = $e->getPartialResult(); + self::assertNotNull($partialResult); + self::assertSame(2, $partialResult->getUpsertedCount()); + self::assertCount(1, $partialResult->getUpdateResults()); + self::assertSame(1, $subscriber->numGetMoreObserved); + self::assertSame(1, $subscriber->numKillCursorsObserved); + } + } +} From 068626326ca7ee465f681c8e5855d7c2675ae1ac Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 1 Apr 2025 16:02:05 -0400 Subject: [PATCH 19/38] Rename BulkWriteCommandBuilder to ClientBulkWrite Also renames the operation class to ClientBulkWriteCommand to avoid aliasing in Client. --- src/Client.php | 16 ++++++++-------- ...iteCommandBuilder.php => ClientBulkWrite.php} | 2 +- ...tBulkWrite.php => ClientBulkWriteCommand.php} | 2 +- ...e3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php | 4 ++-- ..._BulkWriteSplitsOnMaxMessageSizeBytesTest.php | 4 ++-- ...llectsWriteConcernErrorsAcrossBatchesTest.php | 4 ++-- ...kWriteHandlesWriteErrorsAcrossBatchesTest.php | 6 +++--- ...ulkWriteHandlesCursorRequiringGetMoreTest.php | 4 ++-- ...rsorRequiringGetMoreWithinTransactionTest.php | 4 ++-- .../Prose9_BulkWriteHandlesGetMoreErrorTest.php | 4 ++-- tests/UnifiedSpecTests/Operation.php | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) rename src/{BulkWriteCommandBuilder.php => ClientBulkWrite.php} (99%) rename src/Operation/{ClientBulkWrite.php => ClientBulkWriteCommand.php} (98%) diff --git a/src/Client.php b/src/Client.php index af7cbe53a..d197dccae 100644 --- a/src/Client.php +++ b/src/Client.php @@ -41,7 +41,7 @@ use MongoDB\Model\BSONArray; use MongoDB\Model\BSONDocument; use MongoDB\Model\DatabaseInfo; -use MongoDB\Operation\ClientBulkWrite; +use MongoDB\Operation\ClientBulkWriteCommand; use MongoDB\Operation\DropDatabase; use MongoDB\Operation\ListDatabaseNames; use MongoDB\Operation\ListDatabases; @@ -193,26 +193,26 @@ final public function addSubscriber(Subscriber $subscriber): void } /** - * Executes multiple write operations. + * Executes multiple write operations across multiple namespaces. * - * @see ClientBulkWrite::__construct() for supported options - * @param string $databaseName Database name - * @param array $options Additional options + * @param BulkWriteCommand|ClientBulkWrite $bulk Assembled bulk write command or builder + * @param array $options Additional options * @throws UnsupportedException if options are unsupported on the selected server * @throws InvalidArgumentException for parameter/option parsing errors * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + * @see ClientBulkWriteCommand::__construct() for supported options */ - public function bulkWrite(BulkWriteCommand|BulkWriteCommandBuilder $bulk, array $options = []): ?BulkWriteCommandResult + public function bulkWrite(BulkWriteCommand|ClientBulkWrite $bulk, array $options = []): ?BulkWriteCommandResult { if (! isset($options['writeConcern']) && ! is_in_transaction($options)) { $options['writeConcern'] = $this->writeConcern; } - if ($bulk instanceof BulkWriteCommandBuilder) { + if ($bulk instanceof ClientBulkWrite) { $bulk = $bulk->bulkWriteCommand; } - $operation = new ClientBulkWrite($bulk, $options); + $operation = new ClientBulkWriteCommand($bulk, $options); $server = select_server_for_write($this->manager, $options); return $operation->execute($server); diff --git a/src/BulkWriteCommandBuilder.php b/src/ClientBulkWrite.php similarity index 99% rename from src/BulkWriteCommandBuilder.php rename to src/ClientBulkWrite.php index 22d3bf42f..058e58971 100644 --- a/src/BulkWriteCommandBuilder.php +++ b/src/ClientBulkWrite.php @@ -27,7 +27,7 @@ use function is_bool; use function is_string; -final readonly class BulkWriteCommandBuilder +final readonly class ClientBulkWrite { private function __construct( public BulkWriteCommand $bulkWriteCommand, diff --git a/src/Operation/ClientBulkWrite.php b/src/Operation/ClientBulkWriteCommand.php similarity index 98% rename from src/Operation/ClientBulkWrite.php rename to src/Operation/ClientBulkWriteCommand.php index 635805930..e0de60252 100644 --- a/src/Operation/ClientBulkWrite.php +++ b/src/Operation/ClientBulkWriteCommand.php @@ -34,7 +34,7 @@ * * @see \MongoDB\Client::bulkWrite() */ -final class ClientBulkWrite +final class ClientBulkWriteCommand { /** * Constructs a client-level bulk write operation. diff --git a/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php b/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php index 4b3066fe3..eb7334c19 100644 --- a/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php +++ b/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber; @@ -30,7 +30,7 @@ public function testSplitOnMaxWriteBatchSize(): void self::assertIsInt($maxWriteBatchSize); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection); + $bulkWrite = ClientBulkWrite::createWithCollection($collection); for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { $bulkWrite->insertOne(['a' => 'b']); diff --git a/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php b/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php index 8e16765cc..1264d4f86 100644 --- a/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php +++ b/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber; @@ -36,7 +36,7 @@ public function testSplitOnMaxWriteBatchSize(): void $document = ['a' => str_repeat('b', $maxBsonObjectSize - 500)]; $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection); + $bulkWrite = ClientBulkWrite::createWithCollection($collection); for ($i = 0; $i < $numModels; ++$i) { $bulkWrite->insertOne($document); diff --git a/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php b/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php index eb864ad66..a32e15d3b 100644 --- a/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php +++ b/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Exception\BulkWriteCommandException; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; @@ -43,7 +43,7 @@ public function testCollectWriteConcernErrors(): void ]); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection); + $bulkWrite = ClientBulkWrite::createWithCollection($collection); for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { $bulkWrite->insertOne(['a' => 'b']); diff --git a/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php b/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php index 2876c5c3c..3a07c8222 100644 --- a/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php +++ b/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Exception\BulkWriteCommandException; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; @@ -39,7 +39,7 @@ public function testOrdered(): void $collection->drop(); $collection->insertOne(['_id' => 1]); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['ordered' => true]); + $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['ordered' => true]); for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { $bulkWrite->insertOne(['_id' => 1]); @@ -68,7 +68,7 @@ public function testUnordered(): void $collection->drop(); $collection->insertOne(['_id' => 1]); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['ordered' => false]); + $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['ordered' => false]); for ($i = 0; $i < $maxWriteBatchSize + 1; ++$i) { $bulkWrite->insertOne(['_id' => 1]); diff --git a/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php b/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php index 202418224..9761c82b4 100644 --- a/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php +++ b/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber; @@ -34,7 +34,7 @@ public function testHandlesCursor(): void $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $collection->drop(); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['verboseResults' => true]); + $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['verboseResults' => true]); $bulkWrite->updateOne( ['_id' => str_repeat('a', (int) ($maxBsonObjectSize / 2))], ['$set' => ['x' => 1]], diff --git a/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php b/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php index a26b7bf43..be900234c 100644 --- a/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php +++ b/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber; @@ -35,7 +35,7 @@ public function testHandlesCursorWithinTransaction(): void $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $collection->drop(); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['verboseResults' => true]); + $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['verboseResults' => true]); $bulkWrite->updateOne( ['_id' => str_repeat('a', (int) ($maxBsonObjectSize / 2))], ['$set' => ['x' => 1]], diff --git a/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php b/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php index ba04c984e..a00b3883d 100644 --- a/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php +++ b/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php @@ -2,7 +2,7 @@ namespace MongoDB\Tests\SpecTests\Crud; -use MongoDB\BulkWriteCommandBuilder; +use MongoDB\ClientBulkWrite; use MongoDB\Driver\Exception\BulkWriteCommandException; use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; @@ -46,7 +46,7 @@ public function testHandlesGetMoreError(): void $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $collection->drop(); - $bulkWrite = BulkWriteCommandBuilder::createWithCollection($collection, ['verboseResults' => true]); + $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['verboseResults' => true]); $bulkWrite->updateOne( ['_id' => str_repeat('a', (int) ($maxBsonObjectSize / 2))], ['$set' => ['x' => 1]], diff --git a/tests/UnifiedSpecTests/Operation.php b/tests/UnifiedSpecTests/Operation.php index d83b71fae..8cb4509dd 100644 --- a/tests/UnifiedSpecTests/Operation.php +++ b/tests/UnifiedSpecTests/Operation.php @@ -259,7 +259,7 @@ private function executeForClient(Client $client) assertArrayHasKey('models', $args); assertIsArray($args['models']); - // Options for BulkWriteCommand and Server::executeBulkWriteCommand() will be mixed + // Options for ClientBulkWriteCommand and Server::executeBulkWriteCommand() will be mixed $options = array_diff_key($args, ['models' => 1]); return $client->bulkWrite( From 408ad40ca8dfbf555cca189dd372dd6fdefebae0 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 1 Apr 2025 16:03:18 -0400 Subject: [PATCH 20/38] Server::executeBulkWriteCommand() always returns a BulkWriteCommandResult --- src/Client.php | 2 +- src/Operation/ClientBulkWriteCommand.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index d197dccae..dce14446e 100644 --- a/src/Client.php +++ b/src/Client.php @@ -202,7 +202,7 @@ final public function addSubscriber(Subscriber $subscriber): void * @throws DriverRuntimeException for other driver errors (e.g. connection errors) * @see ClientBulkWriteCommand::__construct() for supported options */ - public function bulkWrite(BulkWriteCommand|ClientBulkWrite $bulk, array $options = []): ?BulkWriteCommandResult + public function bulkWrite(BulkWriteCommand|ClientBulkWrite $bulk, array $options = []): BulkWriteCommandResult { if (! isset($options['writeConcern']) && ! is_in_transaction($options)) { $options['writeConcern'] = $this->writeConcern; diff --git a/src/Operation/ClientBulkWriteCommand.php b/src/Operation/ClientBulkWriteCommand.php index e0de60252..252629f83 100644 --- a/src/Operation/ClientBulkWriteCommand.php +++ b/src/Operation/ClientBulkWriteCommand.php @@ -78,7 +78,7 @@ public function __construct(private BulkWriteCommand $bulkWriteCommand, private * @throws UnsupportedException if write concern is used and unsupported * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ - public function execute(Server $server): ?BulkWriteCommandResult + public function execute(Server $server): BulkWriteCommandResult { $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); if ($inTransaction && isset($this->options['writeConcern'])) { From bc455713a2b8808684c740682c589248ec2d02b4 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 2 Apr 2025 12:11:35 -0400 Subject: [PATCH 21/38] Use dropCollection() helper to ensure collections are cleaned up --- .../Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php | 1 + .../Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php | 1 + ...5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php | 1 + .../Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php | 4 ++-- .../Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php | 2 +- ...riteHandlesCursorRequiringGetMoreWithinTransactionTest.php | 2 +- .../Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php | 2 +- 7 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php b/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php index eb7334c19..94e158423 100644 --- a/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php +++ b/tests/SpecTests/Crud/Prose3_BulkWriteSplitsOnMaxWriteBatchSizeTest.php @@ -29,6 +29,7 @@ public function testSplitOnMaxWriteBatchSize(): void $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; self::assertIsInt($maxWriteBatchSize); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $bulkWrite = ClientBulkWrite::createWithCollection($collection); diff --git a/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php b/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php index 1264d4f86..2b45a5999 100644 --- a/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php +++ b/tests/SpecTests/Crud/Prose4_BulkWriteSplitsOnMaxMessageSizeBytesTest.php @@ -35,6 +35,7 @@ public function testSplitOnMaxWriteBatchSize(): void $numModels = (int) ($maxMessageSizeBytes / $maxBsonObjectSize + 1); $document = ['a' => str_repeat('b', $maxBsonObjectSize - 500)]; + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $bulkWrite = ClientBulkWrite::createWithCollection($collection); diff --git a/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php b/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php index a32e15d3b..727347a95 100644 --- a/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php +++ b/tests/SpecTests/Crud/Prose5_BulkWriteCollectsWriteConcernErrorsAcrossBatchesTest.php @@ -42,6 +42,7 @@ public function testCollectWriteConcernErrors(): void ], ]); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $bulkWrite = ClientBulkWrite::createWithCollection($collection); diff --git a/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php b/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php index 3a07c8222..975147e4f 100644 --- a/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php +++ b/tests/SpecTests/Crud/Prose6_BulkWriteHandlesWriteErrorsAcrossBatchesTest.php @@ -35,8 +35,8 @@ public function testOrdered(): void $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; self::assertIsInt($maxWriteBatchSize); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $collection->drop(); $collection->insertOne(['_id' => 1]); $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['ordered' => true]); @@ -64,8 +64,8 @@ public function testUnordered(): void $maxWriteBatchSize = $this->getPrimaryServer()->getInfo()['maxWriteBatchSize'] ?? null; self::assertIsInt($maxWriteBatchSize); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $collection->drop(); $collection->insertOne(['_id' => 1]); $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['ordered' => false]); diff --git a/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php b/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php index 9761c82b4..1171c3998 100644 --- a/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php +++ b/tests/SpecTests/Crud/Prose7_BulkWriteHandlesCursorRequiringGetMoreTest.php @@ -31,8 +31,8 @@ public function testHandlesCursor(): void $maxBsonObjectSize = $this->getPrimaryServer()->getInfo()['maxBsonObjectSize'] ?? null; self::assertIsInt($maxBsonObjectSize); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $collection->drop(); $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['verboseResults' => true]); $bulkWrite->updateOne( diff --git a/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php b/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php index be900234c..d6b3290ae 100644 --- a/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php +++ b/tests/SpecTests/Crud/Prose8_BulkWriteHandlesCursorRequiringGetMoreWithinTransactionTest.php @@ -32,8 +32,8 @@ public function testHandlesCursorWithinTransaction(): void $maxBsonObjectSize = $this->getPrimaryServer()->getInfo()['maxBsonObjectSize'] ?? null; self::assertIsInt($maxBsonObjectSize); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $collection->drop(); $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['verboseResults' => true]); $bulkWrite->updateOne( diff --git a/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php b/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php index a00b3883d..d65cf0b05 100644 --- a/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php +++ b/tests/SpecTests/Crud/Prose9_BulkWriteHandlesGetMoreErrorTest.php @@ -43,8 +43,8 @@ public function testHandlesGetMoreError(): void ], ]); + $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); - $collection->drop(); $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['verboseResults' => true]); $bulkWrite->updateOne( From 7b150c8d0a23c1d7a8fde2942d597697f7cfa0c3 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 2 Apr 2025 12:12:18 -0400 Subject: [PATCH 22/38] Prose test 11 --- ...itsWhenNamespaceExceedsMessageSizeTest.php | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/SpecTests/Crud/Prose11_BulkWriteBatchSplitsWhenNamespaceExceedsMessageSizeTest.php diff --git a/tests/SpecTests/Crud/Prose11_BulkWriteBatchSplitsWhenNamespaceExceedsMessageSizeTest.php b/tests/SpecTests/Crud/Prose11_BulkWriteBatchSplitsWhenNamespaceExceedsMessageSizeTest.php new file mode 100644 index 000000000..a44986217 --- /dev/null +++ b/tests/SpecTests/Crud/Prose11_BulkWriteBatchSplitsWhenNamespaceExceedsMessageSizeTest.php @@ -0,0 +1,127 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $this->client = self::createTestClient(); + + $hello = $this->getPrimaryServer()->getInfo(); + self::assertIsInt($maxBsonObjectSize = $hello['maxBsonObjectSize'] ?? null); + self::assertIsInt($maxMessageSizeBytes = $hello['maxMessageSizeBytes'] ?? null); + + $opsBytes = $maxMessageSizeBytes - 1122; + $this->numModels = (int) ($opsBytes / $maxBsonObjectSize); + $remainderBytes = $opsBytes % $maxBsonObjectSize; + + // Use namespaces specific to the test, as they are relevant to batch calculations + $this->dropCollection('db', 'coll'); + $collection = $this->client->selectCollection('db', 'coll'); + + $this->bulkWrite = ClientBulkWrite::createWithCollection($collection); + + for ($i = 0; $i < $this->numModels; ++$i) { + $this->bulkWrite->insertOne(['a' => str_repeat('b', $maxBsonObjectSize - 57)]); + } + + if ($remainderBytes >= 217) { + ++$this->numModels; + $this->bulkWrite->insertOne(['a' => str_repeat('b', $remainderBytes - 57)]); + } + } + + public function testNoBatchSplittingRequired(): void + { + $subscriber = $this->createSubscriber(); + $this->client->addSubscriber($subscriber); + + $this->bulkWrite->insertOne(['a' => 'b']); + + $result = $this->client->bulkWrite($this->bulkWrite); + + self::assertSame($this->numModels + 1, $result->getInsertedCount()); + self::assertCount(1, $subscriber->commandStartedEvents); + $command = $subscriber->commandStartedEvents[0]->getCommand(); + self::assertCount($this->numModels + 1, $command->ops); + self::assertCount(1, $command->nsInfo); + self::assertSame('db.coll', $command->nsInfo[0]->ns ?? null); + } + + public function testBatchSplittingRequired(): void + { + $subscriber = $this->createSubscriber(); + $this->client->addSubscriber($subscriber); + + $secondCollectionName = str_repeat('c', 200); + $this->dropCollection('db', $secondCollectionName); + $secondCollection = $this->client->selectCollection('db', $secondCollectionName); + $this->bulkWrite->withCollection($secondCollection)->insertOne(['a' => 'b']); + + $result = $this->client->bulkWrite($this->bulkWrite); + + self::assertSame($this->numModels + 1, $result->getInsertedCount()); + self::assertCount(2, $subscriber->commandStartedEvents); + [$firstEvent, $secondEvent] = $subscriber->commandStartedEvents; + + $firstCommand = $firstEvent->getCommand(); + self::assertCount($this->numModels, $firstCommand->ops); + self::assertCount(1, $firstCommand->nsInfo); + self::assertSame('db.coll', $firstCommand->nsInfo[0]->ns ?? null); + + $secondCommand = $secondEvent->getCommand(); + self::assertCount(1, $secondCommand->ops); + self::assertCount(1, $secondCommand->nsInfo); + self::assertSame($secondCollection->getNamespace(), $secondCommand->nsInfo[0]->ns ?? null); + } + + private function createSubscriber(): CommandSubscriber + { + return new class implements CommandSubscriber { + public array $commandStartedEvents = []; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'bulkWrite') { + $this->commandStartedEvents[] = $event; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + } +} From 49a97e9f952a0dacd7b778404ccef421661f74a8 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 2 Apr 2025 13:16:28 -0400 Subject: [PATCH 23/38] Prose test 12 --- ...ulkWriteExceedsMaxMessageSizeBytesTest.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php diff --git a/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php b/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php new file mode 100644 index 000000000..854c101d0 --- /dev/null +++ b/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php @@ -0,0 +1,76 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + } + + public function testDocumentTooLarge(): void + { + $client = self::createTestClient(); + + $maxMessageSizeBytes = $this->getPrimaryServer()->getInfo()['maxMessageSizeBytes'] ?? null; + self::assertIsInt($maxMessageSizeBytes); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $bulkWrite = ClientBulkWrite::createWithCollection($collection); + $bulkWrite->insertOne(['a' => str_repeat('b', $maxMessageSizeBytes)]); + + try { + $client->bulkWrite($bulkWrite); + self::fail('Exception was not thrown'); + } catch (BulkWriteCommandException $e) { + /* Note: although the client-side error occurs on the first operation, libmongoc still populates the partial + * result (see: CDRIVER-5969). This causes PHPC to proxy the underlying InvalidArgumentException behind + * BulkWriteCommandException. Until this is addressed, unwrap the error and check the partial result. */ + self::assertInstanceOf(InvalidArgumentException::class, $e->getPrevious()); + self::assertSame(0, $e->getPartialResult()->getInsertedCount()); + } + } + + public function testNamespaceTooLarge(): void + { + $client = self::createTestClient(); + + $maxMessageSizeBytes = $this->getPrimaryServer()->getInfo()['maxMessageSizeBytes'] ?? null; + self::assertIsInt($maxMessageSizeBytes); + + $collectionName = str_repeat('c', $maxMessageSizeBytes); + $collection = $client->selectCollection($this->getDatabaseName(), $collectionName); + $bulkWrite = ClientBulkWrite::createWithCollection($collection); + $bulkWrite->insertOne(['a' => 'b']); + + try { + $client->bulkWrite($bulkWrite); + self::fail('Exception was not thrown'); + } catch (BulkWriteCommandException $e) { + /* Note: although the client-side error occurs on the first operation, libmongoc still populates the partial + * result (see: CDRIVER-5969). This causes PHPC to proxy the underlying InvalidArgumentException behind + * BulkWriteCommandException. Until this is addressed, unwrap the error and check the partial result. */ + self::assertInstanceOf(InvalidArgumentException::class, $e->getPrevious()); + self::assertSame(0, $e->getPartialResult()->getInsertedCount()); + } + } +} From e6a37b2e839b1ec7e85e29ba3fec257fde212dbc Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 2 Apr 2025 14:44:42 -0400 Subject: [PATCH 24/38] Prose test 13 --- ...kWriteUnsupportedForAutoEncryptionTest.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/SpecTests/Crud/Prose13_BulkWriteUnsupportedForAutoEncryptionTest.php diff --git a/tests/SpecTests/Crud/Prose13_BulkWriteUnsupportedForAutoEncryptionTest.php b/tests/SpecTests/Crud/Prose13_BulkWriteUnsupportedForAutoEncryptionTest.php new file mode 100644 index 000000000..d9b6b70a7 --- /dev/null +++ b/tests/SpecTests/Crud/Prose13_BulkWriteUnsupportedForAutoEncryptionTest.php @@ -0,0 +1,44 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $this->skipIfClientSideEncryptionIsNotSupported(); + + $client = self::createTestClient(null, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => $this->getNamespace(), + 'kmsProviders' => ['aws' => ['accessKeyId' => 'foo', 'secretAccessKey' => 'bar']], + ], + ]); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $bulkWrite = ClientBulkWrite::createWithCollection($collection); + $bulkWrite->insertOne(['a' => 'b']); + + try { + $client->bulkWrite($bulkWrite); + self::fail('InvalidArgumentException was not thrown'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('bulkWrite does not currently support automatic encryption', $e->getMessage()); + } + } +} From e63f54b01abb2eba22ef8ba900b9b600581e49a4 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 2 Apr 2025 16:29:24 -0400 Subject: [PATCH 25/38] Prose test 15 --- ...ulkWriteUnacknowledgedWriteConcernTest.php | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/SpecTests/Crud/Prose15_BulkWriteUnacknowledgedWriteConcernTest.php diff --git a/tests/SpecTests/Crud/Prose15_BulkWriteUnacknowledgedWriteConcernTest.php b/tests/SpecTests/Crud/Prose15_BulkWriteUnacknowledgedWriteConcernTest.php new file mode 100644 index 000000000..f35433dac --- /dev/null +++ b/tests/SpecTests/Crud/Prose15_BulkWriteUnacknowledgedWriteConcernTest.php @@ -0,0 +1,87 @@ +isServerless()) { + $this->markTestSkipped('bulkWrite command is not supported'); + } + + $this->skipIfServerVersion('<', '8.0', 'bulkWrite command is not supported'); + + $client = self::createTestClient(); + + $hello = $this->getPrimaryServer()->getInfo(); + self::assertIsInt($maxBsonObjectSize = $hello['maxBsonObjectSize'] ?? null); + self::assertIsInt($maxMessageSizeBytes = $hello['maxMessageSizeBytes'] ?? null); + + // Explicitly create the collection to work around SERVER-95537 + $this->createCollection($this->getDatabaseName(), $this->getCollectionName()); + + $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); + $bulkWrite = ClientBulkWrite::createWithCollection($collection, ['ordered' => false]); + + $numModels = (int) ($maxMessageSizeBytes / $maxBsonObjectSize) + 1; + + for ($i = 0; $i < $numModels; ++$i) { + $bulkWrite->insertOne(['a' => str_repeat('b', $maxBsonObjectSize - 500)]); + } + + $subscriber = new class implements CommandSubscriber { + public array $commandStartedEvents = []; + + public function commandStarted(CommandStartedEvent $event): void + { + if ($event->getCommandName() === 'bulkWrite') { + $this->commandStartedEvents[] = $event; + } + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + } + + public function commandFailed(CommandFailedEvent $event): void + { + } + }; + + $client->addSubscriber($subscriber); + + $result = $client->bulkWrite($bulkWrite, ['writeConcern' => new WriteConcern(0)]); + + self::assertFalse($result->isAcknowledged()); + self::assertCount(2, $subscriber->commandStartedEvents); + [$firstEvent, $secondEvent] = $subscriber->commandStartedEvents; + + $firstCommand = $firstEvent->getCommand(); + self::assertIsArray($firstCommand->ops ?? null); + self::assertCount($numModels - 1, $firstCommand->ops); + self::assertSame(0, $firstCommand->writeConcern->w ?? null); + + $secondCommand = $secondEvent->getCommand(); + self::assertIsArray($secondCommand->ops ?? null); + self::assertCount(1, $secondCommand->ops); + self::assertSame(0, $secondCommand->writeConcern->w ?? null); + + self::assertSame($numModels, $collection->countDocuments()); + } +} From 683da0377da06a1e5c37d5410b52a68264c7425f Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 4 Apr 2025 14:04:00 -0400 Subject: [PATCH 26/38] Validate assigned $options property instead of ctor arg --- src/Operation/ClientBulkWriteCommand.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Operation/ClientBulkWriteCommand.php b/src/Operation/ClientBulkWriteCommand.php index 252629f83..ba6632116 100644 --- a/src/Operation/ClientBulkWriteCommand.php +++ b/src/Operation/ClientBulkWriteCommand.php @@ -59,16 +59,16 @@ public function __construct(private BulkWriteCommand $bulkWriteCommand, private throw new InvalidArgumentException('$bulkWriteCommand is empty'); } - if (isset($options['session']) && ! $options['session'] instanceof Session) { - throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class); + if (isset($this->options['session']) && ! $this->options['session'] instanceof Session) { + throw InvalidArgumentException::invalidType('"session" option', $this->options['session'], Session::class); } - if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) { - throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); + if (isset($this->options['writeConcern']) && ! $this->options['writeConcern'] instanceof WriteConcern) { + throw InvalidArgumentException::invalidType('"writeConcern" option', $this->options['writeConcern'], WriteConcern::class); } - if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) { - unset($options['writeConcern']); + if (isset($this->options['writeConcern']) && $this->options['writeConcern']->isDefault()) { + unset($this->options['writeConcern']); } } From 0d13135a3d0692bf14f9e4eb7e0cd2541c79f56e Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 7 Apr 2025 10:42:16 -0400 Subject: [PATCH 27/38] Fix Psalm errors and update baseline --- psalm-baseline.xml | 14 ++++++++++++++ src/ClientBulkWrite.php | 5 +++++ src/Collection.php | 1 + src/Operation/ClientBulkWriteCommand.php | 6 +++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 005534e0a..a6dd2d5bf 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -235,6 +235,12 @@ + + + + + + @@ -545,6 +551,14 @@ + + + + + + executeBulkWriteCommand($this->bulkWriteCommand, $options)]]> + + diff --git a/src/ClientBulkWrite.php b/src/ClientBulkWrite.php index 058e58971..b83852320 100644 --- a/src/ClientBulkWrite.php +++ b/src/ClientBulkWrite.php @@ -17,12 +17,15 @@ namespace MongoDB; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; use MongoDB\Codec\DocumentCodec; use MongoDB\Codec\Encoder; use MongoDB\Driver\BulkWriteCommand; use MongoDB\Driver\Manager; use MongoDB\Exception\InvalidArgumentException; +use stdClass; use function is_array; use function is_bool; use function is_string; @@ -33,6 +36,7 @@ private function __construct( public BulkWriteCommand $bulkWriteCommand, private Manager $manager, private string $namespace, + /** @psalm-var Encoder */ private Encoder $builderEncoder, private ?DocumentCodec $codec, ) { @@ -108,6 +112,7 @@ public function insertOne(array|object $document, mixed &$id = null): self } // Capture the document's _id, which may have been generated, in an optional output variable + /** @var mixed */ $id = $this->bulkWriteCommand->insertOne($this->namespace, $document); return $this; diff --git a/src/Collection.php b/src/Collection.php index 52936d8eb..04a61981c 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -753,6 +753,7 @@ public function findOneAndUpdate(array|object $filter, array|object $update, arr return $operation->execute(select_server_for_write($this->manager, $options)); } + /** @psalm-return Encoder */ public function getBuilderEncoder(): Encoder { return $this->builderEncoder; diff --git a/src/Operation/ClientBulkWriteCommand.php b/src/Operation/ClientBulkWriteCommand.php index ba6632116..8a13527df 100644 --- a/src/Operation/ClientBulkWriteCommand.php +++ b/src/Operation/ClientBulkWriteCommand.php @@ -53,7 +53,11 @@ final class ClientBulkWriteCommand * @param array $options Command options * @throws InvalidArgumentException for parameter/option parsing errors */ - public function __construct(private BulkWriteCommand $bulkWriteCommand, private array $options = []) + public function __construct( + private BulkWriteCommand $bulkWriteCommand, + /** @param array{session: ?Session, writeConcern: ?WriteConcern} */ + private array $options = [], + ) { if (count($bulkWriteCommand) === 0) { throw new InvalidArgumentException('$bulkWriteCommand is empty'); From b3cd95000bc97fc486314ca9f21b574edb3a6e9a Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 7 Apr 2025 10:47:09 -0400 Subject: [PATCH 28/38] phpcs fixes --- src/ClientBulkWrite.php | 2 +- src/Operation/ClientBulkWriteCommand.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ClientBulkWrite.php b/src/ClientBulkWrite.php index b83852320..02c6c16f8 100644 --- a/src/ClientBulkWrite.php +++ b/src/ClientBulkWrite.php @@ -24,8 +24,8 @@ use MongoDB\Driver\BulkWriteCommand; use MongoDB\Driver\Manager; use MongoDB\Exception\InvalidArgumentException; - use stdClass; + use function is_array; use function is_bool; use function is_string; diff --git a/src/Operation/ClientBulkWriteCommand.php b/src/Operation/ClientBulkWriteCommand.php index 8a13527df..9bf9c6d2a 100644 --- a/src/Operation/ClientBulkWriteCommand.php +++ b/src/Operation/ClientBulkWriteCommand.php @@ -57,8 +57,7 @@ public function __construct( private BulkWriteCommand $bulkWriteCommand, /** @param array{session: ?Session, writeConcern: ?WriteConcern} */ private array $options = [], - ) - { + ) { if (count($bulkWriteCommand) === 0) { throw new InvalidArgumentException('$bulkWriteCommand is empty'); } From b448941e3667aa574064b94f4a1824c4f7865c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 9 May 2025 13:38:08 +0200 Subject: [PATCH 29/38] Test against v2.x --- .../generated/build/build-extension.yml | 20 ++++--------------- .../templates/build/build-extension.yml | 5 +---- .github/workflows/coding-standards.yml | 4 +--- .github/workflows/generator.yml | 4 +--- .github/workflows/static-analysis.yml | 4 +--- .github/workflows/tests.yml | 4 +--- 6 files changed, 9 insertions(+), 32 deletions(-) diff --git a/.evergreen/config/generated/build/build-extension.yml b/.evergreen/config/generated/build/build-extension.yml index db9b19010..04860628e 100644 --- a/.evergreen/config/generated/build/build-extension.yml +++ b/.evergreen/config/generated/build/build-extension.yml @@ -36,10 +36,7 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged - # EXTENSION_BRANCH: "v2.x" - EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" - EXTENSION_BRANCH: "2.x-bulkwrite" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.3" tags: ["build", "php8.3", "stable", "pr", "tag"] @@ -77,10 +74,7 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged - # EXTENSION_BRANCH: "v2.x" - EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" - EXTENSION_BRANCH: "2.x-bulkwrite" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.2" tags: ["build", "php8.2", "stable", "pr", "tag"] @@ -118,10 +112,7 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged - # EXTENSION_BRANCH: "v2.x" - EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" - EXTENSION_BRANCH: "2.x-bulkwrite" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.1" tags: ["build", "php8.1", "stable", "pr", "tag"] @@ -159,8 +150,5 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged - # EXTENSION_BRANCH: "v2.x" - EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" - EXTENSION_BRANCH: "2.x-bulkwrite" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" diff --git a/.evergreen/config/templates/build/build-extension.yml b/.evergreen/config/templates/build/build-extension.yml index 4f76c7889..08d4ecc2a 100644 --- a/.evergreen/config/templates/build/build-extension.yml +++ b/.evergreen/config/templates/build/build-extension.yml @@ -34,8 +34,5 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - # TODO: replace with "v2.x" once mongodb/mongo-php-driver#1790 is merged - # EXTENSION_BRANCH: "v2.x" - EXTENSION_REPO: "https://github.com/jmikola/mongo-php-driver.git" - EXTENSION_BRANCH: "2.x-bulkwrite" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index a64982b99..3810a5ded 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -15,9 +15,7 @@ env: PHP_VERSION: "8.2" # TODO: change to "stable" once 2.0.0 is released # DRIVER_VERSION: "stable" - # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged - # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" - DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" + DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" jobs: phpcs: diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index a780bce76..008f8d84b 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -15,9 +15,7 @@ env: PHP_VERSION: "8.2" # TODO: change to "stable" once 2.0.0 is released # DRIVER_VERSION: "stable" - # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged - # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" - DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" + DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" jobs: diff: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index e145fe802..cf96ca895 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -21,9 +21,7 @@ env: PHP_VERSION: "8.2" # TODO: change to "stable" once 2.0.0 is released # DRIVER_VERSION: "stable" - # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged - # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" - DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" + DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" jobs: psalm: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e4880354e..9281d8516 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,9 +14,7 @@ on: env: # TODO: change to "stable" once 2.0.0 is released # DRIVER_VERSION: "stable" - # TODO: change to "mongodb/mongo-php-driver@v2.x" once mongodb/mongo-php-driver#1790 is merged - # DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" - DRIVER_VERSION: "jmikola/mongo-php-driver@2.x-bulkwrite" + DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" jobs: phpunit: From ff9e162f13822adb3d889723906c2d08de27a7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 9 May 2025 13:42:58 +0200 Subject: [PATCH 30/38] Fix CS --- src/ClientBulkWrite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClientBulkWrite.php b/src/ClientBulkWrite.php index 02c6c16f8..e3b7592ed 100644 --- a/src/ClientBulkWrite.php +++ b/src/ClientBulkWrite.php @@ -112,7 +112,7 @@ public function insertOne(array|object $document, mixed &$id = null): self } // Capture the document's _id, which may have been generated, in an optional output variable - /** @var mixed */ + /** @var mixed $id */ $id = $this->bulkWriteCommand->insertOne($this->namespace, $document); return $this; From 1befabe521bc1990c3c02fbed544429839089c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 9 May 2025 13:48:24 +0200 Subject: [PATCH 31/38] Remove readonly qualifier on the class, PHP 8.2 would be required --- src/ClientBulkWrite.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ClientBulkWrite.php b/src/ClientBulkWrite.php index e3b7592ed..7942da9ae 100644 --- a/src/ClientBulkWrite.php +++ b/src/ClientBulkWrite.php @@ -30,15 +30,15 @@ use function is_bool; use function is_string; -final readonly class ClientBulkWrite +final class ClientBulkWrite { private function __construct( - public BulkWriteCommand $bulkWriteCommand, - private Manager $manager, - private string $namespace, + public readonly BulkWriteCommand $bulkWriteCommand, + private readonly Manager $manager, + private readonly string $namespace, /** @psalm-var Encoder */ - private Encoder $builderEncoder, - private ?DocumentCodec $codec, + private readonly Encoder $builderEncoder, + private readonly ?DocumentCodec $codec, ) { } From 66ed280ca9d78b26bb94499ce9535b159b5e5c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 9 May 2025 13:52:12 +0200 Subject: [PATCH 32/38] Add NoDiscard attribute to 'withCollection' methods --- composer.json | 3 ++- src/ClientBulkWrite.php | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7df73fbed..632fda577 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "php": "^8.1", "ext-mongodb": "^2.0", "composer-runtime-api": "^2.0", - "psr/log": "^1.1.4|^2|^3" + "psr/log": "^1.1.4|^2|^3", + "symfony/polyfill-php85": "^1.32" }, "require-dev": { "doctrine/coding-standard": "^12.0", diff --git a/src/ClientBulkWrite.php b/src/ClientBulkWrite.php index 7942da9ae..f11452d3a 100644 --- a/src/ClientBulkWrite.php +++ b/src/ClientBulkWrite.php @@ -24,6 +24,7 @@ use MongoDB\Driver\BulkWriteCommand; use MongoDB\Driver\Manager; use MongoDB\Exception\InvalidArgumentException; +use NoDiscard; use stdClass; use function is_array; @@ -42,6 +43,7 @@ private function __construct( ) { } + #[NoDiscard] public static function createWithCollection(Collection $collection, array $options = []): self { $options += ['ordered' => true]; @@ -224,6 +226,7 @@ public function updateOne(array|object $filter, array|object $update, array $opt return $this; } + #[NoDiscard] public function withCollection(Collection $collection): self { /* Prohibit mixing Collections associated with different Manager From 97a5121a7639b46593a8c2d062684661554fde84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 9 May 2025 14:33:25 +0200 Subject: [PATCH 33/38] Switch all EXTENSION_BRANCH versions --- .evergreen/config/generated/build/build-extension.yml | 8 ++++---- .evergreen/config/templates/build/build-extension.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.evergreen/config/generated/build/build-extension.yml b/.evergreen/config/generated/build/build-extension.yml index 04860628e..8cc0e8e68 100644 --- a/.evergreen/config/generated/build/build-extension.yml +++ b/.evergreen/config/generated/build/build-extension.yml @@ -26,7 +26,7 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.4-next-minor" tags: ["build", "php8.4", "next-minor"] @@ -64,7 +64,7 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.3-next-minor" tags: ["build", "php8.3", "next-minor"] @@ -102,7 +102,7 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.2-next-minor" tags: ["build", "php8.2", "next-minor"] @@ -140,7 +140,7 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.1-next-minor" tags: ["build", "php8.1", "next-minor"] diff --git a/.evergreen/config/templates/build/build-extension.yml b/.evergreen/config/templates/build/build-extension.yml index 08d4ecc2a..de55e9b2e 100644 --- a/.evergreen/config/templates/build/build-extension.yml +++ b/.evergreen/config/templates/build/build-extension.yml @@ -24,7 +24,7 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-%phpVersion%-next-minor" tags: ["build", "php%phpVersion%", "next-minor"] From 3029042224e11077cca07f1c75616b560b663672 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Mon, 12 May 2025 09:11:49 +0200 Subject: [PATCH 34/38] Always build against v2.x --- .../generated/build/build-extension.yml | 36 ++++++++++++++++--- .../templates/build/build-extension.yml | 9 ++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.evergreen/config/generated/build/build-extension.yml b/.evergreen/config/generated/build/build-extension.yml index 8cc0e8e68..48c7c26a3 100644 --- a/.evergreen/config/generated/build/build-extension.yml +++ b/.evergreen/config/generated/build/build-extension.yml @@ -7,6 +7,9 @@ tasks: vars: PHP_VERSION: "8.4" - func: "compile extension" + # TODO: Remove vars to switch to latest stable version when 2.1.0 is releeased + vars: + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.4-lowest" tags: ["build", "php8.4", "lowest", "pr", "tag"] @@ -16,7 +19,9 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - EXTENSION_VERSION: "2.0.0" + # TODO: Switch to 2.1.0 when it is released + # EXTENSION_VERSION: "2.0.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.4-next-stable" tags: ["build", "php8.4", "next-stable", "pr", "tag"] @@ -26,6 +31,8 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: + # TODO: Switch to v2.1 when 2.1.0 is released + # EXTENSION_VERSION: "v2.1" EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.4-next-minor" @@ -45,6 +52,9 @@ tasks: vars: PHP_VERSION: "8.3" - func: "compile extension" + # TODO: Remove vars to switch to latest stable version when 2.1.0 is releeased + vars: + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.3-lowest" tags: ["build", "php8.3", "lowest", "pr", "tag"] @@ -54,7 +64,9 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - EXTENSION_VERSION: "2.0.0" + # TODO: Switch to 2.1.0 when it is released + # EXTENSION_VERSION: "2.0.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.3-next-stable" tags: ["build", "php8.3", "next-stable", "pr", "tag"] @@ -64,6 +76,8 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: + # TODO: Switch to v2.1 when 2.1.0 is released + # EXTENSION_VERSION: "v2.1" EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.3-next-minor" @@ -83,6 +97,9 @@ tasks: vars: PHP_VERSION: "8.2" - func: "compile extension" + # TODO: Remove vars to switch to latest stable version when 2.1.0 is releeased + vars: + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.2-lowest" tags: ["build", "php8.2", "lowest", "pr", "tag"] @@ -92,7 +109,9 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - EXTENSION_VERSION: "2.0.0" + # TODO: Switch to 2.1.0 when it is released + # EXTENSION_VERSION: "2.0.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.2-next-stable" tags: ["build", "php8.2", "next-stable", "pr", "tag"] @@ -102,6 +121,8 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: + # TODO: Switch to v2.1 when 2.1.0 is released + # EXTENSION_VERSION: "v2.1" EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.2-next-minor" @@ -121,6 +142,9 @@ tasks: vars: PHP_VERSION: "8.1" - func: "compile extension" + # TODO: Remove vars to switch to latest stable version when 2.1.0 is releeased + vars: + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.1-lowest" tags: ["build", "php8.1", "lowest", "pr", "tag"] @@ -130,7 +154,9 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - EXTENSION_VERSION: "2.0.0" + # TODO: Switch to 2.1.0 when it is released + # EXTENSION_VERSION: "2.0.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.1-next-stable" tags: ["build", "php8.1", "next-stable", "pr", "tag"] @@ -140,6 +166,8 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: + # TODO: Switch to v2.1 when 2.1.0 is released + # EXTENSION_VERSION: "v2.1" EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.1-next-minor" diff --git a/.evergreen/config/templates/build/build-extension.yml b/.evergreen/config/templates/build/build-extension.yml index de55e9b2e..c6b1f411c 100644 --- a/.evergreen/config/templates/build/build-extension.yml +++ b/.evergreen/config/templates/build/build-extension.yml @@ -5,6 +5,9 @@ vars: PHP_VERSION: "%phpVersion%" - func: "compile extension" + # TODO: Remove vars to switch to latest stable version when 2.1.0 is releeased + vars: + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-%phpVersion%-lowest" tags: ["build", "php%phpVersion%", "lowest", "pr", "tag"] @@ -14,7 +17,9 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - EXTENSION_VERSION: "2.0.0" + # TODO: Switch to 2.1.0 when it is released + # EXTENSION_VERSION: "2.0.0" + EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-%phpVersion%-next-stable" tags: ["build", "php%phpVersion%", "next-stable", "pr", "tag"] @@ -24,6 +29,8 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: + # TODO: Switch to v2.1 when 2.1.0 is released + # EXTENSION_VERSION: "v2.1" EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-%phpVersion%-next-minor" From f207cdb19d2c04ad09cdde573154461f80f1f49d Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Mon, 12 May 2025 10:13:55 +0200 Subject: [PATCH 35/38] Append CRYPT_SHARED_LIB_PATH to autoEncryptionOpts in tests --- tests/FunctionalTestCase.php | 10 +++++++--- .../ClientSideEncryption/FunctionalTestCase.php | 9 --------- tests/SpecTests/ClientSideEncryptionSpecTest.php | 9 --------- tests/SpecTests/Context.php | 5 ----- 4 files changed, 7 insertions(+), 26 deletions(-) diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php index ecd6d2372..05c9c1765 100644 --- a/tests/FunctionalTestCase.php +++ b/tests/FunctionalTestCase.php @@ -85,7 +85,7 @@ public static function createTestClient(?string $uri = null, array $options = [] return new Client( $uri ?? static::getUri(), static::appendAuthenticationOptions($options), - static::appendServerApiOption($driverOptions), + static::appendDriverOptions($driverOptions), ); } @@ -94,7 +94,7 @@ public static function createTestManager(?string $uri = null, array $options = [ return new Manager( $uri ?? static::getUri(), static::appendAuthenticationOptions($options), - static::appendServerApiOption($driverOptions), + static::appendDriverOptions($driverOptions), ); } @@ -603,8 +603,12 @@ private static function appendAuthenticationOptions(array $options): array return $options; } - private static function appendServerApiOption(array $driverOptions): array + private static function appendDriverOptions(array $driverOptions): array { + if (isset($driverOptions['autoEncryption']) && getenv('CRYPT_SHARED_LIB_PATH')) { + $driverOptions['autoEncryption']['extraOptions']['cryptSharedLibPath'] = getenv('CRYPT_SHARED_LIB_PATH'); + } + if (getenv('API_VERSION') && ! isset($driverOptions['serverApi'])) { $driverOptions['serverApi'] = new ServerApi(getenv('API_VERSION')); } diff --git a/tests/SpecTests/ClientSideEncryption/FunctionalTestCase.php b/tests/SpecTests/ClientSideEncryption/FunctionalTestCase.php index 71648a853..58c2aa24d 100644 --- a/tests/SpecTests/ClientSideEncryption/FunctionalTestCase.php +++ b/tests/SpecTests/ClientSideEncryption/FunctionalTestCase.php @@ -27,15 +27,6 @@ public function setUp(): void $this->skipIfClientSideEncryptionIsNotSupported(); } - public static function createTestClient(?string $uri = null, array $options = [], array $driverOptions = []): Client - { - if (isset($driverOptions['autoEncryption']) && getenv('CRYPT_SHARED_LIB_PATH')) { - $driverOptions['autoEncryption']['extraOptions']['cryptSharedLibPath'] = getenv('CRYPT_SHARED_LIB_PATH'); - } - - return parent::createTestClient($uri, $options, $driverOptions); - } - protected static function getAWSCredentials(): array { return [ diff --git a/tests/SpecTests/ClientSideEncryptionSpecTest.php b/tests/SpecTests/ClientSideEncryptionSpecTest.php index b4d2986e2..e2ad52563 100644 --- a/tests/SpecTests/ClientSideEncryptionSpecTest.php +++ b/tests/SpecTests/ClientSideEncryptionSpecTest.php @@ -143,15 +143,6 @@ public static function assertCommandMatches(stdClass $expected, stdClass $actual static::assertDocumentsMatch($expected, $actual); } - public static function createTestClient(?string $uri = null, array $options = [], array $driverOptions = []): Client - { - if (isset($driverOptions['autoEncryption']) && getenv('CRYPT_SHARED_LIB_PATH')) { - $driverOptions['autoEncryption']['extraOptions']['cryptSharedLibPath'] = getenv('CRYPT_SHARED_LIB_PATH'); - } - - return parent::createTestClient($uri, $options, $driverOptions); - } - /** * Execute an individual test case from the specification. * diff --git a/tests/SpecTests/Context.php b/tests/SpecTests/Context.php index c3704900f..4d2db90cb 100644 --- a/tests/SpecTests/Context.php +++ b/tests/SpecTests/Context.php @@ -105,11 +105,6 @@ public static function fromClientSideEncryption(stdClass $test, $databaseName, $ $autoEncryptionOptions['tlsOptions']->kmip = self::getKmsTlsOptions(); } - - // Intentionally ignore empty values for CRYPT_SHARED_LIB_PATH - if (getenv('CRYPT_SHARED_LIB_PATH')) { - $autoEncryptionOptions['extraOptions']['cryptSharedLibPath'] = getenv('CRYPT_SHARED_LIB_PATH'); - } } if (isset($test->outcome->collection->name)) { From bb34d7fc2a607a36e89cdd4f0fa05b4885ce4c1b Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Mon, 12 May 2025 13:34:25 +0200 Subject: [PATCH 36/38] PHPLIB-1677: Assert unset BulkWriteException.partialResult in CRUD prose tests --- ..._BulkWriteExceedsMaxMessageSizeBytesTest.php | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php b/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php index 854c101d0..7fa0c6f30 100644 --- a/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php +++ b/tests/SpecTests/Crud/Prose12_BulkWriteExceedsMaxMessageSizeBytesTest.php @@ -3,7 +3,6 @@ namespace MongoDB\Tests\SpecTests\Crud; use MongoDB\ClientBulkWrite; -use MongoDB\Driver\Exception\BulkWriteCommandException; use MongoDB\Driver\Exception\InvalidArgumentException; use MongoDB\Tests\SpecTests\FunctionalTestCase; @@ -41,12 +40,8 @@ public function testDocumentTooLarge(): void try { $client->bulkWrite($bulkWrite); self::fail('Exception was not thrown'); - } catch (BulkWriteCommandException $e) { - /* Note: although the client-side error occurs on the first operation, libmongoc still populates the partial - * result (see: CDRIVER-5969). This causes PHPC to proxy the underlying InvalidArgumentException behind - * BulkWriteCommandException. Until this is addressed, unwrap the error and check the partial result. */ - self::assertInstanceOf(InvalidArgumentException::class, $e->getPrevious()); - self::assertSame(0, $e->getPartialResult()->getInsertedCount()); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('unable to send document', $e->getMessage()); } } @@ -65,12 +60,8 @@ public function testNamespaceTooLarge(): void try { $client->bulkWrite($bulkWrite); self::fail('Exception was not thrown'); - } catch (BulkWriteCommandException $e) { - /* Note: although the client-side error occurs on the first operation, libmongoc still populates the partial - * result (see: CDRIVER-5969). This causes PHPC to proxy the underlying InvalidArgumentException behind - * BulkWriteCommandException. Until this is addressed, unwrap the error and check the partial result. */ - self::assertInstanceOf(InvalidArgumentException::class, $e->getPrevious()); - self::assertSame(0, $e->getPartialResult()->getInsertedCount()); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('unable to send document', $e->getMessage()); } } } From 72f5b6ee68f3b7ce0f054c4f66e1262694802e0f Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Mon, 12 May 2025 14:09:11 +0200 Subject: [PATCH 37/38] Use explicit null check Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Operation/ClientBulkWriteCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Operation/ClientBulkWriteCommand.php b/src/Operation/ClientBulkWriteCommand.php index 9bf9c6d2a..e7768adcb 100644 --- a/src/Operation/ClientBulkWriteCommand.php +++ b/src/Operation/ClientBulkWriteCommand.php @@ -88,7 +88,7 @@ public function execute(Server $server): BulkWriteCommandResult throw UnsupportedException::writeConcernNotSupportedInTransaction(); } - $options = array_filter($this->options, fn ($value) => isset($value)); + $options = array_filter($this->options, fn ($value) => $value !== null); return $server->executeBulkWriteCommand($this->bulkWriteCommand, $options); } From 2d978467b49f94c1f3d27186f5d2c27891f421f1 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Mon, 12 May 2025 14:45:29 +0200 Subject: [PATCH 38/38] Require ext-mongodb 2.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 632fda577..6ba77e202 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "^8.1", - "ext-mongodb": "^2.0", + "ext-mongodb": "^2.1", "composer-runtime-api": "^2.0", "psr/log": "^1.1.4|^2|^3", "symfony/polyfill-php85": "^1.32"