Skip to content

Commit 168a952

Browse files
authored
feat: add commitQuorum option to createIndexes command
Adds commitQuorum option (new in MongoDB 4.4) to createIndexes command and associated helpers: db.createIndex, collection.createIndex, collection.createIndexes NODE-2569
1 parent 2b7b936 commit 168a952

File tree

7 files changed

+195
-135
lines changed

7 files changed

+195
-135
lines changed

.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"jsdoc/require-jsdoc": "off",
2222
"jsdoc/no-undefined-types": "off",
2323

24+
"jsdoc/require-param": "off",
2425
"jsdoc/require-param-description": "off",
2526
"jsdoc/require-returns": "off",
2627
"jsdoc/require-returns-description": "off",

lib/collection.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const { removeDocuments, updateDocuments } = require('./operations/common_functi
3939
const AggregateOperation = require('./operations/aggregate');
4040
const BulkWriteOperation = require('./operations/bulk_write');
4141
const CountDocumentsOperation = require('./operations/count_documents');
42-
const CreateIndexOperation = require('./operations/create_index');
4342
const CreateIndexesOperation = require('./operations/create_indexes');
4443
const DeleteManyOperation = require('./operations/delete_many');
4544
const DeleteOneOperation = require('./operations/delete_one');
@@ -1233,6 +1232,7 @@ Collection.prototype.isCapped = function(options, callback) {
12331232
* @param {object} [options.partialFilterExpression] Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
12341233
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
12351234
* @param {ClientSession} [options.session] optional session to use for this operation
1235+
* @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.
12361236
* @param {Collection~resultCallback} [callback] The command result callback
12371237
* @returns {Promise} returns Promise if no callback passed
12381238
* @example
@@ -1259,14 +1259,14 @@ Collection.prototype.createIndex = function(fieldOrSpec, options, callback) {
12591259
if (typeof options === 'function') (callback = options), (options = {});
12601260
options = options || {};
12611261

1262-
const createIndexOperation = new CreateIndexOperation(
1263-
this.s.db,
1262+
const createIndexesOperation = new CreateIndexesOperation(
1263+
this,
12641264
this.collectionName,
12651265
fieldOrSpec,
12661266
options
12671267
);
12681268

1269-
return executeOperation(this.s.topology, createIndexOperation, callback);
1269+
return executeOperation(this.s.topology, createIndexesOperation, callback);
12701270
};
12711271

12721272
/**
@@ -1281,6 +1281,7 @@ Collection.prototype.createIndex = function(fieldOrSpec, options, callback) {
12811281
* @param {Collection~IndexDefinition[]} indexSpecs An array of index specifications to be created
12821282
* @param {object} [options] Optional settings
12831283
* @param {ClientSession} [options.session] optional session to use for this operation
1284+
* @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.
12841285
* @param {Collection~resultCallback} [callback] The command result callback
12851286
* @returns {Promise} returns Promise if no callback passed
12861287
* @example
@@ -1305,9 +1306,15 @@ Collection.prototype.createIndexes = function(indexSpecs, options, callback) {
13051306
if (typeof options === 'function') (callback = options), (options = {});
13061307

13071308
options = options ? Object.assign({}, options) : {};
1309+
13081310
if (typeof options.maxTimeMS !== 'number') delete options.maxTimeMS;
13091311

1310-
const createIndexesOperation = new CreateIndexesOperation(this, indexSpecs, options);
1312+
const createIndexesOperation = new CreateIndexesOperation(
1313+
this,
1314+
this.collectionName,
1315+
indexSpecs,
1316+
options
1317+
);
13111318

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

lib/db.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const AddUserOperation = require('./operations/add_user');
3838
const CollectionsOperation = require('./operations/collections');
3939
const CommandOperation = require('./operations/command');
4040
const CreateCollectionOperation = require('./operations/create_collection');
41-
const CreateIndexOperation = require('./operations/create_index');
41+
const CreateIndexesOperation = require('./operations/create_indexes');
4242
const { DropCollectionOperation, DropDatabaseOperation } = require('./operations/drop');
4343
const ExecuteDbAdminCommandOperation = require('./operations/execute_db_admin_command');
4444
const IndexInformationOperation = require('./operations/index_information');
@@ -713,16 +713,17 @@ Db.prototype.executeDbAdminCommand = function(selector, options, callback) {
713713
* @param {string} [options.name] Override the autogenerated index name (useful if the resulting name is larger than 128 bytes)
714714
* @param {object} [options.partialFilterExpression] Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
715715
* @param {ClientSession} [options.session] optional session to use for this operation
716+
* @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.
716717
* @param {Db~resultCallback} [callback] The command result callback
717718
* @returns {Promise} returns Promise if no callback passed
718719
*/
719720
Db.prototype.createIndex = function(name, fieldOrSpec, options, callback) {
720721
if (typeof options === 'function') (callback = options), (options = {});
721722
options = options ? Object.assign({}, options) : {};
722723

723-
const createIndexOperation = new CreateIndexOperation(this, name, fieldOrSpec, options);
724+
const createIndexesOperation = new CreateIndexesOperation(this, name, fieldOrSpec, options);
724725

725-
return executeOperation(this.s.topology, createIndexOperation, callback);
726+
return executeOperation(this.s.topology, createIndexesOperation, callback);
726727
};
727728

728729
/**

lib/operations/create_index.js

-90
This file was deleted.

lib/operations/create_indexes.js

+84-35
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,108 @@
11
'use strict';
22

3-
const ReadPreference = require('../read_preference');
4-
const { Aspect, defineAspects, OperationBase } = require('./operation');
5-
const { executeCommand } = require('./db_ops');
3+
const { Aspect, defineAspects } = require('./operation');
64
const { MongoError } = require('../error');
5+
const CommandOperationV2 = require('./command_v2');
6+
const { maxWireVersion, parseIndexOptions } = require('../utils');
77

8-
class CreateIndexesOperation extends OperationBase {
9-
constructor(collection, indexSpecs, options) {
10-
super(options);
8+
const validIndexOptions = new Set([
9+
'unique',
10+
'partialFilterExpression',
11+
'sparse',
12+
'background',
13+
'expireAfterSeconds',
14+
'storageEngine',
15+
'collation'
16+
]);
1117

18+
class CreateIndexesOperation extends CommandOperationV2 {
19+
constructor(parent, collection, indexes, options) {
20+
super(parent, options);
1221
this.collection = collection;
13-
this.indexSpecs = indexSpecs;
22+
23+
// createIndex can be called with a variety of styles:
24+
// coll.createIndex('a');
25+
// coll.createIndex({ a: 1 });
26+
// coll.createIndex([['a', 1]]);
27+
// createIndexes is always called with an array of index spec objects
28+
if (!Array.isArray(indexes) || Array.isArray(indexes[0])) {
29+
this.onlyReturnNameOfCreatedIndex = true;
30+
// TODO: remove in v4 (breaking change); make createIndex return full response as createIndexes does
31+
32+
const indexParameters = parseIndexOptions(indexes);
33+
// Generate the index name
34+
const name = typeof options.name === 'string' ? options.name : indexParameters.name;
35+
// Set up the index
36+
const indexSpec = { name, key: indexParameters.fieldHash };
37+
// merge valid index options into the index spec
38+
for (let optionName in options) {
39+
if (validIndexOptions.has(optionName)) {
40+
indexSpec[optionName] = options[optionName];
41+
}
42+
}
43+
this.indexes = [indexSpec];
44+
return;
45+
}
46+
47+
this.indexes = indexes;
1448
}
1549

16-
execute(callback) {
17-
const coll = this.collection;
18-
const indexSpecs = this.indexSpecs;
19-
let options = this.options;
50+
execute(server, callback) {
51+
const options = this.options;
52+
const indexes = this.indexes;
2053

21-
const capabilities = coll.s.topology.capabilities();
54+
const serverWireVersion = maxWireVersion(server);
2255

2356
// Ensure we generate the correct name if the parameter is not set
24-
for (let i = 0; i < indexSpecs.length; i++) {
25-
if (indexSpecs[i].name == null) {
26-
const keys = [];
57+
for (let i = 0; i < indexes.length; i++) {
58+
// Did the user pass in a collation, check if our write server supports it
59+
if (indexes[i].collation && serverWireVersion < 5) {
60+
callback(
61+
new MongoError(
62+
`Server ${server.name}, which reports wire version ${serverWireVersion}, does not support collation`
63+
)
64+
);
65+
return;
66+
}
2767

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

33-
for (let name in indexSpecs[i].key) {
34-
keys.push(`${name}_${indexSpecs[i].key[name]}`);
71+
for (let name in indexes[i].key) {
72+
keys.push(`${name}_${indexes[i].key[name]}`);
3573
}
3674

3775
// Set the name
38-
indexSpecs[i].name = keys.join('_');
76+
indexes[i].name = keys.join('_');
77+
}
78+
}
79+
80+
const cmd = { createIndexes: this.collection, indexes };
81+
82+
if (options.commitQuorum != null) {
83+
if (serverWireVersion < 9) {
84+
callback(
85+
new MongoError('`commitQuorum` option for `createIndexes` not supported on servers < 4.4')
86+
);
87+
return;
3988
}
89+
cmd.commitQuorum = options.commitQuorum;
4090
}
4191

42-
options = Object.assign({}, options, { readPreference: ReadPreference.PRIMARY });
43-
44-
// Execute the index
45-
executeCommand(
46-
coll.s.db,
47-
{
48-
createIndexes: coll.collectionName,
49-
indexes: indexSpecs
50-
},
51-
options,
52-
callback
53-
);
92+
// collation is set on each index, it should not be defined at the root
93+
this.options.collation = undefined;
94+
95+
super.executeCommand(server, cmd, (err, result) => {
96+
if (err) {
97+
callback(err);
98+
return;
99+
}
100+
101+
callback(null, this.onlyReturnNameOfCreatedIndex ? indexes[0].name : result);
102+
});
54103
}
55104
}
56105

57-
defineAspects(CreateIndexesOperation, Aspect.WRITE_OPERATION);
106+
defineAspects(CreateIndexesOperation, [Aspect.WRITE_OPERATION, Aspect.EXECUTE_WITH_SELECTION]);
58107

59108
module.exports = CreateIndexesOperation;

test/functional/collations.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ describe('Collation', function() {
716716
.then(() => Promise.reject('should not succeed'))
717717
.catch(err => {
718718
expect(err).to.exist;
719-
expect(err.message).to.equal('server/primary/mongos does not support collation');
719+
expect(err.message).to.match(/does not support collation$/);
720720
})
721721
.then(() => client.close());
722722
});
@@ -750,7 +750,7 @@ describe('Collation', function() {
750750
.createIndexes([{ key: { a: 1 }, collation: { caseLevel: true } }])
751751
.then(() => Promise.reject('should not succeed'))
752752
.catch(err => {
753-
expect(err.message).to.equal('server/primary/mongos does not support collation');
753+
expect(err.message).to.match(/does not support collation$/);
754754
return client.close();
755755
});
756756
});

0 commit comments

Comments
 (0)