Skip to content

Commit 1113e1b

Browse files
committed
feat(NODE-6472): run findOne without cursor
1 parent bff57ed commit 1113e1b

File tree

7 files changed

+437
-10
lines changed

7 files changed

+437
-10
lines changed

src/collection.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
type EstimatedDocumentCountOptions
4040
} from './operations/estimated_document_count';
4141
import { executeOperation } from './operations/execute_operation';
42-
import type { FindOptions } from './operations/find';
42+
import { type FindOneOptions, type FindOptions } from './operations/find';
4343
import {
4444
FindOneAndDeleteOperation,
4545
type FindOneAndDeleteOptions,
@@ -48,6 +48,7 @@ import {
4848
FindOneAndUpdateOperation,
4949
type FindOneAndUpdateOptions
5050
} from './operations/find_and_modify';
51+
import { FindOneOperation } from './operations/find_one_operation';
5152
import {
5253
CreateIndexesOperation,
5354
type CreateIndexesOptions,
@@ -507,25 +508,33 @@ export class Collection<TSchema extends Document = Document> {
507508
async findOne(filter: Filter<TSchema>): Promise<WithId<TSchema> | null>;
508509
async findOne(
509510
filter: Filter<TSchema>,
510-
options: Omit<FindOptions, 'timeoutMode'> & Abortable
511+
options: Omit<FindOneOptions, 'timeoutMode'> & Abortable
511512
): Promise<WithId<TSchema> | null>;
512513

513514
// allow an override of the schema.
514515
async findOne<T = TSchema>(): Promise<T | null>;
515516
async findOne<T = TSchema>(filter: Filter<TSchema>): Promise<T | null>;
516517
async findOne<T = TSchema>(
517518
filter: Filter<TSchema>,
518-
options?: Omit<FindOptions, 'timeoutMode'> & Abortable
519+
options?: Omit<FindOneOptions, 'timeoutMode'> & Abortable
519520
): Promise<T | null>;
520521

521522
async findOne(
522523
filter: Filter<TSchema> = {},
523-
options: FindOptions & Abortable = {}
524+
options: Omit<FindOneOptions, ''> & Abortable = {}
524525
): Promise<WithId<TSchema> | null> {
525-
const cursor = this.find(filter, options).limit(-1).batchSize(1);
526-
const res = await cursor.next();
527-
await cursor.close();
528-
return res;
526+
//const cursor = this.find(filter, options).limit(-1).batchSize(1);
527+
//const res = await cursor.next();
528+
//await cursor.close();
529+
return await executeOperation(
530+
this.client,
531+
new FindOneOperation(
532+
this.s.db,
533+
this.collectionName,
534+
filter,
535+
resolveOptions(this as TODO_NODE_3286, options)
536+
)
537+
);
529538
}
530539

531540
/**

src/operations/find.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ export interface FindOptions<TSchema extends Document = Document>
8181
timeoutMode?: CursorTimeoutMode;
8282
}
8383

84+
/** @public */
85+
export interface FindOneOptions extends FindOptions {
86+
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
87+
batchSize?: number;
88+
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
89+
limit?: number;
90+
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
91+
noCursorTimeout?: boolean;
92+
}
93+
8494
/** @internal */
8595
export class FindOperation extends CommandOperation<CursorResponse> {
8696
/**
@@ -142,7 +152,11 @@ export class FindOperation extends CommandOperation<CursorResponse> {
142152
}
143153
}
144154

145-
function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOptions): Document {
155+
export function makeFindCommand(
156+
ns: MongoDBNamespace,
157+
filter: Document,
158+
options: FindOptions
159+
): Document {
146160
const findCommand: Document = {
147161
find: ns.collection,
148162
filter
@@ -195,7 +209,13 @@ function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOp
195209

196210
findCommand.singleBatch = true;
197211
} else {
198-
findCommand.batchSize = options.batchSize;
212+
if (options.batchSize === options.limit) {
213+
// Spec dictates that if these are equal the batchSize should be one more than the
214+
// limit to avoid leaving the cursor open.
215+
findCommand.batchSize = options.batchSize + 1;
216+
} else {
217+
findCommand.batchSize = options.batchSize;
218+
}
199219
}
200220
}
201221

src/operations/find_one_operation.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { type Db } from '..';
2+
import { type Document, pluckBSONSerializeOptions } from '../bson';
3+
import { type OnDemandDocumentDeserializeOptions } from '../cmap/wire_protocol/on_demand/document';
4+
import type { Server } from '../sdam/server';
5+
import type { ClientSession } from '../sessions';
6+
import { type TimeoutContext } from '../timeout';
7+
import { MongoDBNamespace } from '../utils';
8+
import { CommandOperation } from './command';
9+
import { type FindOptions, makeFindCommand } from './find';
10+
import { Aspect, defineAspects } from './operation';
11+
12+
/** @public */
13+
export interface FindOneOptions extends FindOptions {
14+
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
15+
batchSize?: number;
16+
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
17+
limit?: number;
18+
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
19+
noCursorTimeout?: boolean;
20+
}
21+
22+
/** @internal */
23+
export class FindOneOperation<TSchema = any> extends CommandOperation<TSchema> {
24+
override options: FindOneOptions;
25+
/** @internal */
26+
private namespace: MongoDBNamespace;
27+
/** @internal */
28+
private filter: Document;
29+
/** @internal */
30+
protected deserializationOptions: OnDemandDocumentDeserializeOptions;
31+
32+
constructor(db: Db, collectionName: string, filter: Document, options: FindOneOptions = {}) {
33+
super(db, options);
34+
this.namespace = new MongoDBNamespace(db.databaseName, collectionName);
35+
this.filter = filter;
36+
this.options = { ...options };
37+
this.deserializationOptions = {
38+
...pluckBSONSerializeOptions(options),
39+
validation: {
40+
utf8: options?.enableUtf8Validation === false ? false : true
41+
}
42+
};
43+
}
44+
45+
override get commandName() {
46+
return 'find' as const;
47+
}
48+
49+
override async execute(
50+
server: Server,
51+
session: ClientSession | undefined,
52+
timeoutContext: TimeoutContext
53+
): Promise<TSchema> {
54+
const command: Document = makeFindCommand(this.namespace, this.filter, this.options);
55+
// Explicitly set the limit to 1 and singleBatch to true for all commands, per the spec.
56+
// noCursorTimeout must be unset as well as batchSize.
57+
// See: https://github.com/mongodb/specifications/blob/master/source/crud/crud.md#findone-api-details
58+
command.limit = 1;
59+
command.singleBatch = true;
60+
if (command.noCursorTimeout != null) {
61+
delete command.noCursorTimeout;
62+
}
63+
if (command.batchSize != null) {
64+
delete command.batchSize;
65+
}
66+
67+
const response = await super.executeCommand(server, session, command, timeoutContext);
68+
// In this case since we are just running a command, the response is a document with
69+
// a single batch cursor, not an OnDemandDocument.
70+
const document = response.cursor?.firstBatch?.[0] ?? null;
71+
return document;
72+
}
73+
}
74+
75+
defineAspects(FindOneOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]);

test/spec/crud/unified/find.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,68 @@
237237
]
238238
}
239239
]
240+
},
241+
{
242+
"description": "Find with batchSize equal to limit",
243+
"operations": [
244+
{
245+
"object": "collection0",
246+
"name": "find",
247+
"arguments": {
248+
"filter": {
249+
"_id": {
250+
"$gt": 1
251+
}
252+
},
253+
"sort": {
254+
"_id": 1
255+
},
256+
"limit": 4,
257+
"batchSize": 4
258+
},
259+
"expectResult": [
260+
{
261+
"_id": 2,
262+
"x": 22
263+
},
264+
{
265+
"_id": 3,
266+
"x": 33
267+
},
268+
{
269+
"_id": 4,
270+
"x": 44
271+
},
272+
{
273+
"_id": 5,
274+
"x": 55
275+
}
276+
]
277+
}
278+
],
279+
"expectEvents": [
280+
{
281+
"client": "client0",
282+
"events": [
283+
{
284+
"commandStartedEvent": {
285+
"command": {
286+
"find": "coll0",
287+
"filter": {
288+
"_id": {
289+
"$gt": 1
290+
}
291+
},
292+
"limit": 4,
293+
"batchSize": 5
294+
},
295+
"commandName": "find",
296+
"databaseName": "find-tests"
297+
}
298+
}
299+
]
300+
}
301+
]
240302
}
241303
]
242304
}

test/spec/crud/unified/find.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,31 @@ tests:
105105
- { _id: 2, x: 22 }
106106
- { _id: 3, x: 33 }
107107
- { _id: 4, x: 44 }
108+
-
109+
description: 'Find with batchSize equal to limit'
110+
operations:
111+
-
112+
object: *collection0
113+
name: find
114+
arguments:
115+
filter: { _id: { $gt: 1 } }
116+
sort: { _id: 1 }
117+
limit: 4
118+
batchSize: 4
119+
expectResult:
120+
- { _id: 2, x: 22 }
121+
- { _id: 3, x: 33 }
122+
- { _id: 4, x: 44 }
123+
- { _id: 5, x: 55 }
124+
expectEvents:
125+
- client: *client0
126+
events:
127+
- commandStartedEvent:
128+
command:
129+
find: *collection0Name
130+
filter: { _id: { $gt: 1 } }
131+
limit: 4
132+
# Drivers use limit + 1 for batchSize to ensure the server closes the cursor
133+
batchSize: 5
134+
commandName: find
135+
databaseName: *database0Name

0 commit comments

Comments
 (0)