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());
+ }
}