Skip to content

Commit 0b8362b

Browse files
committed
PHPLIB-618: Change estimatedDocumentCount to use $collStats
1 parent f915f4b commit 0b8362b

8 files changed

+1809
-12
lines changed

src/Operation/EstimatedDocumentCount.php

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717

1818
namespace MongoDB\Operation;
1919

20+
use MongoDB\Driver\Exception\CommandException;
2021
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
22+
use MongoDB\Driver\ReadConcern;
23+
use MongoDB\Driver\ReadPreference;
2124
use MongoDB\Driver\Server;
25+
use MongoDB\Driver\Session;
2226
use MongoDB\Exception\InvalidArgumentException;
2327
use MongoDB\Exception\UnexpectedValueException;
2428
use MongoDB\Exception\UnsupportedException;
2529
use function array_intersect_key;
30+
use function is_integer;
31+
use function MongoDB\server_supports_feature;
2632

2733
/**
2834
* Operation for obtaining an estimated count of documents in a collection
@@ -42,11 +48,15 @@ class EstimatedDocumentCount implements Executable, Explainable
4248
/** @var array */
4349
private $options;
4450

45-
/** @var Count */
46-
private $count;
51+
/** @var int */
52+
private static $errorCodeCollectionNotFound = 26;
53+
54+
/** @var int */
55+
private static $wireVersionForCollStats = 12;
4756

4857
/**
49-
* Constructs a count command.
58+
* Constructs a command to get the estimated number of documents in a
59+
* collection.
5060
*
5161
* Supported options:
5262
*
@@ -73,9 +83,24 @@ public function __construct($databaseName, $collectionName, array $options = [])
7383
{
7484
$this->databaseName = (string) $databaseName;
7585
$this->collectionName = (string) $collectionName;
76-
$this->options = array_intersect_key($options, ['maxTimeMS' => 1, 'readConcern' => 1, 'readPreference' => 1, 'session' => 1]);
7786

78-
$this->count = $this->createCount();
87+
if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
88+
throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
89+
}
90+
91+
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
92+
throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class);
93+
}
94+
95+
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
96+
throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class);
97+
}
98+
99+
if (isset($options['session']) && ! $options['session'] instanceof Session) {
100+
throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
101+
}
102+
103+
$this->options = array_intersect_key($options, ['maxTimeMS' => 1, 'readConcern' => 1, 'readPreference' => 1, 'session' => 1]);
79104
}
80105

81106
/**
@@ -90,18 +115,53 @@ public function __construct($databaseName, $collectionName, array $options = [])
90115
*/
91116
public function execute(Server $server)
92117
{
93-
return $this->count->execute($server);
118+
$command = $this->createCommand($server);
119+
if ($command instanceof Aggregate) {
120+
try {
121+
$cursor = $command->execute($server);
122+
} catch (CommandException $e) {
123+
if ($e->getCode() == self::$errorCodeCollectionNotFound) {
124+
return 0;
125+
}
126+
127+
throw $e;
128+
}
129+
130+
$cursor->rewind();
131+
132+
return $cursor->current()->n;
133+
}
134+
135+
return $command->execute($server);
94136
}
95137

96138
public function getCommandDocument(Server $server)
97139
{
98-
return $this->count->getCommandDocument($server);
140+
return $this->createCommand($server)->getCommandDocument($server);
99141
}
100142

101-
/**
102-
* @return Count
103-
*/
104-
private function createCount()
143+
private function createAggregate() : Aggregate
144+
{
145+
return new Aggregate(
146+
$this->databaseName,
147+
$this->collectionName,
148+
[
149+
['$collStats' => ['count' => (object) []]],
150+
['$group' => ['_id' => 1, 'n' => ['$sum' => '$count']]],
151+
],
152+
$this->options
153+
);
154+
}
155+
156+
/** @return Aggregate|Count */
157+
private function createCommand(Server $server)
158+
{
159+
return server_supports_feature($server, self::$wireVersionForCollStats)
160+
? $this->createAggregate()
161+
: $this->createCount();
162+
}
163+
164+
private function createCount() : Count
105165
{
106166
return new Count($this->databaseName, $this->collectionName, [], $this->options);
107167
}

tests/FunctionalTestCase.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use function phpinfo;
3333
use function preg_match;
3434
use function preg_quote;
35+
use function preg_replace;
3536
use function sprintf;
3637
use function version_compare;
3738
use const INFO_MODULES;
@@ -278,7 +279,7 @@ protected function getServerVersion(ReadPreference $readPreference = null)
278279
$document = current($cursor->toArray());
279280

280281
if (isset($document['version']) && is_string($document['version'])) {
281-
return $document['version'];
282+
return preg_replace('#^(\d+\.\d+\.\d+).*$#', '\1', $document['version']);
282283
}
283284

284285
throw new UnexpectedValueException('Could not determine server version');
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
{
2+
"runOn": [
3+
{
4+
"minServerVersion": "4.9.0"
5+
}
6+
],
7+
"database_name": "retryable-reads-tests",
8+
"collection_name": "coll",
9+
"data": [
10+
{
11+
"_id": 1,
12+
"x": 11
13+
},
14+
{
15+
"_id": 2,
16+
"x": 22
17+
}
18+
],
19+
"tests": [
20+
{
21+
"description": "EstimatedDocumentCount succeeds on first attempt",
22+
"operations": [
23+
{
24+
"name": "estimatedDocumentCount",
25+
"object": "collection",
26+
"result": 2
27+
}
28+
],
29+
"expectations": [
30+
{
31+
"command_started_event": {
32+
"command": {
33+
"aggregate": "coll",
34+
"pipeline": [
35+
{
36+
"$collStats": {
37+
"count": {}
38+
}
39+
},
40+
{
41+
"$group": {
42+
"_id": 1,
43+
"n": {
44+
"$sum": "$count"
45+
}
46+
}
47+
}
48+
]
49+
},
50+
"database_name": "retryable-reads-tests"
51+
}
52+
}
53+
]
54+
},
55+
{
56+
"description": "EstimatedDocumentCount succeeds on second attempt",
57+
"failPoint": {
58+
"configureFailPoint": "failCommand",
59+
"mode": {
60+
"times": 1
61+
},
62+
"data": {
63+
"failCommands": [
64+
"aggregate"
65+
],
66+
"closeConnection": true
67+
}
68+
},
69+
"operations": [
70+
{
71+
"name": "estimatedDocumentCount",
72+
"object": "collection",
73+
"result": 2
74+
}
75+
],
76+
"expectations": [
77+
{
78+
"command_started_event": {
79+
"command": {
80+
"aggregate": "coll",
81+
"pipeline": [
82+
{
83+
"$collStats": {
84+
"count": {}
85+
}
86+
},
87+
{
88+
"$group": {
89+
"_id": 1,
90+
"n": {
91+
"$sum": "$count"
92+
}
93+
}
94+
}
95+
]
96+
},
97+
"database_name": "retryable-reads-tests"
98+
}
99+
},
100+
{
101+
"command_started_event": {
102+
"command": {
103+
"aggregate": "coll",
104+
"pipeline": [
105+
{
106+
"$collStats": {
107+
"count": {}
108+
}
109+
},
110+
{
111+
"$group": {
112+
"_id": 1,
113+
"n": {
114+
"$sum": "$count"
115+
}
116+
}
117+
}
118+
]
119+
},
120+
"database_name": "retryable-reads-tests"
121+
}
122+
}
123+
]
124+
},
125+
{
126+
"description": "EstimatedDocumentCount fails on first attempt",
127+
"clientOptions": {
128+
"retryReads": false
129+
},
130+
"failPoint": {
131+
"configureFailPoint": "failCommand",
132+
"mode": {
133+
"times": 1
134+
},
135+
"data": {
136+
"failCommands": [
137+
"aggregate"
138+
],
139+
"closeConnection": true
140+
}
141+
},
142+
"operations": [
143+
{
144+
"name": "estimatedDocumentCount",
145+
"object": "collection",
146+
"error": true
147+
}
148+
],
149+
"expectations": [
150+
{
151+
"command_started_event": {
152+
"command": {
153+
"aggregate": "coll",
154+
"pipeline": [
155+
{
156+
"$collStats": {
157+
"count": {}
158+
}
159+
},
160+
{
161+
"$group": {
162+
"_id": 1,
163+
"n": {
164+
"$sum": "$count"
165+
}
166+
}
167+
}
168+
]
169+
},
170+
"database_name": "retryable-reads-tests"
171+
}
172+
}
173+
]
174+
},
175+
{
176+
"description": "EstimatedDocumentCount fails on second attempt",
177+
"failPoint": {
178+
"configureFailPoint": "failCommand",
179+
"mode": {
180+
"times": 2
181+
},
182+
"data": {
183+
"failCommands": [
184+
"aggregate"
185+
],
186+
"closeConnection": true
187+
}
188+
},
189+
"operations": [
190+
{
191+
"name": "estimatedDocumentCount",
192+
"object": "collection",
193+
"error": true
194+
}
195+
],
196+
"expectations": [
197+
{
198+
"command_started_event": {
199+
"command": {
200+
"aggregate": "coll",
201+
"pipeline": [
202+
{
203+
"$collStats": {
204+
"count": {}
205+
}
206+
},
207+
{
208+
"$group": {
209+
"_id": 1,
210+
"n": {
211+
"$sum": "$count"
212+
}
213+
}
214+
}
215+
]
216+
},
217+
"database_name": "retryable-reads-tests"
218+
}
219+
},
220+
{
221+
"command_started_event": {
222+
"command": {
223+
"aggregate": "coll",
224+
"pipeline": [
225+
{
226+
"$collStats": {
227+
"count": {}
228+
}
229+
},
230+
{
231+
"$group": {
232+
"_id": 1,
233+
"n": {
234+
"$sum": "$count"
235+
}
236+
}
237+
}
238+
]
239+
},
240+
"database_name": "retryable-reads-tests"
241+
}
242+
}
243+
]
244+
}
245+
]
246+
}

0 commit comments

Comments
 (0)