diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a6d55420..b9f153751 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -471,7 +471,7 @@ $this->options['writeConcern'] - $cmd['writeConcern'] + $cmd['comment'] $deleteOptions['hint'] $options['comment'] $options['session'] diff --git a/src/Operation/Delete.php b/src/Operation/Delete.php index 48fe99470..5f3318012 100644 --- a/src/Operation/Delete.php +++ b/src/Operation/Delete.php @@ -177,7 +177,17 @@ public function execute(Server $server) */ public function getCommandDocument() { - return ['delete' => $this->collectionName, 'deletes' => [['q' => $this->filter] + $this->createDeleteOptions()]]; + $cmd = ['delete' => $this->collectionName, 'deletes' => [['q' => $this->filter] + $this->createDeleteOptions()]]; + + if (isset($this->options['comment'])) { + $cmd['comment'] = $this->options['comment']; + } + + if (isset($this->options['let'])) { + $cmd['let'] = (object) $this->options['let']; + } + + return $cmd; } /** diff --git a/src/Operation/Find.php b/src/Operation/Find.php index 01929c60e..8725d37e8 100644 --- a/src/Operation/Find.php +++ b/src/Operation/Find.php @@ -329,21 +329,6 @@ public function execute(Server $server) * @return array */ public function getCommandDocument() - { - $cmd = $this->createCommandDocument(); - - // Read concern can change the query plan - if (isset($this->options['readConcern'])) { - $cmd['readConcern'] = $this->options['readConcern']; - } - - return $cmd; - } - - /** - * Construct a command document for Find - */ - private function createCommandDocument(): array { $cmd = ['find' => $this->collectionName, 'filter' => (object) $this->filter]; diff --git a/tests/Operation/AggregateTest.php b/tests/Operation/AggregateTest.php index b046e8583..f72fc5a22 100644 --- a/tests/Operation/AggregateTest.php +++ b/tests/Operation/AggregateTest.php @@ -2,6 +2,9 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\ReadConcern; +use MongoDB\Driver\ReadPreference; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\Aggregate; @@ -104,4 +107,41 @@ private function getInvalidHintValues() { return [123, 3.14, true]; } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'allowDiskUse' => true, + 'batchSize' => 100, + 'bypassDocumentValidation' => true, + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'hint' => '_id_', + 'let' => ['a' => 1], + 'maxTimeMS' => 100, + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + 'useCursor' => true, + // Intentionally omitted options + // The "explain" option is illegal + 'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED), + 'typeMap' => ['root' => 'array', 'document' => 'array'], + 'writeConcern' => new WriteConcern(0), + ]; + $operation = new Aggregate($this->getDatabaseName(), $this->getCollectionName(), [['$project' => ['_id' => 0]]], $options); + + $expected = [ + 'aggregate' => $this->getCollectionName(), + 'pipeline' => [['$project' => ['_id' => 0]]], + 'allowDiskUse' => true, + 'bypassDocumentValidation' => true, + 'collation' => (object) ['locale' => 'fr'], + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + 'let' => (object) ['a' => 1], + 'cursor' => ['batchSize' => 100], + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/CountTest.php b/tests/Operation/CountTest.php index 23969595c..5259a9d44 100644 --- a/tests/Operation/CountTest.php +++ b/tests/Operation/CountTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\ReadConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\Count; @@ -64,4 +65,31 @@ private function getInvalidHintValues() { return [123, 3.14, true]; } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'hint' => '_id_', + 'limit' => 10, + 'skip' => 20, + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'maxTimeMS' => 100, + ]; + $operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], $options); + + $expected = [ + 'count' => $this->getCollectionName(), + 'query' => (object) ['x' => 1], + 'collation' => (object) ['locale' => 'fr'], + 'hint' => '_id_', + 'comment' => 'explain me', + 'limit' => 10, + 'skip' => 20, + 'maxTimeMS' => 100, + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/DeleteTest.php b/tests/Operation/DeleteTest.php index acdbefc31..90b9cae52 100644 --- a/tests/Operation/DeleteTest.php +++ b/tests/Operation/DeleteTest.php @@ -7,6 +7,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\Delete; use TypeError; @@ -65,4 +66,32 @@ public function provideInvalidConstructorOptions() return $options; } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'collation' => ['locale' => 'fr'], + 'hint' => '_id_', + 'let' => ['a' => 1], + 'comment' => 'explain me', + // Intentionally omitted options + 'writeConcern' => new WriteConcern(0), + ]; + $operation = new Delete($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], 0, $options); + + $expected = [ + 'delete' => $this->getCollectionName(), + 'deletes' => [ + [ + 'q' => ['x' => 1], + 'limit' => 0, + 'collation' => (object) ['locale' => 'fr'], + 'hint' => '_id_', + ], + ], + 'comment' => 'explain me', + 'let' => (object) ['a' => 1], + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/DistinctTest.php b/tests/Operation/DistinctTest.php index 8473e833d..9ece08fa5 100644 --- a/tests/Operation/DistinctTest.php +++ b/tests/Operation/DistinctTest.php @@ -2,6 +2,8 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\ReadConcern; +use MongoDB\Driver\ReadPreference; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\Distinct; @@ -51,4 +53,29 @@ public function provideInvalidConstructorOptions() return $options; } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'collation' => ['locale' => 'fr'], + 'maxTimeMS' => 100, + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + 'comment' => 'explain me', + // Intentionally omitted options + 'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED), + 'typeMap' => ['root' => 'array'], + ]; + $operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'f', ['x' => 1], $options); + + $expected = [ + 'distinct' => $this->getCollectionName(), + 'key' => 'f', + 'query' => (object) ['x' => 1], + 'collation' => (object) ['locale' => 'fr'], + 'comment' => 'explain me', + 'maxTimeMS' => 100, + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/FindAndModifyTest.php b/tests/Operation/FindAndModifyTest.php index 3904385e9..a0738bb12 100644 --- a/tests/Operation/FindAndModifyTest.php +++ b/tests/Operation/FindAndModifyTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\FindAndModify; @@ -83,4 +84,46 @@ public function testConstructorUpdateAndRemoveOptionsAreMutuallyExclusive(): voi $this->expectExceptionMessage('The "remove" option must be true or an "update" document must be specified, but not both'); new FindAndModify($this->getDatabaseName(), $this->getCollectionName(), ['remove' => true, 'update' => []]); } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'arrayFilters' => [['x' => 1]], + 'bypassDocumentValidation' => true, + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'fields' => ['_id' => 0], + 'hint' => '_id_', + 'maxTimeMS' => 100, + 'new' => true, + 'query' => ['y' => 2], + 'sort' => ['x' => 1], + 'update' => ['$set' => ['x' => 2]], + 'upsert' => true, + 'let' => ['a' => 3], + // Intentionally omitted options + 'remove' => false, // When "update" is set + 'typeMap' => ['root' => 'array'], + 'writeConcern' => new WriteConcern(0), + ]; + $operation = new FindAndModify($this->getDatabaseName(), $this->getCollectionName(), $options); + + $expected = [ + 'findAndModify' => $this->getCollectionName(), + 'new' => true, + 'upsert' => true, + 'collation' => (object) ['locale' => 'fr'], + 'fields' => (object) ['_id' => 0], + 'let' => (object) ['a' => 3], + 'query' => (object) ['y' => 2], + 'sort' => (object) ['x' => 1], + 'update' => (object) ['$set' => ['x' => 2]], + 'arrayFilters' => [['x' => 1]], + 'bypassDocumentValidation' => true, + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/FindOneAndDeleteTest.php b/tests/Operation/FindOneAndDeleteTest.php index 42055ba27..e63062ace 100644 --- a/tests/Operation/FindOneAndDeleteTest.php +++ b/tests/Operation/FindOneAndDeleteTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\FindOneAndDelete; @@ -31,4 +32,35 @@ public function provideInvalidConstructorOptions() return $options; } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + 'sort' => ['x' => 1], + 'let' => ['a' => 3], + // Intentionally omitted options + 'projection' => ['_id' => 0], + 'typeMap' => ['root' => 'array'], + 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY), + ]; + $operation = new FindOneAndDelete($this->getDatabaseName(), $this->getCollectionName(), ['y' => 2], $options); + + $expected = [ + 'findAndModify' => $this->getCollectionName(), + 'collation' => (object) ['locale' => 'fr'], + 'fields' => (object) ['_id' => 0], + 'let' => (object) ['a' => 3], + 'query' => (object) ['y' => 2], + 'sort' => (object) ['x' => 1], + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + 'remove' => true, + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/FindOneAndReplaceTest.php b/tests/Operation/FindOneAndReplaceTest.php index b084036f4..743098e4f 100644 --- a/tests/Operation/FindOneAndReplaceTest.php +++ b/tests/Operation/FindOneAndReplaceTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\FindOneAndReplace; @@ -82,4 +83,40 @@ public function provideInvalidConstructorReturnDocumentOptions() { return $this->wrapValuesForDataProvider([-1, 0, 3]); } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'bypassDocumentValidation' => true, + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'fields' => ['_id' => 0], + 'hint' => '_id_', + 'maxTimeMS' => 100, + 'projection' => ['_id' => 0], + 'sort' => ['x' => 1], + 'let' => ['a' => 3], + // Intentionally omitted options + 'returnDocument' => FindOneAndReplace::RETURN_DOCUMENT_AFTER, + 'typeMap' => ['root' => 'array'], + 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY), + ]; + $operation = new FindOneAndReplace($this->getDatabaseName(), $this->getCollectionName(), ['y' => 2], ['y' => 3], $options); + + $expected = [ + 'findAndModify' => $this->getCollectionName(), + 'new' => true, + 'collation' => (object) ['locale' => 'fr'], + 'fields' => (object) ['_id' => 0], + 'let' => (object) ['a' => 3], + 'query' => (object) ['y' => 2], + 'sort' => (object) ['x' => 1], + 'update' => (object) ['y' => 3], + 'bypassDocumentValidation' => true, + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/FindOneAndUpdateTest.php b/tests/Operation/FindOneAndUpdateTest.php index 8514cea86..d174b73f3 100644 --- a/tests/Operation/FindOneAndUpdateTest.php +++ b/tests/Operation/FindOneAndUpdateTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\FindOneAndUpdate; @@ -65,4 +66,43 @@ public function provideInvalidConstructorReturnDocumentOptions() { return $this->wrapValuesForDataProvider([-1, 0, 3]); } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'arrayFilters' => [['x' => 1]], + 'bypassDocumentValidation' => true, + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + 'sort' => ['x' => 1], + 'upsert' => true, + 'let' => ['a' => 3], + // Intentionally omitted options + 'projection' => ['_id' => 0], + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, + 'typeMap' => ['root' => 'array'], + 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY), + ]; + $operation = new FindOneAndUpdate($this->getDatabaseName(), $this->getCollectionName(), ['y' => 2], ['$set' => ['x' => 2]], $options); + + $expected = [ + 'findAndModify' => $this->getCollectionName(), + 'new' => true, + 'upsert' => true, + 'collation' => (object) ['locale' => 'fr'], + 'fields' => (object) ['_id' => 0], + 'let' => (object) ['a' => 3], + 'query' => (object) ['y' => 2], + 'sort' => (object) ['x' => 1], + 'update' => (object) ['$set' => ['x' => 2]], + 'arrayFilters' => [['x' => 1]], + 'bypassDocumentValidation' => true, + 'comment' => 'explain me', + 'hint' => '_id_', + 'maxTimeMS' => 100, + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/FindTest.php b/tests/Operation/FindTest.php index f5889f260..1da7d23fa 100644 --- a/tests/Operation/FindTest.php +++ b/tests/Operation/FindTest.php @@ -2,6 +2,8 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\ReadConcern; +use MongoDB\Driver\ReadPreference; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\Find; @@ -154,4 +156,62 @@ public function provideInvalidConstructorCursorTypeOptions() { return $this->wrapValuesForDataProvider([-1, 0, 4]); } + + public function testExplainableCommandDocument(): void + { + // all options except deprecated "snapshot" and "maxScan" + $options = [ + 'allowDiskUse' => true, + 'allowPartialResults' => true, + 'batchSize' => 123, + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'hint' => '_id_', + 'limit' => 15, + 'max' => ['x' => 100], + 'maxTimeMS' => 100, + 'min' => ['x' => 10], + 'noCursorTimeout' => true, + 'oplogReplay' => true, + 'projection' => ['_id' => 0], + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + 'returnKey' => true, + 'showRecordId' => true, + 'skip' => 5, + 'sort' => ['x' => 1], + 'let' => ['y' => 2], + // Intentionally omitted options + 'cursorType' => Find::NON_TAILABLE, + 'maxAwaitTimeMS' => 500, + 'modifiers' => ['foo' => 'bar'], + 'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED), + 'typeMap' => ['root' => 'array'], + ]; + $operation = new Find($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], $options); + + $expected = [ + 'find' => $this->getCollectionName(), + 'filter' => (object) ['x' => 1], + 'allowDiskUse' => true, + 'allowPartialResults' => true, + 'batchSize' => 123, + 'comment' => 'explain me', + 'hint' => '_id_', + 'limit' => 15, + 'maxTimeMS' => 100, + 'noCursorTimeout' => true, + 'oplogReplay' => true, + 'projection' => ['_id' => 0], + 'readConcern' => new ReadConcern(ReadConcern::LOCAL), + 'returnKey' => true, + 'showRecordId' => true, + 'skip' => 5, + 'sort' => ['x' => 1], + 'collation' => (object) ['locale' => 'fr'], + 'let' => (object) ['y' => 2], + 'max' => (object) ['x' => 100], + 'min' => (object) ['x' => 10], + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } } diff --git a/tests/Operation/UpdateTest.php b/tests/Operation/UpdateTest.php index bcc445544..de9fcbadd 100644 --- a/tests/Operation/UpdateTest.php +++ b/tests/Operation/UpdateTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Operation; +use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Operation\Update; @@ -75,4 +76,37 @@ public function testConstructorMultiOptionProhibitsReplacementDocumentOrEmptyPip $this->expectExceptionMessage('"multi" option cannot be true unless $update has update operator(s) or non-empty pipeline'); new Update($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], $update, ['multi' => true]); } + + public function testExplainableCommandDocument(): void + { + $options = [ + 'arrayFilters' => [['x' => 1]], + 'bypassDocumentValidation' => true, + 'collation' => ['locale' => 'fr'], + 'comment' => 'explain me', + 'hint' => '_id_', + 'multi' => true, + 'upsert' => true, + 'let' => ['a' => 3], + 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY), + ]; + $operation = new Update($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], ['$set' => ['x' => 2]], $options); + + $expected = [ + 'update' => $this->getCollectionName(), + 'bypassDocumentValidation' => true, + 'updates' => [ + [ + 'q' => ['x' => 1], + 'u' => ['$set' => ['x' => 2]], + 'multi' => true, + 'upsert' => true, + 'arrayFilters' => [['x' => 1]], + 'hint' => '_id_', + 'collation' => (object) ['locale' => 'fr'], + ], + ], + ]; + $this->assertEquals($expected, $operation->getCommandDocument()); + } }