Skip to content

Commit a5240ae

Browse files
authored
feat(collection): Implement new count API
Deprecate count, implement and test countDocuments and estimatedDocumentCount Fixes NODE-1501
1 parent 2cb4894 commit a5240ae

File tree

6 files changed

+261
-37
lines changed

6 files changed

+261
-37
lines changed

lib/collection.js

+84-13
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,7 @@ var indexInformation = function(self, options, callback) {
18291829
* @param {ClientSession} [options.session] optional session to use for this operation
18301830
* @param {Collection~countCallback} [callback] The command result callback
18311831
* @return {Promise} returns Promise if no callback passed
1832+
* @deprecated use countDocuments or estimatedDocumentCount instead
18321833
*/
18331834
Collection.prototype.count = function(query, options, callback) {
18341835
var args = Array.prototype.slice.call(arguments, 0);
@@ -1839,19 +1840,36 @@ Collection.prototype.count = function(query, options, callback) {
18391840
return executeOperation(this.s.topology, count, [this, query, options, callback]);
18401841
};
18411842

1842-
var count = function(self, query, options, callback) {
1843-
var skip = options.skip;
1844-
var limit = options.limit;
1845-
var hint = options.hint;
1846-
var maxTimeMS = options.maxTimeMS;
1843+
/**
1844+
* Gets an estimate of the count of documents in a collection using collection metadata.
1845+
* @method
1846+
* @param {object} [options] Optional settings.
1847+
* @param {number} [options.maxTimeMS] The maximum amount of time to allow the operation to run.
1848+
* @param {Collection~countCallback} [callback] The command result callback.
1849+
* @return {Promise} returns Promise if no callback passed.
1850+
*/
1851+
Collection.prototype.estimatedDocumentCount = function(options, callback) {
1852+
if (typeof options === 'function') (callback = options), (options = {});
1853+
options = options || {};
18471854

1848-
// Final query
1849-
var cmd = {
1850-
count: self.s.name,
1855+
options = typeof options.maxTimeMS === 'number' ? options : {};
1856+
1857+
return executeOperation(this.s.topology, count, [this, null, options, callback]);
1858+
};
1859+
1860+
const count = function(collection, query, options, callback) {
1861+
const skip = options.skip;
1862+
const limit = options.limit;
1863+
const hint = options.hint;
1864+
const maxTimeMS = options.maxTimeMS;
1865+
query = query || {};
1866+
1867+
const cmd = {
1868+
count: collection.s.name,
18511869
query: query
18521870
};
18531871

1854-
// Add limit, skip and maxTimeMS if defined
1872+
// Add skip, limit, maxTimeMS, and hint if defined
18551873
if (typeof skip === 'number') cmd.skip = skip;
18561874
if (typeof limit === 'number') cmd.limit = limit;
18571875
if (typeof maxTimeMS === 'number') cmd.maxTimeMS = maxTimeMS;
@@ -1860,21 +1878,74 @@ var count = function(self, query, options, callback) {
18601878
options = shallowClone(options);
18611879

18621880
// Ensure we have the right read preference inheritance
1863-
options = getReadPreference(self, options, self.s.db);
1881+
options = getReadPreference(collection, options, collection.s.db);
18641882

18651883
// Do we have a readConcern specified
1866-
decorateWithReadConcern(cmd, self, options);
1884+
decorateWithReadConcern(cmd, collection, options);
18671885

18681886
// Have we specified collation
1869-
decorateWithCollation(cmd, self, options);
1887+
decorateWithCollation(cmd, collection, options);
18701888

18711889
// Execute command
1872-
self.s.db.command(cmd, options, function(err, result) {
1890+
collection.s.db.command(cmd, options, function(err, result) {
18731891
if (err) return handleCallback(callback, err);
18741892
handleCallback(callback, null, result.n);
18751893
});
18761894
};
18771895

1896+
/**
1897+
* Gets the number of documents matching the filter.
1898+
* @param {object} [query] the query for the count
1899+
* @param {object} [options] Optional settings.
1900+
* @param {object} [options.collation] Specifies a collation.
1901+
* @param {string|object} [options.hint] The index to use.
1902+
* @param {number} [options.limit] The maximum number of document to count.
1903+
* @param {number} [options.maxTimeMS] The maximum amount of time to allow the operation to run.
1904+
* @param {number} [options.skip] The number of documents to skip before counting.
1905+
* @param {Collection~countCallback} [callback] The command result callback.
1906+
* @return {Promise} returns Promise if no callback passed.
1907+
*/
1908+
1909+
Collection.prototype.countDocuments = function(query, options, callback) {
1910+
var args = Array.prototype.slice.call(arguments, 0);
1911+
callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
1912+
query = args.length ? args.shift() || {} : {};
1913+
options = args.length ? args.shift() || {} : {};
1914+
1915+
return executeOperation(this.s.topology, countDocuments, [this, query, options, callback]);
1916+
};
1917+
1918+
const countDocuments = function(collection, query, options, callback) {
1919+
const skip = options.skip;
1920+
const limit = options.limit;
1921+
options = Object.assign({}, options);
1922+
1923+
const pipeline = [{ $match: query }];
1924+
1925+
// Add skip and limit if defined
1926+
if (typeof skip === 'number') {
1927+
pipeline.push({ $skip: skip });
1928+
}
1929+
1930+
if (typeof limit === 'number') {
1931+
pipeline.push({ $limit: limit });
1932+
}
1933+
1934+
pipeline.push({ $group: { _id: null, n: { $sum: 1 } } });
1935+
1936+
delete options.limit;
1937+
delete options.skip;
1938+
1939+
// TODO: look out for how this plays into operations refactor
1940+
collection.aggregate(pipeline, options, function(err, result) {
1941+
if (err) return handleCallback(callback, err);
1942+
result
1943+
.toArray()
1944+
.then(docs => handleCallback(callback, null, docs[0].n))
1945+
.catch(e => handleCallback(e));
1946+
});
1947+
};
1948+
18781949
/**
18791950
* The distinct command returns returns a list of distinct values for the given key across a collection.
18801951
* @method

test/functional/crud_spec_tests.js

+47-12
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,21 @@ describe('CRUD spec', function() {
3838
const scenario = scenarioData[1];
3939
scenario.name = scenarioName;
4040

41+
const metadata = {
42+
requires: {
43+
topology: ['single', 'replicaset', 'sharded']
44+
}
45+
};
46+
47+
if (scenario.minServerVersion) {
48+
metadata.requires.mongodb = `>=${scenario.minServerVersion}`;
49+
}
50+
4151
describe(scenarioName, function() {
4252
scenario.tests.forEach(scenarioTest => {
4353
beforeEach(() => testContext.db.dropDatabase());
4454
it(scenarioTest.description, {
45-
metadata: {
46-
requires: {
47-
topology: ['single', 'replicaset', 'sharded'],
48-
mongodb: `>=${scenario.minServerVersion}`
49-
}
50-
},
55+
metadata,
5156
test: function() {
5257
return executeScenario(scenario, scenarioTest, this.configuration, testContext);
5358
}
@@ -63,17 +68,22 @@ describe('CRUD spec', function() {
6368
const scenario = scenarioData[1];
6469
scenario.name = scenarioName;
6570

71+
const metadata = {
72+
requires: {
73+
topology: ['single', 'replicaset', 'sharded']
74+
}
75+
};
76+
77+
if (scenario.minServerVersion) {
78+
metadata.requires.mongodb = `>=${scenario.minServerVersion}`;
79+
}
80+
6681
describe(scenarioName, function() {
6782
beforeEach(() => testContext.db.dropDatabase());
6883

6984
scenario.tests.forEach(scenarioTest => {
7085
it(scenarioTest.description, {
71-
metadata: {
72-
requires: {
73-
topology: ['single', 'replicaset', 'sharded'],
74-
mongodb: `>=${scenario.minServerVersion}`
75-
}
76-
},
86+
metadata,
7787
test: function() {
7888
return executeScenario(scenario, scenarioTest, this.configuration, testContext);
7989
}
@@ -120,6 +130,27 @@ describe('CRUD spec', function() {
120130
.then(result => test.equal(result, scenarioTest.outcome.result));
121131
}
122132

133+
function executeCountDocumentsTest(scenarioTest, db, collection) {
134+
const args = scenarioTest.operation.arguments;
135+
const filter = args.filter;
136+
const options = Object.assign({}, args);
137+
delete options.filter;
138+
139+
return collection
140+
.countDocuments(filter, options)
141+
.then(result => test.equal(result, scenarioTest.outcome.result));
142+
}
143+
144+
function executeEstimatedDocumentCountTest(scenarioTest, db, collection) {
145+
const args = scenarioTest.operation.arguments;
146+
const options = Object.assign({}, args);
147+
delete options.filter;
148+
149+
return collection
150+
.estimatedDocumentCount(options)
151+
.then(result => test.equal(result, scenarioTest.outcome.result));
152+
}
153+
123154
function executeDistinctTest(scenarioTest, db, collection) {
124155
const args = scenarioTest.operation.arguments;
125156
const fieldName = args.fieldName;
@@ -326,6 +357,10 @@ describe('CRUD spec', function() {
326357
return executeAggregateTest(scenarioTest, context.db, collection);
327358
case 'count':
328359
return executeCountTest(scenarioTest, context.db, collection);
360+
case 'countDocuments':
361+
return executeCountDocumentsTest(scenarioTest, context.db, collection);
362+
case 'estimatedDocumentCount':
363+
return executeEstimatedDocumentCountTest(scenarioTest, context.db, collection);
329364
case 'distinct':
330365
return executeDistinctTest(scenarioTest, context.db, collection);
331366
case 'find':

test/functional/spec/crud/read/count-collation.json

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,25 @@
88
"minServerVersion": "3.4",
99
"tests": [
1010
{
11-
"description": "Count with collation",
11+
"description": "Count documents with collation",
12+
"operation": {
13+
"name": "countDocuments",
14+
"arguments": {
15+
"filter": {
16+
"x": "ping"
17+
},
18+
"collation": {
19+
"locale": "en_US",
20+
"strength": 2
21+
}
22+
}
23+
},
24+
"outcome": {
25+
"result": 1
26+
}
27+
},
28+
{
29+
"description": "Deprecated count with collation",
1230
"operation": {
1331
"name": "count",
1432
"arguments": {

test/functional/spec/crud/read/count-collation.yml

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@ minServerVersion: '3.4'
44

55
tests:
66
-
7-
description: "Count with collation"
7+
description: "Count documents with collation"
88
operation:
9-
name: count
9+
name: countDocuments
1010
arguments:
1111
filter: { x: 'ping' }
1212
collation: { locale: 'en_US', strength: 2 } # https://docs.mongodb.com/master/reference/collation/#collation-document
1313

1414
outcome:
1515
result: 1
16+
-
17+
description: "Deprecated count with collation"
18+
operation:
19+
name: count
20+
arguments:
21+
filter: { x: 'ping' }
22+
collation: { locale: 'en_US', strength: 2 }
23+
24+
outcome:
25+
result: 1

test/functional/spec/crud/read/count.json

+56-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,59 @@
1515
],
1616
"tests": [
1717
{
18-
"description": "Count without a filter",
18+
"description": "Estimated document count",
19+
"operation": {
20+
"name": "estimatedDocumentCount",
21+
"arguments": {}
22+
},
23+
"outcome": {
24+
"result": 3
25+
}
26+
},
27+
{
28+
"description": "Count documents without a filter",
29+
"operation": {
30+
"name": "countDocuments",
31+
"arguments": {
32+
"filter": {}
33+
}
34+
},
35+
"outcome": {
36+
"result": 3
37+
}
38+
},
39+
{
40+
"description": "Count documents with a filter",
41+
"operation": {
42+
"name": "countDocuments",
43+
"arguments": {
44+
"filter": {
45+
"_id": {
46+
"$gt": 1
47+
}
48+
}
49+
}
50+
},
51+
"outcome": {
52+
"result": 2
53+
}
54+
},
55+
{
56+
"description": "Count documents with skip and limit",
57+
"operation": {
58+
"name": "countDocuments",
59+
"arguments": {
60+
"filter": {},
61+
"skip": 1,
62+
"limit": 3
63+
}
64+
},
65+
"outcome": {
66+
"result": 2
67+
}
68+
},
69+
{
70+
"description": "Deprecated count without a filter",
1971
"operation": {
2072
"name": "count",
2173
"arguments": {
@@ -27,7 +79,7 @@
2779
}
2880
},
2981
{
30-
"description": "Count with a filter",
82+
"description": "Deprecated count with a filter",
3183
"operation": {
3284
"name": "count",
3385
"arguments": {
@@ -43,9 +95,9 @@
4395
}
4496
},
4597
{
46-
"description": "Count with skip and limit",
98+
"description": "Deprecated count with skip and limit",
4799
"operation": {
48-
"name": "count",
100+
"name": "countDocuments",
49101
"arguments": {
50102
"filter": {},
51103
"skip": 1,

0 commit comments

Comments
 (0)