diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 12fa3b36175..d00caff025d 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { once } from 'events'; import { type Collection, @@ -8,17 +9,66 @@ import { MongoServerError } from '../../mongodb'; -describe('Explain', function () { +const explain = [true, false, 'queryPlanner', 'allPlansExecution', 'executionStats', 'invalid']; + +describe('CRUD API explain option', function () { let client: MongoClient; let db: Db; let collection: Collection; + let commandStartedPromise: Promise; + const ops = [ + { + name: 'deleteOne', + op: async (explain: boolean | string) => await collection.deleteOne({ a: 1 }, { explain }) + }, + { + name: 'deleteMany', + op: async (explain: boolean | string) => await collection.deleteMany({ a: 1 }, { explain }) + }, + { + name: 'updateOne', + op: async (explain: boolean | string) => + await collection.updateOne({ a: 1 }, { $inc: { a: 2 } }, { explain }) + }, + { + name: 'updateMany', + op: async (explain: boolean | string) => + await collection.updateMany({ a: 1 }, { $inc: { a: 2 } }, { explain }) + }, + { + name: 'distinct', + op: async (explain: boolean | string) => await collection.distinct('a', {}, { explain }) + }, + { + name: 'findOneAndDelete', + op: async (explain: boolean | string) => + await collection.findOneAndDelete({ a: 1 }, { explain }) + }, + { + name: 'findOne', + op: async (explain: boolean | string) => await collection.findOne({ a: 1 }, { explain }) + }, + { name: 'find', op: (explain: boolean | string) => collection.find({ a: 1 }).explain(explain) }, + { + name: 'findOneAndReplace', + op: async (explain: boolean | string) => + await collection.findOneAndReplace({ a: 1 }, { a: 2 }, { explain }) + }, + { + name: 'aggregate', + op: async (explain: boolean | string) => + await collection + .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }], { explain }) + .toArray() + } + ]; beforeEach(async function () { client = this.configuration.newClient({ monitorCommands: true }); db = client.db('queryPlannerExplainResult'); collection = db.collection('test'); - await collection.insertOne({ a: 1 }); + commandStartedPromise = once(client, 'commandStarted'); }); afterEach(async function () { @@ -26,231 +76,64 @@ describe('Explain', function () { await client.close(); }); - context('when explain is set to true', () => { - it('deleteOne returns queryPlanner explain result', async function () { - const explanation = await collection.deleteOne({ a: 1 }, { explain: true }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('deleteMany returns queryPlanner explain result', async function () { - const explanation = await collection.deleteMany({ a: 1 }, { explain: true }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('updateOne returns queryPlanner explain result', async function () { - const explanation = await collection.updateOne( - { a: 1 }, - { $inc: { a: 2 } }, - { explain: true } - ); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('updateMany returns queryPlanner explain result', async function () { - const explanation = await collection.updateMany( - { a: 1 }, - { $inc: { a: 2 } }, - { explain: true } - ); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('distinct returns queryPlanner explain result', async function () { - const explanation = await collection.distinct('a', {}, { explain: true }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('findOneAndDelete returns queryPlanner explain result', async function () { - const explanation = await collection.findOneAndDelete({ a: 1 }, { explain: true }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('allPlansExecution returns verbose queryPlanner explain result', async function () { - const explanation = await collection.deleteOne({ a: 1 }, { explain: true }); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).nested.property('executionStats.allPlansExecution').to.exist; - }); - - it('findOne returns queryPlanner explain result', async function () { - const explanation = await collection.findOne({ a: 1 }, { explain: true }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('find returns queryPlanner explain result', async () => { - const [explanation] = await collection.find({ a: 1 }, { explain: true }).toArray(); - expect(explanation).property('queryPlanner').to.exist; - }); - }); - - context('when explain is set to false', () => { - it('only queryPlanner property is used in explain result', async function () { - const explanation = await collection.deleteOne({ a: 1 }, { explain: false }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('find returns "queryPlanner" explain result specified on cursor', async function () { - const explanation = await collection.find({ a: 1 }).explain(false); - expect(explanation).property('queryPlanner').to.exist; - }); - }); - - context('when explain is set to "queryPlanner"', () => { - it('only queryPlanner property is used in explain result', async function () { - const explanation = await collection.deleteOne({ a: 1 }, { explain: 'queryPlanner' }); - expect(explanation).property('queryPlanner').to.exist; - }); - - it('findOneAndReplace returns queryPlanner explain result', async function () { - const explanation = await collection.findOneAndReplace( - { a: 1 }, - { a: 2 }, - { explain: 'queryPlanner' } - ); - expect(explanation).property('queryPlanner').to.exist; - }); - }); - - context('when explain is set to "executionStats"', () => { - it('"executionStats" property is used in explain result', async function () { - const explanation = await collection.deleteMany({ a: 1 }, { explain: 'executionStats' }); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).property('executionStats').to.exist; - expect(explanation).to.not.have.nested.property('executionStats.allPlansExecution'); - }); - - it('distinct returns executionStats explain result', async function () { - const explanation = await collection.distinct('a', {}, { explain: 'executionStats' }); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).property('executionStats').to.exist; - }); - - it('find returns executionStats explain result', async function () { - const [explanation] = await collection - .find({ a: 1 }, { explain: 'executionStats' }) - .toArray(); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).property('executionStats').to.exist; - }); - - it('findOne returns executionStats explain result', async function () { - const explanation = await collection.findOne({ a: 1 }, { explain: 'executionStats' }); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).property('executionStats').to.exist; - }); - }); - - context('when explain is set to "allPlansExecution"', () => { - it('allPlansExecution property is used in explain result', async function () { - const explanation = await collection.deleteOne({ a: 1 }, { explain: 'allPlansExecution' }); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).property('executionStats').to.exist; - expect(explanation).nested.property('executionStats.allPlansExecution').to.exist; - }); - - it('find returns allPlansExecution explain result specified on cursor', async function () { - const explanation = await collection.find({ a: 1 }).explain('allPlansExecution'); - expect(explanation).property('queryPlanner').to.exist; - expect(explanation).property('executionStats').to.exist; - }); - }); - - context('aggregate()', () => { - it('when explain is set to true, aggregate result returns queryPlanner and executionStats properties', async function () { - const aggResult = await collection - .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }], { explain: true }) - .toArray(); - - if (aggResult[0].stages) { - expect(aggResult[0].stages).to.have.length.gte(1); - expect(aggResult[0].stages[0]).to.have.property('$cursor'); - expect(aggResult[0].stages[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult[0].stages[0].$cursor).to.have.property('executionStats'); - } else if (aggResult[0].$cursor) { - expect(aggResult[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult[0].$cursor).to.have.property('executionStats'); - } else { - expect(aggResult[0]).to.have.property('queryPlanner'); - expect(aggResult[0]).to.have.property('executionStats'); - } - }); - - it('when explain is set to "executionStats", aggregate result returns queryPlanner and executionStats properties', async function () { - const aggResult = await collection - .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }], { - explain: 'executionStats' - }) - .toArray(); - if (aggResult[0].stages) { - expect(aggResult[0].stages).to.have.length.gte(1); - expect(aggResult[0].stages[0]).to.have.property('$cursor'); - expect(aggResult[0].stages[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult[0].stages[0].$cursor).to.have.property('executionStats'); - } else if (aggResult[0].$cursor) { - expect(aggResult[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult[0].$cursor).to.have.property('executionStats'); - } else { - expect(aggResult[0]).to.have.property('queryPlanner'); - expect(aggResult[0]).to.have.property('executionStats'); - } - }); - - it('when explain is set to false, aggregate result returns queryPlanner property', async function () { - const aggResult = await collection - .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }]) - .explain(false); - if (aggResult && aggResult.stages) { - expect(aggResult.stages).to.have.length.gte(1); - expect(aggResult.stages[0]).to.have.property('$cursor'); - expect(aggResult.stages[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult.stages[0].$cursor).to.not.have.property('executionStats'); - } else if (aggResult.$cursor) { - expect(aggResult.$cursor).to.have.property('queryPlanner'); - expect(aggResult.$cursor).to.not.have.property('executionStats'); - } else { - expect(aggResult).to.have.property('queryPlanner'); - expect(aggResult).to.not.have.property('executionStats'); - } - }); - - it('when explain is set to "allPlansExecution", aggregate result returns queryPlanner and executionStats properties', async function () { - const aggResult = await collection - .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }]) - .explain('allPlansExecution'); - - if (aggResult && aggResult.stages) { - expect(aggResult.stages).to.have.length.gte(1); - expect(aggResult.stages[0]).to.have.property('$cursor'); - expect(aggResult.stages[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult.stages[0].$cursor).to.have.property('executionStats'); - } else { - expect(aggResult).to.have.property('queryPlanner'); - expect(aggResult).to.have.property('executionStats'); - } - }); - - it('when explain is not set, aggregate result returns queryPlanner and executionStats properties', async function () { - const aggResult = await collection - .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }]) - .explain(); - if (aggResult && aggResult.stages) { - expect(aggResult.stages).to.have.length.gte(1); - expect(aggResult.stages[0]).to.have.property('$cursor'); - expect(aggResult.stages[0].$cursor).to.have.property('queryPlanner'); - expect(aggResult.stages[0].$cursor).to.have.property('executionStats'); - } else { - expect(aggResult).to.have.property('queryPlanner'); - expect(aggResult).to.have.property('executionStats'); - } - }); - }); - - context('when explain is set to "invalidExplain", result returns MongoServerError', () => { - it('should throw a catchable error with invalid explain string', async function () { - const error = await collection - .find({ a: 1 }) - .explain('invalidExplain') - .catch(error => error); - expect(error).to.be.instanceOf(MongoServerError); - }); - }); + for (const explainValue of explain) { + for (const op of ops) { + const name = op.name; + context(`When explain is ${explainValue}, operation ${name}`, function () { + it(`sets command verbosity to ${explainValue} and includes ${explainValueToExpectation(explainValue)} in the return response`, async function () { + const response = await op.op(explainValue).catch(error => error); + const commandStartedEvent = await commandStartedPromise; + let explainDocument; + if (name === 'aggregate' && explainValue !== 'invalid') { + // value changes depending on server version + explainDocument = + response[0].stages?.[0]?.$cursor ?? response[0]?.stages ?? response[0]; + } else { + explainDocument = response; + } + switch (explainValue) { + case true: + case 'allPlansExecution': + expect(commandStartedEvent[0].command.verbosity).to.be.equal('allPlansExecution'); + expect(explainDocument).to.have.property('queryPlanner'); + expect(explainDocument).nested.property('executionStats.allPlansExecution').to.exist; + break; + case false: + case 'queryPlanner': + expect(commandStartedEvent[0].command.verbosity).to.be.equal('queryPlanner'); + expect(explainDocument).to.have.property('queryPlanner'); + expect(explainDocument).to.not.have.property('executionStats'); + break; + case 'executionStats': + expect(commandStartedEvent[0].command.verbosity).to.be.equal('executionStats'); + expect(explainDocument).to.have.property('queryPlanner'); + expect(explainDocument).to.have.property('executionStats'); + expect(explainDocument).to.not.have.nested.property( + 'executionStats.allPlansExecution' + ); + break; + default: + // for invalid values of explain + expect(response).to.be.instanceOf(MongoServerError); + break; + } + }); + }); + } + } }); + +function explainValueToExpectation(explainValue: boolean | string) { + switch (explainValue) { + case true: + case 'allPlansExecution': + return 'queryPlanner, executionStats, and nested allPlansExecution properties'; + case false: + case 'queryPlanner': + return 'only queryPlanner property'; + case 'executionStats': + return 'queryPlanner and executionStats property'; + default: + return 'error'; + } +}