Skip to content

Commit fc71a5c

Browse files
committed
PHPLIB-130: Support readConcern option on read operations
1 parent 523ca61 commit fc71a5c

File tree

13 files changed

+176
-8
lines changed

13 files changed

+176
-8
lines changed

src/Collection.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,20 @@ public function __toString()
143143
*/
144144
public function aggregate(array $pipeline, array $options = [])
145145
{
146+
$hasOutStage = \MongoDB\is_last_pipeline_operator_out($pipeline);
147+
148+
/* A "majority" read concern is not compatible with the $out stage, so
149+
* avoid providing the Collection's read concern if it would conflict.
150+
*/
151+
if ( ! isset($options['readConcern']) && ! ($hasOutStage && $this->readConcern->getLevel() === ReadConcern::MAJORITY)) {
152+
$options['readConcern'] = $this->readConcern;
153+
}
154+
146155
if ( ! isset($options['readPreference'])) {
147156
$options['readPreference'] = $this->readPreference;
148157
}
149158

150-
if (\MongoDB\is_last_pipeline_operator_out($pipeline)) {
159+
if ($hasOutStage) {
151160
$options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
152161
}
153162

@@ -187,6 +196,10 @@ public function bulkWrite(array $operations, array $options = [])
187196
*/
188197
public function count($filter = [], array $options = [])
189198
{
199+
if ( ! isset($options['readConcern'])) {
200+
$options['readConcern'] = $this->readConcern;
201+
}
202+
190203
if ( ! isset($options['readPreference'])) {
191204
$options['readPreference'] = $this->readPreference;
192205
}
@@ -295,6 +308,10 @@ public function deleteOne($filter, array $options = [])
295308
*/
296309
public function distinct($fieldName, $filter = [], array $options = [])
297310
{
311+
if ( ! isset($options['readConcern'])) {
312+
$options['readConcern'] = $this->readConcern;
313+
}
314+
298315
if ( ! isset($options['readPreference'])) {
299316
$options['readPreference'] = $this->readPreference;
300317
}
@@ -363,6 +380,10 @@ public function dropIndexes()
363380
*/
364381
public function find($filter = [], array $options = [])
365382
{
383+
if ( ! isset($options['readConcern'])) {
384+
$options['readConcern'] = $this->readConcern;
385+
}
386+
366387
if ( ! isset($options['readPreference'])) {
367388
$options['readPreference'] = $this->readPreference;
368389
}
@@ -384,6 +405,10 @@ public function find($filter = [], array $options = [])
384405
*/
385406
public function findOne($filter = [], array $options = [])
386407
{
408+
if ( ! isset($options['readConcern'])) {
409+
$options['readConcern'] = $this->readConcern;
410+
}
411+
387412
if ( ! isset($options['readPreference'])) {
388413
$options['readPreference'] = $this->readPreference;
389414
}

src/Operation/Aggregate.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Operation;
44

55
use MongoDB\Driver\Command;
6+
use MongoDB\Driver\ReadConcern;
67
use MongoDB\Driver\ReadPreference;
78
use MongoDB\Driver\Server;
89
use MongoDB\Exception\InvalidArgumentException;
@@ -23,6 +24,7 @@ class Aggregate implements Executable
2324
{
2425
private static $wireVersionForCursor = 2;
2526
private static $wireVersionForDocumentLevelValidation = 4;
27+
private static $wireVersionForReadConcern = 4;
2628

2729
private $databaseName;
2830
private $collectionName;
@@ -50,6 +52,12 @@ class Aggregate implements Executable
5052
* * maxTimeMS (integer): The maximum amount of time to allow the query to
5153
* run.
5254
*
55+
* * readConcern (MongoDB\Driver\ReadConcern): Read concern. Note that a
56+
* "majority" read concern is not compatible with the $out stage.
57+
*
58+
* For servers < 3.2, this option is ignored as read concern is not
59+
* available.
60+
*
5361
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
5462
*
5563
* * useCursor (boolean): Indicates whether the command will request that
@@ -108,6 +116,10 @@ public function __construct($databaseName, $collectionName, array $pipeline, arr
108116
throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
109117
}
110118

119+
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
120+
throw new InvalidArgumentTypeException('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
121+
}
122+
111123
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
112124
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
113125
}
@@ -183,6 +195,10 @@ private function createCommand(Server $server, $isCursorSupported)
183195
$cmd['maxTimeMS'] = $this->options['maxTimeMS'];
184196
}
185197

198+
if (isset($this->options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
199+
$cmd['readConcern'] = \MongoDB\read_concern_as_document($this->options['readConcern']);
200+
}
201+
186202
if ($this->options['useCursor']) {
187203
$cmd['cursor'] = isset($this->options["batchSize"])
188204
? ['batchSize' => $this->options["batchSize"]]

src/Operation/Count.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Operation;
44

55
use MongoDB\Driver\Command;
6+
use MongoDB\Driver\ReadConcern;
67
use MongoDB\Driver\ReadPreference;
78
use MongoDB\Driver\Server;
89
use MongoDB\Exception\InvalidArgumentException;
@@ -18,6 +19,8 @@
1819
*/
1920
class Count implements Executable
2021
{
22+
private static $wireVersionForReadConcern = 4;
23+
2124
private $databaseName;
2225
private $collectionName;
2326
private $filter;
@@ -36,6 +39,11 @@ class Count implements Executable
3639
* * maxTimeMS (integer): The maximum amount of time to allow the query to
3740
* run.
3841
*
42+
* * readConcern (MongoDB\Driver\ReadConcern): Read concern.
43+
*
44+
* For servers < 3.2, this option is ignored as read concern is not
45+
* available.
46+
*
3947
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
4048
*
4149
* * skip (integer): The number of documents to skip before returning the
@@ -71,6 +79,10 @@ public function __construct($databaseName, $collectionName, $filter = [], array
7179
throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
7280
}
7381

82+
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
83+
throw new InvalidArgumentTypeException('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
84+
}
85+
7486
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
7587
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
7688
}
@@ -96,7 +108,7 @@ public function execute(Server $server)
96108
{
97109
$readPreference = isset($this->options['readPreference']) ? $this->options['readPreference'] : null;
98110

99-
$cursor = $server->executeCommand($this->databaseName, $this->createCommand(), $readPreference);
111+
$cursor = $server->executeCommand($this->databaseName, $this->createCommand($server), $readPreference);
100112
$result = current($cursor->toArray());
101113

102114
// Older server versions may return a float
@@ -110,9 +122,10 @@ public function execute(Server $server)
110122
/**
111123
* Create the count command.
112124
*
125+
* @param Server $server
113126
* @return Command
114127
*/
115-
private function createCommand()
128+
private function createCommand(Server $server)
116129
{
117130
$cmd = ['count' => $this->collectionName];
118131

@@ -126,6 +139,10 @@ private function createCommand()
126139
}
127140
}
128141

142+
if (isset($this->options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
143+
$cmd['readConcern'] = \MongoDB\read_concern_as_document($this->options['readConcern']);
144+
}
145+
129146
return new Command($cmd);
130147
}
131148
}

src/Operation/Distinct.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Operation;
44

55
use MongoDB\Driver\Command;
6+
use MongoDB\Driver\ReadConcern;
67
use MongoDB\Driver\ReadPreference;
78
use MongoDB\Driver\Server;
89
use MongoDB\Exception\InvalidArgumentException;
@@ -18,6 +19,8 @@
1819
*/
1920
class Distinct implements Executable
2021
{
22+
private static $wireVersionForReadConcern = 4;
23+
2124
private $databaseName;
2225
private $collectionName;
2326
private $fieldName;
@@ -32,6 +35,11 @@ class Distinct implements Executable
3235
* * maxTimeMS (integer): The maximum amount of time to allow the query to
3336
* run.
3437
*
38+
* * readConcern (MongoDB\Driver\ReadConcern): Read concern.
39+
*
40+
* For servers < 3.2, this option is ignored as read concern is not
41+
* available.
42+
*
3543
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
3644
*
3745
* @param string $databaseName Database name
@@ -51,6 +59,10 @@ public function __construct($databaseName, $collectionName, $fieldName, $filter
5159
throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
5260
}
5361

62+
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
63+
throw new InvalidArgumentTypeException('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
64+
}
65+
5466
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
5567
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
5668
}
@@ -73,7 +85,7 @@ public function execute(Server $server)
7385
{
7486
$readPreference = isset($this->options['readPreference']) ? $this->options['readPreference'] : null;
7587

76-
$cursor = $server->executeCommand($this->databaseName, $this->createCommand(), $readPreference);
88+
$cursor = $server->executeCommand($this->databaseName, $this->createCommand($server), $readPreference);
7789
$result = current($cursor->toArray());
7890

7991
if ( ! isset($result->values) || ! is_array($result->values)) {
@@ -86,9 +98,10 @@ public function execute(Server $server)
8698
/**
8799
* Create the distinct command.
88100
*
101+
* @param Server $server
89102
* @return Command
90103
*/
91-
private function createCommand()
104+
private function createCommand(Server $server)
92105
{
93106
$cmd = [
94107
'distinct' => $this->collectionName,
@@ -103,6 +116,10 @@ private function createCommand()
103116
$cmd['maxTimeMS'] = $this->options['maxTimeMS'];
104117
}
105118

119+
if (isset($this->options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
120+
$cmd['readConcern'] = \MongoDB\read_concern_as_document($this->options['readConcern']);
121+
}
122+
106123
return new Command($cmd);
107124
}
108125
}

src/Operation/Find.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Operation;
44

55
use MongoDB\Driver\Query;
6+
use MongoDB\Driver\ReadConcern;
67
use MongoDB\Driver\ReadPreference;
78
use MongoDB\Driver\Server;
89
use MongoDB\Exception\InvalidArgumentException;
@@ -65,6 +66,11 @@ class Find implements Executable
6566
* * projection (document): Limits the fields to return for the matching
6667
* document.
6768
*
69+
* * readConcern (MongoDB\Driver\ReadConcern): Read concern.
70+
*
71+
* For servers < 3.2, this option is ignored as read concern is not
72+
* available.
73+
*
6874
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
6975
*
7076
* * skip (integer): The number of documents to skip before returning.
@@ -133,6 +139,10 @@ public function __construct($databaseName, $collectionName, $filter, array $opti
133139
throw new InvalidArgumentTypeException('"projection" option', $options['projection'], 'array or object');
134140
}
135141

142+
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
143+
throw new InvalidArgumentTypeException('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
144+
}
145+
136146
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
137147
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
138148
}
@@ -188,7 +198,7 @@ private function createQuery()
188198
}
189199
}
190200

191-
foreach (['batchSize', 'limit', 'skip', 'sort', 'noCursorTimeout', 'oplogReplay', 'projection'] as $option) {
201+
foreach (['batchSize', 'limit', 'skip', 'sort', 'noCursorTimeout', 'oplogReplay', 'projection', 'readConcern'] as $option) {
192202
if (isset($this->options[$option])) {
193203
$options[$option] = $this->options[$option];
194204
}

src/Operation/FindOne.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ class FindOne implements Executable
3636
* * projection (document): Limits the fields to return for the matching
3737
* document.
3838
*
39+
* * readConcern (MongoDB\Driver\ReadConcern): Read concern.
40+
*
41+
* For servers < 3.2, this option is ignored as read concern is not
42+
* available.
43+
*
3944
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
4045
*
4146
* * skip (integer): The number of documents to skip before returning.

src/functions.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace MongoDB;
44

5+
use MongoDB\Driver\ReadConcern;
56
use MongoDB\Driver\Server;
67
use MongoDB\Exception\InvalidArgumentTypeException;
8+
use stdClass;
79

810
/**
911
* Return whether the first key in the document starts with a "$" character.
@@ -81,6 +83,25 @@ function generate_index_name($document)
8183
return $name;
8284
}
8385

86+
/**
87+
* Converts a ReadConcern instance to a stdClass for use in a BSON document.
88+
*
89+
* @internal
90+
* @see https://jira.mongodb.org/browse/PHPC-498
91+
* @param ReadConcern $readConcern Read concern
92+
* @return stdClass
93+
*/
94+
function read_concern_as_document(ReadConcern $readConcern)
95+
{
96+
$document = [];
97+
98+
if ($readConcern->getLevel() !== null) {
99+
$document['level'] = $readConcern->getLevel();
100+
}
101+
102+
return (object) $document;
103+
}
104+
84105
/**
85106
* Return whether the server supports a particular feature.
86107
*

tests/FunctionsTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace MongoDB\Tests;
4+
5+
use MongoDB\Driver\ReadConcern;
6+
7+
/**
8+
* Unit tests for utility functions.
9+
*/
10+
class FunctionsTest extends \PHPUnit_Framework_TestCase
11+
{
12+
/**
13+
* @dataProvider provideReadConcernsAndDocuments
14+
*/
15+
public function testReadConcernAsDocument(ReadConcern $readConcern, $expectedDocument)
16+
{
17+
$this->assertEquals($expectedDocument, \MongoDB\read_concern_as_document($readConcern));
18+
}
19+
20+
public function provideReadConcernsAndDocuments()
21+
{
22+
return [
23+
[ new ReadConcern, (object) [] ],
24+
[ new ReadConcern(ReadConcern::LOCAL), (object) ['level' => ReadConcern::LOCAL] ],
25+
[ new ReadConcern(ReadConcern::MAJORITY), (object) ['level' => ReadConcern::MAJORITY] ],
26+
];
27+
}
28+
}

tests/Operation/AggregateTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public function provideInvalidConstructorOptions()
5353
$options[][] = ['maxTimeMS' => $value];
5454
}
5555

56+
foreach ($this->getInvalidReadConcernValues() as $value) {
57+
$options[][] = ['readConcern' => $value];
58+
}
59+
5660
foreach ($this->getInvalidReadPreferenceValues() as $value) {
5761
$options[][] = ['readPreference' => $value];
5862
}

0 commit comments

Comments
 (0)