diff options
-rw-r--r-- | jstests/core/collmod_convert_to_ttl.js | 50 | ||||
-rw-r--r-- | jstests/core/timeseries/timeseries_collmod.js | 59 | ||||
-rw-r--r-- | src/mongo/db/catalog/coll_mod.cpp | 46 |
3 files changed, 106 insertions, 49 deletions
diff --git a/jstests/core/collmod_convert_to_ttl.js b/jstests/core/collmod_convert_to_ttl.js index b5803db68d2..e1e66ea7a0b 100644 --- a/jstests/core/collmod_convert_to_ttl.js +++ b/jstests/core/collmod_convert_to_ttl.js @@ -5,60 +5,62 @@ * # Cannot implicitly shard accessed collections because of collection existing when none * # expected. * assumes_no_implicit_collection_creation_after_drop, - * * requires_non_retryable_commands, * requires_ttl_index, - * multiversion_incompatible, + * requires_fcv_51, * ] */ (function() { 'use strict'; -let collName = "collmod_convert_to_ttl"; -let coll = db.getCollection(collName); +const collName = "collmod_convert_to_ttl"; +const coll = db.getCollection(collName); coll.drop(); db.createCollection(collName); function findTTL(key, expireAfterSeconds) { - let all = coll.getIndexes(); - all = all.filter(function(z) { + const all = coll.getIndexes().filter(function(z) { return z.expireAfterSeconds == expireAfterSeconds && friendlyEqual(z.key, key); }); return all.length == 1; } // Creates a regular index and use collMod to convert it to a TTL index. -coll.dropIndex({a: 1}); coll.createIndex({a: 1}); // Tries to modify with a string 'expireAfterSeconds' value. -let res = db.runCommand( - {"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": "100"}}); -assert.commandFailedWithCode(res, ErrorCodes.TypeMismatch); +assert.commandFailedWithCode( + db.runCommand( + {"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": "100"}}), + ErrorCodes.TypeMismatch); // Tries to modify with a negative 'expireAfterSeconds' value. -res = - db.runCommand({"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": -1}}); -assert.commandFailedWithCode(res, ErrorCodes.InvalidOptions); +assert.commandFailedWithCode( + db.runCommand({"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": -1}}), + ErrorCodes.InvalidOptions); // Tries to modify with an 'expireAfterSeconds' value too large. -res = db.runCommand( - {"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": 10000000000000}}); -assert.commandFailedWithCode(res, ErrorCodes.InvalidOptions); +assert.commandFailedWithCode(db.runCommand({ + "collMod": collName, + "index": {"keyPattern": {a: 1}, "expireAfterSeconds": 10000000000000} +}), + ErrorCodes.InvalidOptions); // Successfully converts to a TTL index. -res = db.runCommand( - {"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": 100}}); +assert.commandWorked(db.runCommand( + {"collMod": collName, "index": {"keyPattern": {a: 1}, "expireAfterSeconds": 100}})); assert(findTTL({a: 1}, 100), "TTL index should be 100 now"); // Tries to convert a compound index to a TTL index. coll.createIndex({a: 1, b: 1}); -res = db.runCommand( - {"collMod": collName, "index": {"keyPattern": {a: 1, b: 1}, "expireAfterSeconds": 100}}); -assert.commandFailedWithCode(res, ErrorCodes.InvalidOptions); +assert.commandFailedWithCode( + db.runCommand( + {"collMod": collName, "index": {"keyPattern": {a: 1, b: 1}, "expireAfterSeconds": 100}}), + ErrorCodes.InvalidOptions); // Tries to convert the '_id' index to a TTL index. -res = db.runCommand( - {"collMod": collName, "index": {"keyPattern": {_id: 1}, "expireAfterSeconds": 100}}); -assert.commandFailedWithCode(res, ErrorCodes.InvalidOptions); +assert.commandFailedWithCode( + db.runCommand( + {"collMod": collName, "index": {"keyPattern": {_id: 1}, "expireAfterSeconds": 100}}), + ErrorCodes.InvalidOptions); })();
\ No newline at end of file diff --git a/jstests/core/timeseries/timeseries_collmod.js b/jstests/core/timeseries/timeseries_collmod.js new file mode 100644 index 00000000000..69fd7dbeb82 --- /dev/null +++ b/jstests/core/timeseries/timeseries_collmod.js @@ -0,0 +1,59 @@ +/** + * This tests which collMod options are allowed on a time-series collection. + * + * @tags: [ + * # Cannot implicitly shard accessed collections because of collection existing when none + * # expected. + * assumes_no_implicit_collection_creation_after_drop, + * requires_non_retryable_commands, + * requires_fcv_51, + * ] + */ + +(function() { +'use strict'; +const collName = "timeseries_collmod"; +const coll = db.getCollection(collName); +coll.drop(); +assert.commandWorked( + db.createCollection(collName, {timeseries: {timeField: "time", granularity: 'seconds'}})); +assert.commandWorked(coll.createIndex({"time": 1})); + +// Tries to convert a time-series secondary index to TTL index. +assert.commandFailedWithCode( + db.runCommand( + {"collMod": collName, "index": {"keyPattern": {"time": 1}, "expireAfterSeconds": 100}}), + ErrorCodes.InvalidOptions); + +// Successfully hides a time-series secondary index. +assert.commandWorked( + db.runCommand({"collMod": collName, "index": {"keyPattern": {"time": 1}, "hidden": true}})); + +// Tries to set the validator for a time-series collection. +assert.commandFailedWithCode( + db.runCommand({"collMod": collName, "validator": {required: ["time"]}}), + ErrorCodes.InvalidOptions); + +// Tries to set the validationLevel for a time-series collection. +assert.commandFailedWithCode(db.runCommand({"collMod": collName, "validationLevel": "moderate"}), + ErrorCodes.InvalidOptions); + +// Tries to set the validationAction for a time-series collection. +assert.commandFailedWithCode(db.runCommand({"collMod": collName, "validationAction": "warn"}), + ErrorCodes.InvalidOptions); + +// Tries to modify the view for a time-series collection. +assert.commandFailedWithCode(db.runCommand({"collMod": collName, "viewOn": "foo", "pipeline": []}), + ErrorCodes.InvalidOptions); + +// Tries to set 'recordPreImages' for a time-series collection. +assert.commandFailedWithCode(db.runCommand({"collMod": collName, "recordPreImages": true}), + ErrorCodes.InvalidOptions); + +// Successfully sets 'expireAfterSeconds' for a time-series collection. +assert.commandWorked(db.runCommand({"collMod": collName, "expireAfterSeconds": 60})); + +// Successfully sets the granularity for a time-series collection. +assert.commandWorked( + db.runCommand({"collMod": collName, "timeseries": {"granularity": "minutes"}})); +})();
\ No newline at end of file diff --git a/src/mongo/db/catalog/coll_mod.cpp b/src/mongo/db/catalog/coll_mod.cpp index f18a6797df5..a683d603d7c 100644 --- a/src/mongo/db/catalog/coll_mod.cpp +++ b/src/mongo/db/catalog/coll_mod.cpp @@ -122,6 +122,7 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, BSONObjBuilder* oplogEntryBuilder) { bool isView = !coll; + bool isTimeseries = coll && coll->getTimeseriesOptions() != boost::none; CollModRequest cmr; @@ -179,6 +180,12 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, return Status(ErrorCodes::InvalidOptions, "no expireAfterSeconds or hidden field"); } if (!cmr.indexExpireAfterSeconds.eoo()) { + if (isTimeseries) { + return Status(ErrorCodes::InvalidOptions, + "TTL indexes are not supported for time-series collections. " + "Please refer to the documentation and use the top-level " + "'expireAfterSeconds' option instead"); + } if (auto status = index_key_validate::validateExpireAfterSeconds( cmr.indexExpireAfterSeconds.safeNumberLong()); !status.isOK()) { @@ -261,7 +268,7 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, return Status(ErrorCodes::BadValue, "can't hide _id index"); } } - } else if (fieldName == "validator" && !isView) { + } else if (fieldName == "validator" && !isView && !isTimeseries) { // If the feature compatibility version is not kLatest, and we are validating features // as primary, ban the use of new agg features introduced in kLatest to prevent them // from being persisted in the catalog. @@ -281,13 +288,13 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, if (!cmr.collValidator->isOK()) { return cmr.collValidator->getStatus(); } - } else if (fieldName == "validationLevel" && !isView) { + } else if (fieldName == "validationLevel" && !isView && !isTimeseries) { try { cmr.collValidationLevel = ValidationLevel_parse({"validationLevel"}, e.String()); } catch (const DBException& exc) { return exc.toStatus(); } - } else if (fieldName == "validationAction" && !isView) { + } else if (fieldName == "validationAction" && !isView && !isTimeseries) { try { cmr.collValidationAction = ValidationAction_parse({"validationAction"}, e.String()); } catch (const DBException& exc) { @@ -311,25 +318,9 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, return Status(ErrorCodes::InvalidOptions, "'viewOn' option must be a string"); } cmr.viewOn = e.str(); - } else if (fieldName == "recordPreImages") { - if (isView) { - return {ErrorCodes::InvalidOptions, - str::stream() << "option not supported on a view: " << fieldName}; - } - + } else if (fieldName == "recordPreImages" && !isView && !isTimeseries) { cmr.recordPreImages = e.trueValue(); - } else if (fieldName == "changeStreamPreAndPostImages") { - if (nss.isTimeseriesBucketsCollection()) { - return {ErrorCodes::InvalidOptions, - str::stream() - << "option not supported on a timeseries collection: " << fieldName}; - } - - if (isView) { - return {ErrorCodes::InvalidOptions, - str::stream() << "option not supported on a view: " << fieldName}; - } - + } else if (fieldName == "changeStreamPreAndPostImages" && !isView && !isTimeseries) { if (e.type() != mongo::Bool) { return {ErrorCodes::InvalidOptions, "'changeStreamPreAndPostImages' option must be a boolean"}; @@ -359,16 +350,21 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, } cmr.clusteredIndexExpireAfterSeconds = e; - } else if (fieldName == "timeseries" && !isView) { - auto tsOptions = coll->getTimeseriesOptions(); - if (!tsOptions) { + } else if (fieldName == "timeseries") { + if (!isTimeseries) { return Status(ErrorCodes::InvalidOptions, - str::stream() << "option only supported on a timeseries collection: " + str::stream() << "option only supported on a time-series collection: " << fieldName); } cmr.timeseries = e; } else { + if (isTimeseries) { + return Status(ErrorCodes::InvalidOptions, + str::stream() << "option not supported on a time-series collection: " + << fieldName); + } + if (isView) { return Status(ErrorCodes::InvalidOptions, str::stream() << "option not supported on a view: " << fieldName); |