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 17 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/cmap/wire_protocol/write_command.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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 } from '../../explain';
import type { ExplainOptions } from '../../operations/explainable_command';

/** @public */
export interface CollationOptions {
Expand All @@ -18,7 +20,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 +45,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 +66,15 @@ 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.
if (options.explain !== undefined) {
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
71 changes: 71 additions & 0 deletions src/explain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Document } from '.';
import type { ExplainOptions } from './operations/explainable_command';
import type { Server } from './sdam/server';
import { maxWireVersion } from './utils';

export const Verbosity = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: can we expand Verbosity to ExplainVerbosity for clarity?

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 VerbosityLike = keyof typeof Verbosity | boolean;

// Minimum server versions which support explain with specific operations
const SUPPORTS_EXPLAIN_WITH_REMOVE = 3;
const SUPPORTS_EXPLAIN_WITH_UPDATE = 3;
const SUPPORTS_EXPLAIN_WITH_DISTINCT = 4;
const SUPPORTS_EXPLAIN_WITH_FIND_AND_MODIFY = 4;
const SUPPORTS_EXPLAIN_WITH_MAP_REDUCE = 9;

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

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

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

static valid(options?: ExplainOptions): boolean {
if (options?.explain === undefined) {
return true;
}
const explain = options.explain;
return typeof explain === 'boolean' || explain in Verbosity;
}

/** Checks that the server supports explain on the given operation or command.*/
static explainSupported(server: Server, op: string | Document): boolean {
const wireVersion = maxWireVersion(server);
if (op === 'remove' || (typeof op === 'object' && op.remove)) {
return wireVersion >= SUPPORTS_EXPLAIN_WITH_REMOVE;
} else if (op === 'update' || (typeof op === 'object' && op.update)) {
return wireVersion >= SUPPORTS_EXPLAIN_WITH_UPDATE;
} else if (op === 'distinct' || (typeof op === 'object' && op.distinct)) {
return wireVersion >= SUPPORTS_EXPLAIN_WITH_DISTINCT;
} else if (op === 'findAndModify' || (typeof op === 'object' && op.findAndModify)) {
return wireVersion >= SUPPORTS_EXPLAIN_WITH_FIND_AND_MODIFY;
} else if (op === 'mapReduce' || (typeof op === 'object' && op.mapReduce)) {
return wireVersion >= SUPPORTS_EXPLAIN_WITH_MAP_REDUCE;
}

return false;
}
}
8 changes: 4 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 { ReadPreference, ReadPreferenceLike } from '../read_preference';
import { commandSupportsReadConcern } from '../sessions';
import { MongoError } from '../error';
Expand Down Expand Up @@ -54,7 +54,6 @@ export abstract class CommandOperation<
readPreference: ReadPreference;
readConcern?: ReadConcern;
writeConcern?: WriteConcern;
explain: boolean;
fullResponse?: boolean;
logger?: Logger;

Expand All @@ -79,7 +78,6 @@ export abstract class CommandOperation<
: ReadPreference.resolve(propertyProvider, this.options);
this.readConcern = resolveReadConcern(propertyProvider, this.options);
this.writeConcern = resolveWriteConcern(propertyProvider, this.options);
this.explain = false;
this.fullResponse =
options && typeof options.fullResponse === 'boolean' ? options.fullResponse : false;

Expand Down Expand Up @@ -141,9 +139,11 @@ export abstract class CommandOperation<
this.logger.debug(`executing command ${JSON.stringify(cmd)} against ${this.ns}`);
}

// If a command is to be explained, we need to reformat the command after
// the other command properties are specified.
server.command(
this.ns.toString(),
cmd,
cmd.explain ? decorateWithExplain(cmd, cmd.explain) : cmd,
{ fullResult: !!this.fullResponse, ...this.options },
callback
);
Expand Down
9 changes: 5 additions & 4 deletions src/operations/delete.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { defineAspects, Aspect, OperationBase, Hint } from './operation';
import { removeDocuments } from './common_functions';
import { CommandOperation, CommandOperationOptions } from './command';
import { isObject } from 'util';
import type { Callback, MongoDBNamespace } from '../utils';
import type { Document } from '../bson';
import type { Server } from '../sdam/server';
import type { Collection } from '../collection';
import type { WriteCommandOptions } from '../cmap/wire_protocol/write_command';
import type { Connection } from '../cmap/connection';
import { ExplainableCommand, ExplainOptions } from '../operations/explainable_command';
import type { CommandOperationOptions } from './command';

/** @public */
export interface DeleteOptions extends CommandOperationOptions {
export interface DeleteOptions extends CommandOperationOptions, ExplainOptions {
single?: boolean;
hint?: Hint;
}
Expand Down Expand Up @@ -51,7 +52,7 @@ export class DeleteOperation extends OperationBase<DeleteOptions, Document> {
}
}

export class DeleteOneOperation extends CommandOperation<DeleteOptions, DeleteResult> {
export class DeleteOneOperation extends ExplainableCommand<DeleteOptions, DeleteResult> {
collection: Collection;
filter: Document;

Expand Down Expand Up @@ -81,7 +82,7 @@ export class DeleteOneOperation extends CommandOperation<DeleteOptions, DeleteRe
}
}

export class DeleteManyOperation extends CommandOperation<DeleteOptions, DeleteResult> {
export class DeleteManyOperation extends ExplainableCommand<DeleteOptions, DeleteResult> {
collection: Collection;
filter: Document;

Expand Down
9 changes: 5 additions & 4 deletions src/operations/distinct.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Aspect, defineAspects } from './operation';
import { CommandOperation, CommandOperationOptions } from './command';
import { decorateWithCollation, decorateWithReadConcern, Callback } from '../utils';
import type { Document } from '../bson';
import type { Server } from '../sdam/server';
import type { Collection } from '../collection';
import { ExplainableCommand, ExplainOptions } from '../operations/explainable_command';
import type { CommandOperationOptions } from './command';

/** @public */
export type DistinctOptions = CommandOperationOptions;
export interface DistinctOptions extends CommandOperationOptions, ExplainOptions {}

/**
* Return a list of distinct values for the given key across a collection.
* @internal
*/
export class DistinctOperation extends CommandOperation<DistinctOptions, Document[]> {
export class DistinctOperation extends ExplainableCommand<DistinctOptions, Document[]> {
collection: Collection;
/** Field of the document to find distinct values for. */
key: string;
Expand Down Expand Up @@ -69,7 +70,7 @@ export class DistinctOperation extends CommandOperation<DistinctOptions, Documen
return;
}

callback(undefined, this.options.fullResponse ? result : result.values);
callback(undefined, this.options.fullResponse || this.explain ? result : result.values);
});
}
}
Expand Down
44 changes: 44 additions & 0 deletions src/operations/explainable_command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CommandOperation, OperationParent, CommandOperationOptions } from './command';
import { Explain, Verbosity, VerbosityLike } from '../explain';
import { Callback, Document, MongoError, Server } from '..';

/** @public */
export interface ExplainOptions {
explain?: VerbosityLike;
}

/** @internal */
export abstract class ExplainableCommand<
T extends ExplainOptions & CommandOperationOptions,
TResult = Document
> extends CommandOperation<T, TResult> {
explain?: Explain;
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved

constructor(parent?: OperationParent, options?: T) {
super(parent, options);

if (!Explain.valid(options)) {
throw new MongoError(`explain must be one of ${Object.keys(Verbosity)} or a boolean`);
}

this.explain = Explain.fromOptions(options);
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
}

get canRetryWrite(): boolean {
return this.explain === undefined;
}

executeCommand(server: Server, cmd: Document, callback: Callback): void {
if (this.explain) {
if (!Explain.explainSupported(server, cmd)) {
callback(new MongoError(`server ${server.name} does not support explain on this command`));
return;
}

// For now, tag the command with the explain; after cmd is finalized in the super class,
// it will be refactored into the required shape using the explain.
cmd.explain = this.explain;
}
super.executeCommand(server, cmd, callback);
}
}
7 changes: 4 additions & 3 deletions src/operations/find_and_modify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import {
Callback
} from '../utils';
import { MongoError } from '../error';
import { CommandOperation, CommandOperationOptions } from './command';
import { defineAspects, Aspect } from './operation';
import type { Document } from '../bson';
import type { Server } from '../sdam/server';
import type { Collection } from '../collection';
import { Sort, formatSort } from '../sort';
import { ExplainableCommand, ExplainOptions } from '../operations/explainable_command';
import type { CommandOperationOptions } from './command';

/** @public */
export interface FindAndModifyOptions extends CommandOperationOptions {
export interface FindAndModifyOptions extends CommandOperationOptions, ExplainOptions {
/** When false, returns the updated document rather than the original. The default is true. */
returnOriginal?: boolean;
/** Upsert the document if it does not exist. */
Expand All @@ -41,7 +42,7 @@ export interface FindAndModifyOptions extends CommandOperationOptions {
}

/** @internal */
export class FindAndModifyOperation extends CommandOperation<FindAndModifyOptions, Document> {
export class FindAndModifyOperation extends ExplainableCommand<FindAndModifyOptions, Document> {
collection: Collection;
query: Document;
sort?: Sort;
Expand Down
14 changes: 11 additions & 3 deletions src/operations/map_reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import {
Callback
} from '../utils';
import { ReadPreference, ReadPreferenceMode } from '../read_preference';
import { CommandOperation, CommandOperationOptions } from './command';
import type { Server } from '../sdam/server';
import type { Collection } from '../collection';
import type { Sort } from '../sort';
import { MongoError } from '../error';
import type { ObjectId } from '../bson';
import { ExplainableCommand, ExplainOptions } from '../operations/explainable_command';
import type { CommandOperationOptions } from './command';

const exclusionList = [
'explain',
'readPreference',
'session',
'bypassDocumentValidation',
Expand All @@ -34,7 +36,7 @@ export type ReduceFunction = (key: string, values: Document[]) => Document;
export type FinalizeFunction = (key: string, reducedValue: Document) => Document;

/** @public */
export interface MapReduceOptions extends CommandOperationOptions {
export interface MapReduceOptions extends CommandOperationOptions, ExplainOptions {
/** Sets the output target for the map reduce job. */
out?: 'inline' | { inline: 1 } | { replace: string } | { merge: string } | { reduce: string };
/** Query filter object. */
Expand Down Expand Up @@ -67,7 +69,10 @@ interface MapReduceStats {
* Run Map Reduce across a collection. Be aware that the inline option for out will return an array of results not a collection.
* @internal
*/
export class MapReduceOperation extends CommandOperation<MapReduceOptions, Document | Document[]> {
export class MapReduceOperation extends ExplainableCommand<
MapReduceOptions,
Document | Document[]
> {
collection: Collection;
/** The mapping function. */
map: MapFunction | string;
Expand Down Expand Up @@ -160,6 +165,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
2 changes: 0 additions & 2 deletions src/operations/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export interface OperationConstructor extends Function {
export interface OperationOptions extends BSONSerializeOptions {
/** Specify ClientSession for this command */
session?: ClientSession;

explain?: boolean;
willRetryWrites?: boolean;
}

Expand Down
Loading