Skip to content
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

feat: add explain support for non-cursor commands #2599

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a8dfabe
FindOneOperation extends CommandOperation
HanaPearlman Oct 30, 2020
5bded70
implement explain for write commands
HanaPearlman Oct 27, 2020
4374b6b
implement explain for distinct command
HanaPearlman Oct 27, 2020
f00fbfe
implement explain for findAndModify commands
HanaPearlman Oct 27, 2020
1d937a8
implement explain for mapReduce command
HanaPearlman Oct 27, 2020
2a9b1be
first attempt: handle removing sessions for write operations
HanaPearlman Oct 28, 2020
237a70f
consider explain during canRetryWrite
HanaPearlman Oct 28, 2020
c2d0a53
small enum cleanup
HanaPearlman Oct 28, 2020
0126381
model explain after read concern
HanaPearlman Oct 29, 2020
82715bd
create ExplainableCommand class
HanaPearlman Oct 30, 2020
3bf0d78
check explain value in explain command constructor
HanaPearlman Nov 2, 2020
7090c39
quick cursor fix
HanaPearlman Nov 2, 2020
82f0e60
move explain cmd/options to separate file
HanaPearlman Nov 2, 2020
f5e65ed
some commenting
HanaPearlman Nov 3, 2020
4bdf503
respond to comments
HanaPearlman Nov 5, 2020
03333d7
test bug fix
HanaPearlman Nov 9, 2020
20ef5da
Merge branch 'master' into NODE-2852/master/explain-non-cursor
HanaPearlman Nov 9, 2020
c1a4ff9
add explain-related exports to index
HanaPearlman Nov 11, 2020
2c7334b
use aspects and throw from fromOptions
HanaPearlman Nov 12, 2020
d486214
use expanded explain types for clarity
HanaPearlman Nov 12, 2020
ddc9826
change test names and ordering
HanaPearlman Nov 12, 2020
6a17656
clean up
HanaPearlman Nov 12, 2020
7235fb7
Merge branch 'master' into NODE-2852/master/explain-non-cursor
HanaPearlman Nov 12, 2020
001ec2e
fix explain export in index
HanaPearlman Nov 12, 2020
0b37761
check explain supported in individual op classes
HanaPearlman Nov 13, 2020
31bb9eb
Merge branch 'master' into NODE-2852/master/explain-non-cursor
HanaPearlman Nov 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/cmap/wire_protocol/write_command.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { MongoError } from '../../error';
import { collectionNamespace, Callback } from '../../utils';
import { collectionNamespace, Callback, decorateWithExplain } from '../../utils';
import { command, CommandOptions } from './command';
import type { Server } from '../../sdam/server';
import type { Document, BSONSerializeOptions } from '../../bson';
import type { WriteConcern } from '../../write_concern';
import { Explain, ExplainOptions } from '../../explain';

/** @public */
export interface CollationOptions {
Expand All @@ -18,7 +19,7 @@ export interface CollationOptions {
}

/** @internal */
export interface WriteCommandOptions extends BSONSerializeOptions, CommandOptions {
export interface WriteCommandOptions extends BSONSerializeOptions, CommandOptions, ExplainOptions {
ordered?: boolean;
writeConcern?: WriteConcern;
collation?: CollationOptions;
Expand All @@ -43,7 +44,7 @@ export function writeCommand(
options = options || {};
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
const writeConcern = options.writeConcern;
const writeCommand: Document = {};
let writeCommand: Document = {};
writeCommand[type] = collectionNamespace(ns);
writeCommand[opsField] = ops;
writeCommand.ordered = ordered;
Expand All @@ -64,6 +65,13 @@ export function writeCommand(
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
}

// If a command is to be explained, we need to reformat the command after
// the other command properties are specified.
const explain = Explain.fromOptions(options);
if (explain) {
writeCommand = decorateWithExplain(writeCommand, explain);
}

const commandOptions = Object.assign(
{
checkKeys: type === 'insert',
Expand Down
10 changes: 6 additions & 4 deletions src/cursor/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1307,10 +1307,12 @@ export class Cursor<
explain(callback?: Callback): Promise<unknown> | void {
// NOTE: the next line includes a special case for operations which do not
// subclass `CommandOperationV2`. To be removed asap.
if (this.operation && this.operation.cmd == null) {
this.operation.options.explain = true;
return executeOperation(this.topology, this.operation as any, callback);
}
// TODO NODE-2853: This had to be removed during NODE-2852; fix while re-implementing
// cursor explain
// if (this.operation && this.operation.cmd == null) {
// this.operation.options.explain = true;
// return executeOperation(this.topology, this.operation as any, callback);
// }

this.cmd.explain = true;

Expand Down
48 changes: 48 additions & 0 deletions src/explain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { MongoError } from './error';

/** @public */
export const ExplainVerbosity = {
queryPlanner: 'queryPlanner',
queryPlannerExtended: 'queryPlannerExtended',
executionStats: 'executionStats',
allPlansExecution: 'allPlansExecution'
} as const;

/**
* For backwards compatibility, true is interpreted as
* "allPlansExecution" and false as "queryPlanner".
* @public
*/
export type ExplainVerbosityLike = keyof typeof ExplainVerbosity | boolean;

/** @public */
export interface ExplainOptions {
/** Specifies the verbosity mode for the explain output. */
explain?: ExplainVerbosityLike;
}

/** @internal */
export class Explain {
verbosity: keyof typeof ExplainVerbosity;

constructor(verbosity: ExplainVerbosityLike) {
if (typeof verbosity === 'boolean') {
this.verbosity = verbosity
? ExplainVerbosity.allPlansExecution
: ExplainVerbosity.queryPlanner;
} else {
this.verbosity = ExplainVerbosity[verbosity];
}
}

static fromOptions(options?: ExplainOptions): Explain | undefined {
if (options?.explain === undefined) return;

const explain = options.explain;
if (typeof explain === 'boolean' || explain in ExplainVerbosity) {
return new Explain(explain);
}

throw new MongoError(`explain must be one of ${Object.keys(ExplainVerbosity)} or a boolean`);
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export type {
export type { DbPrivate, DbOptions } from './db';
export type { AutoEncryptionOptions, AutoEncryptionLoggerLevels, AutoEncrypter } from './deps';
export type { AnyError, ErrorDescription } from './error';
export type { ExplainOptions, ExplainVerbosity, ExplainVerbosityLike } from './explain';
export type {
GridFSBucketReadStream,
GridFSBucketReadStreamOptions,
Expand Down
2 changes: 0 additions & 2 deletions src/operations/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export interface AggregateOptions extends CommandOperationOptions {
bypassDocumentValidation?: boolean;
/** Return the query as cursor, on 2.6 \> it returns as a real cursor on pre 2.6 it returns as an emulated cursor. */
cursor?: Document;
/** Explain returns the aggregation execution plan (requires mongodb 2.6 \>) */
explain?: boolean;
/** specifies a cumulative time limit in milliseconds for processing operations on the cursor. MongoDB interrupts the operation at the earliest following interrupt point. */
maxTimeMS?: number;
/** The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. */
Expand Down
28 changes: 24 additions & 4 deletions src/operations/command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Aspect, OperationBase, OperationOptions } from './operation';
import { ReadConcern } from '../read_concern';
import { WriteConcern, WriteConcernOptions } from '../write_concern';
import { maxWireVersion, MongoDBNamespace, Callback } from '../utils';
import { maxWireVersion, MongoDBNamespace, Callback, decorateWithExplain } from '../utils';
import type { ReadPreference } from '../read_preference';
import { commandSupportsReadConcern } from '../sessions';
import { MongoError } from '../error';
Expand All @@ -10,11 +10,15 @@ import type { Server } from '../sdam/server';
import type { BSONSerializeOptions, Document } from '../bson';
import type { CollationOptions } from '../cmap/wire_protocol/write_command';
import type { ReadConcernLike } from './../read_concern';
import { Explain, ExplainOptions } from '../explain';

const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;

/** @public */
export interface CommandOperationOptions extends OperationOptions, WriteConcernOptions {
export interface CommandOperationOptions
extends OperationOptions,
WriteConcernOptions,
ExplainOptions {
/** Return the full server response for the command */
fullResponse?: boolean;
/** Specify a read concern and level for the collection. (only MongoDB 3.2 or higher supported) */
Expand Down Expand Up @@ -51,7 +55,7 @@ export abstract class CommandOperation<
ns: MongoDBNamespace;
readConcern?: ReadConcern;
writeConcern?: WriteConcern;
explain: boolean;
explain?: Explain;
fullResponse?: boolean;
logger?: Logger;

Expand All @@ -73,14 +77,26 @@ export abstract class CommandOperation<
this.readConcern = ReadConcern.fromOptions(options);
this.writeConcern = WriteConcern.fromOptions(options);

this.explain = false;
this.fullResponse =
options && typeof options.fullResponse === 'boolean' ? options.fullResponse : false;

// TODO(NODE-2056): make logger another "inheritable" property
if (parent && parent.logger) {
this.logger = parent.logger;
}

if (this.hasAspect(Aspect.EXPLAINABLE)) {
this.explain = Explain.fromOptions(options);
} else if (options?.explain !== undefined) {
throw new MongoError(`explain is not supported on this command`);
}
}

get canRetryWrite(): boolean {
if (this.hasAspect(Aspect.EXPLAINABLE)) {
return this.explain === undefined;
}
return true;
}

abstract execute(server: Server, callback: Callback<TResult>): void;
Expand Down Expand Up @@ -128,6 +144,10 @@ export abstract class CommandOperation<
this.logger.debug(`executing command ${JSON.stringify(cmd)} against ${this.ns}`);
}

if (this.hasAspect(Aspect.EXPLAINABLE) && this.explain) {
cmd = decorateWithExplain(cmd, this.explain);
}

server.command(
this.ns.toString(),
cmd,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a stylistic nit, but I think it'd be better to do cmd.explain ? decorateWithExplain(cmd, cmd.explain) : cmd here rather than reassigning the cmd parameter.

Expand Down
20 changes: 19 additions & 1 deletion src/operations/common_functions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { MongoError } from '../error';
import { applyRetryableWrites, decorateWithCollation, Callback, getTopology } from '../utils';
import {
applyRetryableWrites,
decorateWithCollation,
Callback,
getTopology,
maxWireVersion
} from '../utils';
import type { Document } from '../bson';
import type { Db } from '../db';
import type { ClientSession } from '../sessions';
Expand Down Expand Up @@ -155,6 +161,12 @@ export function removeDocuments(
return callback ? callback(err, null) : undefined;
}

if (options.explain !== undefined && maxWireVersion(server) < 3) {
return callback
? callback(new MongoError(`server ${server.name} does not support explain on remove`))
: undefined;
}

// Execute the remove
server.remove(
coll.s.namespace.toString(),
Expand Down Expand Up @@ -240,6 +252,12 @@ export function updateDocuments(
return callback(err, null);
}

if (options.explain !== undefined && maxWireVersion(server) < 3) {
return callback
? callback(new MongoError(`server ${server.name} does not support explain on update`))
: undefined;
}

// Update options
server.update(
coll.s.namespace.toString(),
Expand Down
4 changes: 2 additions & 2 deletions src/operations/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@ export class DeleteManyOperation extends CommandOperation<DeleteOptions, DeleteR
}

defineAspects(DeleteOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION]);
defineAspects(DeleteOneOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION]);
defineAspects(DeleteManyOperation, [Aspect.WRITE_OPERATION]);
defineAspects(DeleteOneOperation, [Aspect.RETRYABLE, Aspect.WRITE_OPERATION, Aspect.EXPLAINABLE]);
defineAspects(DeleteManyOperation, [Aspect.WRITE_OPERATION, Aspect.EXPLAINABLE]);
12 changes: 9 additions & 3 deletions src/operations/distinct.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Aspect, defineAspects } from './operation';
import { CommandOperation, CommandOperationOptions } from './command';
import { decorateWithCollation, decorateWithReadConcern, Callback } from '../utils';
import { decorateWithCollation, decorateWithReadConcern, Callback, maxWireVersion } from '../utils';
import type { Document } from '../bson';
import type { Server } from '../sdam/server';
import type { Collection } from '../collection';
import { MongoError } from '../error';

/** @public */
export type DistinctOptions = CommandOperationOptions;
Expand Down Expand Up @@ -63,15 +64,20 @@ export class DistinctOperation extends CommandOperation<DistinctOptions, Documen
return callback(err);
}

if (this.explain && maxWireVersion(server) < 4) {
callback(new MongoError(`server ${server.name} does not support explain on distinct`));
return;
}

super.executeCommand(server, cmd, (err, result) => {
if (err) {
callback(err);
return;
}

callback(undefined, this.options.fullResponse ? result : result.values);
callback(undefined, this.options.fullResponse || this.explain ? result : result.values);
});
}
}

defineAspects(DistinctOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]);
defineAspects(DistinctOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE, Aspect.EXPLAINABLE]);
2 changes: 0 additions & 2 deletions src/operations/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export interface FindOptions extends QueryOptions, CommandOperationOptions {
skip?: number;
/** Tell the query to use specific indexes in the query. Object of indexes to use, `{'_id':1}` */
hint?: Hint;
/** Explain the query instead of returning the data. */
explain?: boolean;
/** Specify if the cursor can timeout. */
timeout?: boolean;
/** Specify if the cursor is tailable. */
Expand Down
11 changes: 10 additions & 1 deletion src/operations/find_and_modify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export class FindAndModifyOperation extends CommandOperation<FindAndModifyOption
cmd.hint = options.hint;
}

if (this.explain && maxWireVersion(server) < 4) {
callback(new MongoError(`server ${server.name} does not support explain on findAndModify`));
return;
}

// Execute the command
super.executeCommand(server, cmd, (err, result) => {
if (err) return callback(err);
Expand Down Expand Up @@ -229,4 +234,8 @@ export class FindOneAndUpdateOperation extends FindAndModifyOperation {
}
}

defineAspects(FindAndModifyOperation, [Aspect.WRITE_OPERATION, Aspect.RETRYABLE]);
defineAspects(FindAndModifyOperation, [
Aspect.WRITE_OPERATION,
Aspect.RETRYABLE,
Aspect.EXPLAINABLE
]);
15 changes: 14 additions & 1 deletion src/operations/map_reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
decorateWithCollation,
decorateWithReadConcern,
isObject,
Callback
Callback,
maxWireVersion
} from '../utils';
import { ReadPreference, ReadPreferenceMode } from '../read_preference';
import { CommandOperation, CommandOperationOptions } from './command';
Expand All @@ -14,8 +15,10 @@ import type { Collection } from '../collection';
import type { Sort } from '../sort';
import { MongoError } from '../error';
import type { ObjectId } from '../bson';
import { Aspect, defineAspects } from './operation';

const exclusionList = [
'explain',
'readPreference',
'readConcern',
'session',
Expand Down Expand Up @@ -156,6 +159,11 @@ export class MapReduceOperation extends CommandOperation<MapReduceOptions, Docum
return callback(err);
}

if (this.explain && maxWireVersion(server) < 9) {
callback(new MongoError(`server ${server.name} does not support explain on mapReduce`));
return;
}

// Execute command
super.executeCommand(server, mapCommandHash, (err, result) => {
if (err) return callback(err);
Expand All @@ -164,6 +172,9 @@ export class MapReduceOperation extends CommandOperation<MapReduceOptions, Docum
return callback(new MongoError(result));
}

// If an explain option was executed, don't process the server results
if (this.explain) return callback(undefined, result);

// Create statistics value
const stats: MapReduceStats = {};
if (result.timeMillis) stats['processtime'] = result.timeMillis;
Expand Down Expand Up @@ -227,3 +238,5 @@ function processScope(scope: Document | ObjectId) {

return newScope;
}

defineAspects(MapReduceOperation, [Aspect.EXPLAINABLE]);
5 changes: 2 additions & 3 deletions src/operations/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { Server } from '../sdam/server';
export const Aspect = {
READ_OPERATION: Symbol('READ_OPERATION'),
WRITE_OPERATION: Symbol('WRITE_OPERATION'),
RETRYABLE: Symbol('RETRYABLE')
RETRYABLE: Symbol('RETRYABLE'),
EXPLAINABLE: Symbol('EXPLAINABLE')
} as const;

/** @public */
Expand All @@ -21,8 +22,6 @@ export interface OperationConstructor extends Function {
export interface OperationOptions extends BSONSerializeOptions {
/** Specify ClientSession for this command */
session?: ClientSession;

explain?: boolean;
willRetryWrites?: boolean;

/** The preferred read preference (ReadPreference.primary, ReadPreference.primary_preferred, ReadPreference.secondary, ReadPreference.secondary_preferred, ReadPreference.nearest). */
Expand Down