Skip to content

Commit 5cb7a55

Browse files
emadumljhaywar
authored andcommitted
fix(NODE-3074): update estimated document count for v1 api (#2764)
The estimatedDocumentCount operation now uses an aggregate command with a $collStats stage against 5.0+ servers.
1 parent 2f86ebe commit 5cb7a55

39 files changed

+2602
-403
lines changed

src/collection.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,6 @@ export class Collection {
939939
callback?: Callback<number>
940940
): Promise<number> | void {
941941
if (typeof options === 'function') (callback = options), (options = {});
942-
943942
return executeOperation(
944943
getTopology(this),
945944
new EstimatedDocumentCountOperation(this, resolveOptions(this, options)),
@@ -1430,7 +1429,7 @@ export class Collection {
14301429
query = query || {};
14311430
return executeOperation(
14321431
getTopology(this),
1433-
new EstimatedDocumentCountOperation(this, query, resolveOptions(this, options)),
1432+
new CountDocumentsOperation(this, query, resolveOptions(this, options)),
14341433
callback
14351434
);
14361435
}

src/operations/estimated_document_count.ts

+31-33
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,60 @@
1-
import { Aspect, defineAspects, Hint } from './operation';
1+
import { Aspect, defineAspects } from './operation';
22
import { CommandOperation, CommandOperationOptions } from './command';
3-
import type { Callback } from '../utils';
3+
import { Callback, maxWireVersion } from '../utils';
44
import type { Document } from '../bson';
55
import type { Server } from '../sdam/server';
66
import type { Collection } from '../collection';
77
import type { ClientSession } from '../sessions';
8+
import type { MongoError } from '../error';
89

910
/** @public */
1011
export interface EstimatedDocumentCountOptions extends CommandOperationOptions {
11-
skip?: number;
12-
limit?: number;
13-
hint?: Hint;
12+
/**
13+
* The maximum amount of time to allow the operation to run.
14+
*
15+
* This option is sent only if the caller explicitly provides a value. The default is to not send a value.
16+
*/
17+
maxTimeMS?: number;
1418
}
1519

1620
/** @internal */
1721
export class EstimatedDocumentCountOperation extends CommandOperation<number> {
1822
options: EstimatedDocumentCountOptions;
1923
collectionName: string;
20-
query?: Document;
21-
22-
constructor(collection: Collection, options: EstimatedDocumentCountOptions);
23-
constructor(collection: Collection, query: Document, options: EstimatedDocumentCountOptions);
24-
constructor(
25-
collection: Collection,
26-
query?: Document | EstimatedDocumentCountOptions,
27-
options?: EstimatedDocumentCountOptions
28-
) {
29-
if (typeof options === 'undefined') {
30-
options = query as EstimatedDocumentCountOptions;
31-
query = undefined;
32-
}
3324

25+
constructor(collection: Collection, options: EstimatedDocumentCountOptions = {}) {
3426
super(collection, options);
3527
this.options = options;
3628
this.collectionName = collection.collectionName;
37-
if (query) {
38-
this.query = query;
39-
}
4029
}
4130

4231
execute(server: Server, session: ClientSession, callback: Callback<number>): void {
43-
const options = this.options;
44-
const cmd: Document = { count: this.collectionName };
45-
46-
if (this.query) {
47-
cmd.query = this.query;
32+
if (maxWireVersion(server) < 12) {
33+
return this.executeLegacy(server, session, callback);
4834
}
35+
const pipeline = [{ $collStats: { count: {} } }, { $group: { _id: 1, n: { $sum: '$count' } } }];
4936

50-
if (typeof options.skip === 'number') {
51-
cmd.skip = options.skip;
52-
}
37+
const cmd: Document = { aggregate: this.collectionName, pipeline, cursor: {} };
5338

54-
if (typeof options.limit === 'number') {
55-
cmd.limit = options.limit;
39+
if (typeof this.options.maxTimeMS === 'number') {
40+
cmd.maxTimeMS = this.options.maxTimeMS;
5641
}
5742

58-
if (options.hint) {
59-
cmd.hint = options.hint;
43+
super.executeCommand(server, session, cmd, (err, response) => {
44+
if (err && (err as MongoError).code !== 26) {
45+
callback(err);
46+
return;
47+
}
48+
49+
callback(undefined, response?.cursor?.firstBatch[0]?.n || 0);
50+
});
51+
}
52+
53+
executeLegacy(server: Server, session: ClientSession, callback: Callback<number>): void {
54+
const cmd: Document = { count: this.collectionName };
55+
56+
if (typeof this.options.maxTimeMS === 'number') {
57+
cmd.maxTimeMS = this.options.maxTimeMS;
6058
}
6159

6260
super.executeCommand(server, session, cmd, (err, response) => {

test/functional/apm.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,7 @@ describe('APM', function () {
875875
}
876876

877877
loadSpecTests('apm').forEach(scenario => {
878+
if (scenario.name === 'command') return; // FIXME(NODE-3074): remove when `count` spec tests have been fixed
878879
describe(scenario.name, function () {
879880
scenario.tests.forEach(test => {
880881
const requirements = { topology: ['single', 'replicaset', 'sharded'] };

test/functional/crud_spec.test.js

+19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const TestRunnerContext = require('./spec-runner').TestRunnerContext;
1010
const gatherTestSuites = require('./spec-runner').gatherTestSuites;
1111
const generateTopologyTests = require('./spec-runner').generateTopologyTests;
1212

13+
const { loadSpecTests } = require('../spec/index');
14+
const { runUnifiedTest } = require('./unified-spec-runner/runner');
15+
1316
function enforceServerVersionLimits(requires, scenario) {
1417
const versionLimits = [];
1518
if (scenario.minServerVersion) {
@@ -433,3 +436,19 @@ describe('CRUD v2', function () {
433436

434437
generateTopologyTests(testSuites, testContext);
435438
});
439+
440+
describe('CRUD unified', function () {
441+
for (const crudSpecTest of loadSpecTests('crud/unified')) {
442+
expect(crudSpecTest).to.exist;
443+
context(String(crudSpecTest.description), function () {
444+
for (const test of crudSpecTest.tests) {
445+
it(String(test.description), {
446+
metadata: { sessions: { skipLeakTests: true } },
447+
test: async function () {
448+
await runUnifiedTest(this, crudSpecTest, test);
449+
}
450+
});
451+
}
452+
});
453+
}
454+
});

test/functional/cursor.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3594,7 +3594,7 @@ describe('Cursor', function () {
35943594
}
35953595
});
35963596

3597-
it('Correctly decorate the collection cursor count command with skip, limit, hint, readConcern', {
3597+
it.skip('Correctly decorate the collection count command with skip, limit, hint, readConcern', {
35983598
// Add a tag that our runner can trigger on
35993599
// in this case we are setting that node needs to be higher than 0.10.X to run
36003600
metadata: {

test/functional/retryable_reads.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ describe('Retryable Reads', function () {
2424
spec.description.match(/listCollections/i) ||
2525
spec.description.match(/listCollectionNames/i) ||
2626
spec.description.match(/estimatedDocumentCount/i) ||
27-
spec.description.match(/count/i) ||
27+
// FIXME(NODE-3074): uncomment when `count` spec tests have been fixed
28+
// spec.description.match(/count/i) ||
2829
spec.description.match(/find/i)
2930
);
3031
});

test/functional/transactions.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ describe('Transactions', function () {
102102
'commitTransaction retry succeeds on new mongos',
103103
'commitTransaction retry fails on new mongos',
104104
'unpin after transient error within a transaction and commit',
105-
// 'count',
105+
// FIXME(NODE-3074): unskip count tests when spec tests have been updated
106+
'count',
106107
// This test needs there to be multiple mongoses
107108
// 'increment txnNumber',
108109
// Skipping this until SPEC-1320 is resolved

test/functional/unified-spec-runner/entities.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ interface UnifiedChangeStream extends ChangeStream {
2727

2828
export type CommandEvent = CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent;
2929

30+
function serverApiConfig() {
31+
if (process.env.MONGODB_API_VERSION) {
32+
return { version: process.env.MONGODB_API_VERSION };
33+
}
34+
}
35+
36+
function getClient(address) {
37+
const serverApi = serverApiConfig();
38+
return new MongoClient(`mongodb://${address}`, serverApi ? { serverApi } : {});
39+
}
40+
3041
export class UnifiedMongoClient extends MongoClient {
3142
events: CommandEvent[];
3243
failPoints: Document[];
@@ -43,11 +54,7 @@ export class UnifiedMongoClient extends MongoClient {
4354
super(url, {
4455
monitorCommands: true,
4556
...description.uriOptions,
46-
serverApi: description.serverApi
47-
? description.serverApi
48-
: process.env.MONGODB_API_VERSION
49-
? { version: process.env.MONGODB_API_VERSION }
50-
: null
57+
serverApi: description.serverApi ? description.serverApi : serverApiConfig()
5158
});
5259
this.events = [];
5360
this.failPoints = [];
@@ -93,7 +100,7 @@ export class FailPointMap extends Map<string, Document> {
93100
} else {
94101
// create a new client
95102
address = addressOrClient.toString();
96-
client = new MongoClient(`mongodb://${address}`);
103+
client = getClient(address);
97104
await client.connect();
98105
}
99106

@@ -114,7 +121,7 @@ export class FailPointMap extends Map<string, Document> {
114121
const entries = Array.from(this.entries());
115122
await Promise.all(
116123
entries.map(async ([hostAddress, configureFailPoint]) => {
117-
const client = new MongoClient(`mongodb://${hostAddress}`);
124+
const client = getClient(hostAddress);
118125
await client.connect();
119126
const admin = client.db('admin');
120127
const result = await admin.command({ configureFailPoint, mode: 'off' });

test/functional/unified-spec-runner/operations.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ReadPreference } from '../../../src/read_preference';
66
import { WriteConcern } from '../../../src/write_concern';
77
import { Document, InsertOneOptions } from '../../../src';
88
import { EventCollector } from '../../tools/utils';
9-
import { EntitiesMap, UnifiedMongoClient } from './entities';
9+
import { EntitiesMap } from './entities';
1010
import { expectErrorCheck, resultCheck } from './match';
1111
import type { OperationDescription } from './schema';
1212
import { CommandStartedEvent } from '../../../src/cmap/command_monitoring_events';
@@ -384,7 +384,7 @@ operations.set('distinct', async ({ entities, operation }) => {
384384

385385
operations.set('estimatedDocumentCount', async ({ entities, operation }) => {
386386
const collection = entities.getEntity('collection', operation.object);
387-
return collection.estimatedDocumentCount();
387+
return collection.estimatedDocumentCount(operation.arguments);
388388
});
389389

390390
operations.set('findOneAndDelete', async ({ entities, operation }) => {

test/functional/versioned-api.test.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,7 @@ describe('Versioned API', function () {
2121
it(String(test.description), {
2222
metadata: { sessions: { skipLeakTests: true } },
2323
test: async function () {
24-
try {
25-
await runUnifiedTest(this, versionedApiTest, test);
26-
} catch (error) {
27-
if (error.message.includes('not implemented.')) {
28-
console.log(`${test.description}: was skipped due to missing functionality`);
29-
console.log(error.stack);
30-
this.skip();
31-
} else {
32-
throw error;
33-
}
34-
}
24+
await runUnifiedTest(this, versionedApiTest, test);
3525
}
3626
});
3727
}

0 commit comments

Comments
 (0)