-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(NODE-5019): add runCursorCommand API #3655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
514f8b3
3298b88
3da03ad
e0626ad
4c23490
e778767
408bbe7
4ae5514
f3f9de1
a91d792
f7d8653
bd7bcc3
f52c98c
ecef528
6bee14b
6ace185
482f816
b95b5c1
9e94b16
d1a3d43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import type { BSONSerializeOptions, Document, Long } from '../bson'; | ||
import type { Db } from '../db'; | ||
import { MongoAPIError, MongoUnexpectedServerResponseError } from '../error'; | ||
import { executeOperation, ExecutionResult } from '../operations/execute_operation'; | ||
import { GetMoreOperation } from '../operations/get_more'; | ||
import { RunCommandOperation } from '../operations/run_command'; | ||
import type { ReadConcernLike } from '../read_concern'; | ||
import type { ReadPreferenceLike } from '../read_preference'; | ||
import type { ClientSession } from '../sessions'; | ||
import { Callback, ns } from '../utils'; | ||
import { AbstractCursor } from './abstract_cursor'; | ||
|
||
/** @public */ | ||
export type RunCursorCommandOptions = { | ||
readPreference?: ReadPreferenceLike; | ||
session?: ClientSession; | ||
} & BSONSerializeOptions; | ||
|
||
/** @internal */ | ||
type RunCursorCommandResponse = { | ||
cursor: { id: bigint | Long | number; ns: string; firstBatch: Document[] }; | ||
ok: 1; | ||
}; | ||
|
||
/** @public */ | ||
export class RunCommandCursor extends AbstractCursor { | ||
public readonly command: Readonly<Record<string, any>>; | ||
public readonly getMoreOptions: { | ||
comment?: any; | ||
maxAwaitTimeMS?: number; | ||
batchSize?: number; | ||
} = {}; | ||
|
||
/** | ||
* Controls the `getMore.comment` field | ||
* @param comment - any BSON value | ||
*/ | ||
public setComment(comment: any): this { | ||
this.getMoreOptions.comment = comment; | ||
return this; | ||
} | ||
|
||
/** | ||
* Controls the `getMore.maxTimeMS` field. Only valid when cursor is tailable await | ||
* @param maxTimeMS - the number of milliseconds to wait for new data | ||
*/ | ||
public setMaxTimeMS(maxTimeMS: number): this { | ||
this.getMoreOptions.maxAwaitTimeMS = maxTimeMS; | ||
return this; | ||
} | ||
|
||
/** | ||
* Controls the `getMore.batchSize` field | ||
* @param maxTimeMS - the number documents to return in the `nextBatch` | ||
*/ | ||
public setBatchSize(batchSize: number): this { | ||
this.getMoreOptions.batchSize = batchSize; | ||
return this; | ||
} | ||
|
||
/** Unsupported for RunCommandCursor */ | ||
public override clone(): never { | ||
throw new MongoAPIError('Clone not supported, create a new cursor with db.runCursorCommand'); | ||
} | ||
|
||
/** Unsupported for RunCommandCursor: readConcern must be configured directly on command document */ | ||
public override withReadConcern(_: ReadConcernLike): never { | ||
throw new MongoAPIError( | ||
'RunCommandCursor does not support readConcern it must be attached to the command being run' | ||
); | ||
} | ||
|
||
/** Unsupported for RunCommandCursor: various cursor flags must be configured directly on command document */ | ||
public override addCursorFlag(_: string, __: boolean): never { | ||
throw new MongoAPIError( | ||
'RunCommandCursor does not support cursor flags, they must be attached to the command being run' | ||
); | ||
} | ||
|
||
/** Unsupported for RunCommandCursor: maxTimeMS must be configured directly on command document */ | ||
public override maxTimeMS(_: number): never { | ||
throw new MongoAPIError( | ||
'maxTimeMS must be configured on the command document directly, to configure getMore.maxTimeMS use cursor.setMaxTimeMS()' | ||
); | ||
} | ||
|
||
/** Unsupported for RunCommandCursor: batchSize must be configured directly on command document */ | ||
public override batchSize(_: number): never { | ||
throw new MongoAPIError( | ||
'batchSize must be configured on the command document directly, to configure getMore.batchSize use cursor.setBatchSize()' | ||
); | ||
} | ||
|
||
/** @internal */ | ||
private db: Db; | ||
|
||
/** @internal */ | ||
constructor(db: Db, command: Document, options: RunCursorCommandOptions = {}) { | ||
super(db.s.client, ns(db.namespace), options); | ||
this.db = db; | ||
this.command = Object.freeze({ ...command }); | ||
} | ||
|
||
/** @internal */ | ||
protected _initialize(session: ClientSession, callback: Callback<ExecutionResult>) { | ||
const operation = new RunCommandOperation<RunCursorCommandResponse>(this.db, this.command, { | ||
...this.cursorOptions, | ||
session: session, | ||
readPreference: this.cursorOptions.readPreference | ||
}); | ||
executeOperation(this.client, operation).then( | ||
response => { | ||
if (response.cursor == null) { | ||
callback( | ||
new MongoUnexpectedServerResponseError('Expected server to respond with cursor') | ||
); | ||
return; | ||
} | ||
callback(undefined, { | ||
server: operation.server, | ||
session, | ||
response | ||
}); | ||
}, | ||
err => callback(err) | ||
); | ||
} | ||
|
||
/** @internal */ | ||
override _getMore(_batchSize: number, callback: Callback<Document>) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const getMoreOperation = new GetMoreOperation(this.namespace, this.id!, this.server!, { | ||
...this.cursorOptions, | ||
session: this.session, | ||
...this.getMoreOptions | ||
}); | ||
|
||
executeOperation(this.client, getMoreOperation, callback); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { expect } from 'chai'; | ||
|
||
import { Db, MongoClient } from '../../mongodb'; | ||
|
||
describe('runCursorCommand API', () => { | ||
let client: MongoClient; | ||
let db: Db; | ||
|
||
beforeEach(async function () { | ||
client = this.configuration.newClient({}, { monitorCommands: true }); | ||
db = client.db(); | ||
await db.dropDatabase().catch(() => null); | ||
await db | ||
.collection<{ _id: number }>('collection') | ||
.insertMany([{ _id: 0 }, { _id: 1 }, { _id: 2 }]); | ||
}); | ||
|
||
afterEach(async function () { | ||
await client.close(); | ||
}); | ||
|
||
it('returns each document only once across multiple iterators', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this test be on Why was it added now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The functionality is in the AbstractCursor but as an integration test I'm using the public API to reproduce the expected behavior. I was curious what the behavior of multiple iterators (or nested for-await loops) from one cursor is. Returning a document only once regardless of how many iterators there are is supported by nature of our dequeuing from a List. Since it is a data structure we have changed in the past it is possible that we could modify this behavior if, say, we used an offset into a List that is specific to each iterator. |
||
const cursor = db.runCursorCommand({ find: 'collection', filter: {}, batchSize: 1 }); | ||
cursor.setBatchSize(1); | ||
|
||
const a = cursor[Symbol.asyncIterator](); | ||
const b = cursor[Symbol.asyncIterator](); | ||
|
||
// Interleaving calls to A and B | ||
const results = [ | ||
await a.next(), // find, first doc | ||
await b.next(), // getMore, second doc | ||
|
||
await a.next(), // getMore, third doc | ||
await b.next(), // getMore, no doc & exhausted id, a.k.a. done | ||
|
||
await a.next(), // done | ||
await b.next() // done | ||
]; | ||
|
||
expect(results).to.deep.equal([ | ||
{ value: { _id: 0 }, done: false }, | ||
{ value: { _id: 1 }, done: false }, | ||
{ value: { _id: 2 }, done: false }, | ||
{ value: undefined, done: true }, | ||
{ value: undefined, done: true }, | ||
{ value: undefined, done: true } | ||
]); | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.