Skip to content

Commit

Permalink
feat: add commitQuorum option to createIndexes command
Browse files Browse the repository at this point in the history
Adds commitQuorum option (new in MongoDB 4.4) to createIndexes command
and associated helpers:
db.createIndex, collection.createIndex, collection.createIndexes

NODE-2569
  • Loading branch information
emadum authored May 5, 2020
1 parent 2b7b936 commit 168a952
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 135 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"jsdoc/require-jsdoc": "off",
"jsdoc/no-undefined-types": "off",

"jsdoc/require-param": "off",
"jsdoc/require-param-description": "off",
"jsdoc/require-returns": "off",
"jsdoc/require-returns-description": "off",
Expand Down
17 changes: 12 additions & 5 deletions lib/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const { removeDocuments, updateDocuments } = require('./operations/common_functi
const AggregateOperation = require('./operations/aggregate');
const BulkWriteOperation = require('./operations/bulk_write');
const CountDocumentsOperation = require('./operations/count_documents');
const CreateIndexOperation = require('./operations/create_index');
const CreateIndexesOperation = require('./operations/create_indexes');
const DeleteManyOperation = require('./operations/delete_many');
const DeleteOneOperation = require('./operations/delete_one');
Expand Down Expand Up @@ -1233,6 +1232,7 @@ Collection.prototype.isCapped = function(options, callback) {
* @param {object} [options.partialFilterExpression] Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {(number|string)} [options.commitQuorum] (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes.
* @param {Collection~resultCallback} [callback] The command result callback
* @returns {Promise} returns Promise if no callback passed
* @example
Expand All @@ -1259,14 +1259,14 @@ Collection.prototype.createIndex = function(fieldOrSpec, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};

const createIndexOperation = new CreateIndexOperation(
this.s.db,
const createIndexesOperation = new CreateIndexesOperation(
this,
this.collectionName,
fieldOrSpec,
options
);

return executeOperation(this.s.topology, createIndexOperation, callback);
return executeOperation(this.s.topology, createIndexesOperation, callback);
};

/**
Expand All @@ -1281,6 +1281,7 @@ Collection.prototype.createIndex = function(fieldOrSpec, options, callback) {
* @param {Collection~IndexDefinition[]} indexSpecs An array of index specifications to be created
* @param {object} [options] Optional settings
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {(number|string)} [options.commitQuorum] (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes.
* @param {Collection~resultCallback} [callback] The command result callback
* @returns {Promise} returns Promise if no callback passed
* @example
Expand All @@ -1305,9 +1306,15 @@ Collection.prototype.createIndexes = function(indexSpecs, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});

options = options ? Object.assign({}, options) : {};

if (typeof options.maxTimeMS !== 'number') delete options.maxTimeMS;

const createIndexesOperation = new CreateIndexesOperation(this, indexSpecs, options);
const createIndexesOperation = new CreateIndexesOperation(
this,
this.collectionName,
indexSpecs,
options
);

return executeOperation(this.s.topology, createIndexesOperation, callback);
};
Expand Down
7 changes: 4 additions & 3 deletions lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const AddUserOperation = require('./operations/add_user');
const CollectionsOperation = require('./operations/collections');
const CommandOperation = require('./operations/command');
const CreateCollectionOperation = require('./operations/create_collection');
const CreateIndexOperation = require('./operations/create_index');
const CreateIndexesOperation = require('./operations/create_indexes');
const { DropCollectionOperation, DropDatabaseOperation } = require('./operations/drop');
const ExecuteDbAdminCommandOperation = require('./operations/execute_db_admin_command');
const IndexInformationOperation = require('./operations/index_information');
Expand Down Expand Up @@ -713,16 +713,17 @@ Db.prototype.executeDbAdminCommand = function(selector, options, callback) {
* @param {string} [options.name] Override the autogenerated index name (useful if the resulting name is larger than 128 bytes)
* @param {object} [options.partialFilterExpression] Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {(number|string)} [options.commitQuorum] (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes.
* @param {Db~resultCallback} [callback] The command result callback
* @returns {Promise} returns Promise if no callback passed
*/
Db.prototype.createIndex = function(name, fieldOrSpec, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options ? Object.assign({}, options) : {};

const createIndexOperation = new CreateIndexOperation(this, name, fieldOrSpec, options);
const createIndexesOperation = new CreateIndexesOperation(this, name, fieldOrSpec, options);

return executeOperation(this.s.topology, createIndexOperation, callback);
return executeOperation(this.s.topology, createIndexesOperation, callback);
};

/**
Expand Down
90 changes: 0 additions & 90 deletions lib/operations/create_index.js

This file was deleted.

119 changes: 84 additions & 35 deletions lib/operations/create_indexes.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,108 @@
'use strict';

const ReadPreference = require('../read_preference');
const { Aspect, defineAspects, OperationBase } = require('./operation');
const { executeCommand } = require('./db_ops');
const { Aspect, defineAspects } = require('./operation');
const { MongoError } = require('../error');
const CommandOperationV2 = require('./command_v2');
const { maxWireVersion, parseIndexOptions } = require('../utils');

class CreateIndexesOperation extends OperationBase {
constructor(collection, indexSpecs, options) {
super(options);
const validIndexOptions = new Set([
'unique',
'partialFilterExpression',
'sparse',
'background',
'expireAfterSeconds',
'storageEngine',
'collation'
]);

class CreateIndexesOperation extends CommandOperationV2 {
constructor(parent, collection, indexes, options) {
super(parent, options);
this.collection = collection;
this.indexSpecs = indexSpecs;

// createIndex can be called with a variety of styles:
// coll.createIndex('a');
// coll.createIndex({ a: 1 });
// coll.createIndex([['a', 1]]);
// createIndexes is always called with an array of index spec objects
if (!Array.isArray(indexes) || Array.isArray(indexes[0])) {
this.onlyReturnNameOfCreatedIndex = true;
// TODO: remove in v4 (breaking change); make createIndex return full response as createIndexes does

const indexParameters = parseIndexOptions(indexes);
// Generate the index name
const name = typeof options.name === 'string' ? options.name : indexParameters.name;
// Set up the index
const indexSpec = { name, key: indexParameters.fieldHash };
// merge valid index options into the index spec
for (let optionName in options) {
if (validIndexOptions.has(optionName)) {
indexSpec[optionName] = options[optionName];
}
}
this.indexes = [indexSpec];
return;
}

this.indexes = indexes;
}

execute(callback) {
const coll = this.collection;
const indexSpecs = this.indexSpecs;
let options = this.options;
execute(server, callback) {
const options = this.options;
const indexes = this.indexes;

const capabilities = coll.s.topology.capabilities();
const serverWireVersion = maxWireVersion(server);

// Ensure we generate the correct name if the parameter is not set
for (let i = 0; i < indexSpecs.length; i++) {
if (indexSpecs[i].name == null) {
const keys = [];
for (let i = 0; i < indexes.length; i++) {
// Did the user pass in a collation, check if our write server supports it
if (indexes[i].collation && serverWireVersion < 5) {
callback(
new MongoError(
`Server ${server.name}, which reports wire version ${serverWireVersion}, does not support collation`
)
);
return;
}

// Did the user pass in a collation, check if our write server supports it
if (indexSpecs[i].collation && capabilities && !capabilities.commandsTakeCollation) {
return callback(new MongoError('server/primary/mongos does not support collation'));
}
if (indexes[i].name == null) {
const keys = [];

for (let name in indexSpecs[i].key) {
keys.push(`${name}_${indexSpecs[i].key[name]}`);
for (let name in indexes[i].key) {
keys.push(`${name}_${indexes[i].key[name]}`);
}

// Set the name
indexSpecs[i].name = keys.join('_');
indexes[i].name = keys.join('_');
}
}

const cmd = { createIndexes: this.collection, indexes };

if (options.commitQuorum != null) {
if (serverWireVersion < 9) {
callback(
new MongoError('`commitQuorum` option for `createIndexes` not supported on servers < 4.4')
);
return;
}
cmd.commitQuorum = options.commitQuorum;
}

options = Object.assign({}, options, { readPreference: ReadPreference.PRIMARY });

// Execute the index
executeCommand(
coll.s.db,
{
createIndexes: coll.collectionName,
indexes: indexSpecs
},
options,
callback
);
// collation is set on each index, it should not be defined at the root
this.options.collation = undefined;

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

callback(null, this.onlyReturnNameOfCreatedIndex ? indexes[0].name : result);
});
}
}

defineAspects(CreateIndexesOperation, Aspect.WRITE_OPERATION);
defineAspects(CreateIndexesOperation, [Aspect.WRITE_OPERATION, Aspect.EXECUTE_WITH_SELECTION]);

module.exports = CreateIndexesOperation;
4 changes: 2 additions & 2 deletions test/functional/collations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ describe('Collation', function() {
.then(() => Promise.reject('should not succeed'))
.catch(err => {
expect(err).to.exist;
expect(err.message).to.equal('server/primary/mongos does not support collation');
expect(err.message).to.match(/does not support collation$/);
})
.then(() => client.close());
});
Expand Down Expand Up @@ -750,7 +750,7 @@ describe('Collation', function() {
.createIndexes([{ key: { a: 1 }, collation: { caseLevel: true } }])
.then(() => Promise.reject('should not succeed'))
.catch(err => {
expect(err.message).to.equal('server/primary/mongos does not support collation');
expect(err.message).to.match(/does not support collation$/);
return client.close();
});
});
Expand Down
Loading

0 comments on commit 168a952

Please sign in to comment.