From 917f2b088f22f4c6ed803f0349859d057389ac1e Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 3 Apr 2020 15:54:15 -0400 Subject: [PATCH] feat: support creating collections and indexes in transactions MongoDB 4.4 now supports creation of collections and indexes within transactions. This patch includes that support as well as a spec tests to validate the behavior. NODE-2295 --- test/functional/spec-runner/index.js | 39 ++++++++++++--- test/functional/transactions.test.js | 74 ++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/test/functional/spec-runner/index.js b/test/functional/spec-runner/index.js index 2d02a0660e..a5a9ee95c1 100644 --- a/test/functional/spec-runner/index.js +++ b/test/functional/spec-runner/index.js @@ -473,30 +473,57 @@ function resolveOperationArgs(operationName, operationArgs, context) { const CURSOR_COMMANDS = new Set(['find', 'aggregate', 'listIndexes', 'listCollections']); const ADMIN_COMMANDS = new Set(['listDatabases']); +function maybeSession(operation, context) { + return ( + operation && + operation.arguments && + operation.arguments.session && + context[operation.arguments.session] + ); +} + const kOperations = new Map([ [ 'createIndex', - (operation, collection /*, context, options */) => { + (operation, collection, context /*, options */) => { const fieldOrSpec = operation.arguments.keys; - return collection.createIndex(fieldOrSpec); + const options = { session: maybeSession(operation, context) }; + if (operation.arguments.name) options.name = operation.arguments.name; + return collection.createIndex(fieldOrSpec, options); + } + ], + [ + 'createCollection', + (operation, db, context /*, options */) => { + const collectionName = operation.arguments.collection; + const session = maybeSession(operation, context); + return db.createCollection(collectionName, { session }); + } + ], + [ + 'dropCollection', + (operation, db, context /*, options */) => { + const collectionName = operation.arguments.collection; + const session = maybeSession(operation, context); + return db.dropCollection(collectionName, { session }); } ], [ 'dropIndex', (operation, collection /*, context, options */) => { const indexName = operation.arguments.name; - return collection.dropIndex(indexName); + const session = maybeSession(operation, context); + return collection.dropIndex(indexName, { session }); } ], [ 'mapReduce', - (operation, collection /*, context, options */) => { + (operation, collection, context /*, options */) => { const args = operation.arguments; const map = args.map; const reduce = args.reduce; - const options = {}; + const options = { session: maybeSession(operation, context) }; if (args.out) options.out = args.out; - return collection.mapReduce(map, reduce, options); } ] diff --git a/test/functional/transactions.test.js b/test/functional/transactions.test.js index db24f7bf53..adee3f5a21 100644 --- a/test/functional/transactions.test.js +++ b/test/functional/transactions.test.js @@ -7,8 +7,66 @@ const { TestRunnerContext, generateTopologyTests } = require('./spec-runner'); const { loadSpecTests } = require('../spec'); const { MongoNetworkError } = require('../../lib/error'); +function ignoreNsNotFoundForListIndexes(err) { + if (err.code !== 26) { + throw err; + } + + return []; +} + +class TransactionsRunnerContext extends TestRunnerContext { + assertCollectionExists(options) { + const client = this.sharedClient; + const db = client.db(options.database); + const collectionName = options.collection; + + return db + .listCollections() + .toArray() + .then(collections => expect(collections.some(coll => coll.name === collectionName)).to.be.ok); + } + + assertCollectionNotExists(options) { + const client = this.sharedClient; + const db = client.db(options.database); + const collectionName = options.collection; + + return db + .listCollections() + .toArray() + .then( + collections => expect(collections.every(coll => coll.name !== collectionName)).to.be.ok + ); + } + + assertIndexExists(options) { + const client = this.sharedClient; + const collection = client.db(options.database).collection(options.collection); + const indexName = options.index; + + return collection + .listIndexes() + .toArray() + .catch(ignoreNsNotFoundForListIndexes) + .then(indexes => expect(indexes.some(idx => idx.name === indexName)).to.be.ok); + } + + assertIndexNotExists(options) { + const client = this.sharedClient; + const collection = client.db(options.database).collection(options.collection); + const indexName = options.index; + + return collection + .listIndexes() + .toArray() + .catch(ignoreNsNotFoundForListIndexes) + .then(indexes => expect(indexes.every(idx => idx.name !== indexName)).to.be.ok); + } +} + describe('Transactions', function() { - const testContext = new TestRunnerContext(); + const testContext = new TransactionsRunnerContext(); [ { name: 'spec tests', specPath: 'transactions' }, @@ -34,7 +92,17 @@ describe('Transactions', function() { // This test needs there to be multiple mongoses 'increment txnNumber', // Skipping this until SPEC-1320 is resolved - 'remain pinned after non-transient error on commit' + 'remain pinned after non-transient error on commit', + + // Will be implemented as part of NODE-2034 + 'Client side error in command starting transaction', + 'Client side error when transaction is in progress', + + // Will be implemented as part of NODE-2538 + 'abortTransaction only retries once with RetryableWriteError from server', + 'abortTransaction does not retry without RetryableWriteError label', + 'commitTransaction does not retry error without RetryableWriteError label', + 'commitTransaction retries once with RetryableWriteError from server' ]; return SKIP_TESTS.indexOf(spec.description) === -1; @@ -132,7 +200,7 @@ describe('Transactions', function() { const session = client.startSession(); const db = client.db(configuration.db); - db.createCollection('transaction_error_test', (err, coll) => { + db.createCollection('transaction_error_test_2', (err, coll) => { expect(err).to.not.exist; session.startTransaction();