Skip to content

Commit

Permalink
feat(collection): Implement new count API
Browse files Browse the repository at this point in the history
Deprecate count, implement and test countDocuments
and estimatedDocumentCount

Fixes NODE-1501
  • Loading branch information
rweinberger committed Jun 13, 2018
1 parent 2cb4894 commit a5240ae
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 37 deletions.
97 changes: 84 additions & 13 deletions lib/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,7 @@ var indexInformation = function(self, options, callback) {
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {Collection~countCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
* @deprecated use countDocuments or estimatedDocumentCount instead
*/
Collection.prototype.count = function(query, options, callback) {
var args = Array.prototype.slice.call(arguments, 0);
Expand All @@ -1839,19 +1840,36 @@ Collection.prototype.count = function(query, options, callback) {
return executeOperation(this.s.topology, count, [this, query, options, callback]);
};

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

// Final query
var cmd = {
count: self.s.name,
options = typeof options.maxTimeMS === 'number' ? options : {};

return executeOperation(this.s.topology, count, [this, null, options, callback]);
};

const count = function(collection, query, options, callback) {
const skip = options.skip;
const limit = options.limit;
const hint = options.hint;
const maxTimeMS = options.maxTimeMS;
query = query || {};

const cmd = {
count: collection.s.name,
query: query
};

// Add limit, skip and maxTimeMS if defined
// Add skip, limit, maxTimeMS, and hint if defined
if (typeof skip === 'number') cmd.skip = skip;
if (typeof limit === 'number') cmd.limit = limit;
if (typeof maxTimeMS === 'number') cmd.maxTimeMS = maxTimeMS;
Expand All @@ -1860,21 +1878,74 @@ var count = function(self, query, options, callback) {
options = shallowClone(options);

// Ensure we have the right read preference inheritance
options = getReadPreference(self, options, self.s.db);
options = getReadPreference(collection, options, collection.s.db);

// Do we have a readConcern specified
decorateWithReadConcern(cmd, self, options);
decorateWithReadConcern(cmd, collection, options);

// Have we specified collation
decorateWithCollation(cmd, self, options);
decorateWithCollation(cmd, collection, options);

// Execute command
self.s.db.command(cmd, options, function(err, result) {
collection.s.db.command(cmd, options, function(err, result) {
if (err) return handleCallback(callback, err);
handleCallback(callback, null, result.n);
});
};

/**
* Gets the number of documents matching the filter.
* @param {object} [query] the query for the count
* @param {object} [options] Optional settings.
* @param {object} [options.collation] Specifies a collation.
* @param {string|object} [options.hint] The index to use.
* @param {number} [options.limit] The maximum number of document to count.
* @param {number} [options.maxTimeMS] The maximum amount of time to allow the operation to run.
* @param {number} [options.skip] The number of documents to skip before counting.
* @param {Collection~countCallback} [callback] The command result callback.
* @return {Promise} returns Promise if no callback passed.
*/

Collection.prototype.countDocuments = function(query, options, callback) {
var args = Array.prototype.slice.call(arguments, 0);
callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
query = args.length ? args.shift() || {} : {};
options = args.length ? args.shift() || {} : {};

return executeOperation(this.s.topology, countDocuments, [this, query, options, callback]);
};

const countDocuments = function(collection, query, options, callback) {
const skip = options.skip;
const limit = options.limit;
options = Object.assign({}, options);

const pipeline = [{ $match: query }];

// Add skip and limit if defined
if (typeof skip === 'number') {
pipeline.push({ $skip: skip });
}

if (typeof limit === 'number') {
pipeline.push({ $limit: limit });
}

pipeline.push({ $group: { _id: null, n: { $sum: 1 } } });

delete options.limit;
delete options.skip;

// TODO: look out for how this plays into operations refactor
collection.aggregate(pipeline, options, function(err, result) {
if (err) return handleCallback(callback, err);
result
.toArray()
.then(docs => handleCallback(callback, null, docs[0].n))
.catch(e => handleCallback(e));
});
};

/**
* The distinct command returns returns a list of distinct values for the given key across a collection.
* @method
Expand Down
59 changes: 47 additions & 12 deletions test/functional/crud_spec_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,21 @@ describe('CRUD spec', function() {
const scenario = scenarioData[1];
scenario.name = scenarioName;

const metadata = {
requires: {
topology: ['single', 'replicaset', 'sharded']
}
};

if (scenario.minServerVersion) {
metadata.requires.mongodb = `>=${scenario.minServerVersion}`;
}

describe(scenarioName, function() {
scenario.tests.forEach(scenarioTest => {
beforeEach(() => testContext.db.dropDatabase());
it(scenarioTest.description, {
metadata: {
requires: {
topology: ['single', 'replicaset', 'sharded'],
mongodb: `>=${scenario.minServerVersion}`
}
},
metadata,
test: function() {
return executeScenario(scenario, scenarioTest, this.configuration, testContext);
}
Expand All @@ -63,17 +68,22 @@ describe('CRUD spec', function() {
const scenario = scenarioData[1];
scenario.name = scenarioName;

const metadata = {
requires: {
topology: ['single', 'replicaset', 'sharded']
}
};

if (scenario.minServerVersion) {
metadata.requires.mongodb = `>=${scenario.minServerVersion}`;
}

describe(scenarioName, function() {
beforeEach(() => testContext.db.dropDatabase());

scenario.tests.forEach(scenarioTest => {
it(scenarioTest.description, {
metadata: {
requires: {
topology: ['single', 'replicaset', 'sharded'],
mongodb: `>=${scenario.minServerVersion}`
}
},
metadata,
test: function() {
return executeScenario(scenario, scenarioTest, this.configuration, testContext);
}
Expand Down Expand Up @@ -120,6 +130,27 @@ describe('CRUD spec', function() {
.then(result => test.equal(result, scenarioTest.outcome.result));
}

function executeCountDocumentsTest(scenarioTest, db, collection) {
const args = scenarioTest.operation.arguments;
const filter = args.filter;
const options = Object.assign({}, args);
delete options.filter;

return collection
.countDocuments(filter, options)
.then(result => test.equal(result, scenarioTest.outcome.result));
}

function executeEstimatedDocumentCountTest(scenarioTest, db, collection) {
const args = scenarioTest.operation.arguments;
const options = Object.assign({}, args);
delete options.filter;

return collection
.estimatedDocumentCount(options)
.then(result => test.equal(result, scenarioTest.outcome.result));
}

function executeDistinctTest(scenarioTest, db, collection) {
const args = scenarioTest.operation.arguments;
const fieldName = args.fieldName;
Expand Down Expand Up @@ -326,6 +357,10 @@ describe('CRUD spec', function() {
return executeAggregateTest(scenarioTest, context.db, collection);
case 'count':
return executeCountTest(scenarioTest, context.db, collection);
case 'countDocuments':
return executeCountDocumentsTest(scenarioTest, context.db, collection);
case 'estimatedDocumentCount':
return executeEstimatedDocumentCountTest(scenarioTest, context.db, collection);
case 'distinct':
return executeDistinctTest(scenarioTest, context.db, collection);
case 'find':
Expand Down
20 changes: 19 additions & 1 deletion test/functional/spec/crud/read/count-collation.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@
"minServerVersion": "3.4",
"tests": [
{
"description": "Count with collation",
"description": "Count documents with collation",
"operation": {
"name": "countDocuments",
"arguments": {
"filter": {
"x": "ping"
},
"collation": {
"locale": "en_US",
"strength": 2
}
}
},
"outcome": {
"result": 1
}
},
{
"description": "Deprecated count with collation",
"operation": {
"name": "count",
"arguments": {
Expand Down
14 changes: 12 additions & 2 deletions test/functional/spec/crud/read/count-collation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ minServerVersion: '3.4'

tests:
-
description: "Count with collation"
description: "Count documents with collation"
operation:
name: count
name: countDocuments
arguments:
filter: { x: 'ping' }
collation: { locale: 'en_US', strength: 2 } # https://docs.mongodb.com/master/reference/collation/#collation-document

outcome:
result: 1
-
description: "Deprecated count with collation"
operation:
name: count
arguments:
filter: { x: 'ping' }
collation: { locale: 'en_US', strength: 2 }

outcome:
result: 1
60 changes: 56 additions & 4 deletions test/functional/spec/crud/read/count.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,59 @@
],
"tests": [
{
"description": "Count without a filter",
"description": "Estimated document count",
"operation": {
"name": "estimatedDocumentCount",
"arguments": {}
},
"outcome": {
"result": 3
}
},
{
"description": "Count documents without a filter",
"operation": {
"name": "countDocuments",
"arguments": {
"filter": {}
}
},
"outcome": {
"result": 3
}
},
{
"description": "Count documents with a filter",
"operation": {
"name": "countDocuments",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
}
}
},
"outcome": {
"result": 2
}
},
{
"description": "Count documents with skip and limit",
"operation": {
"name": "countDocuments",
"arguments": {
"filter": {},
"skip": 1,
"limit": 3
}
},
"outcome": {
"result": 2
}
},
{
"description": "Deprecated count without a filter",
"operation": {
"name": "count",
"arguments": {
Expand All @@ -27,7 +79,7 @@
}
},
{
"description": "Count with a filter",
"description": "Deprecated count with a filter",
"operation": {
"name": "count",
"arguments": {
Expand All @@ -43,9 +95,9 @@
}
},
{
"description": "Count with skip and limit",
"description": "Deprecated count with skip and limit",
"operation": {
"name": "count",
"name": "countDocuments",
"arguments": {
"filter": {},
"skip": 1,
Expand Down

0 comments on commit a5240ae

Please sign in to comment.