diff options
-rw-r--r-- | jstests/core/clustered_index_creation.js | 101 | ||||
-rw-r--r-- | jstests/core/timeseries/clustered_index_options.js | 15 | ||||
-rw-r--r-- | jstests/multiVersion/timeseries_clustered_index_upgrade.js | 65 | ||||
-rw-r--r-- | src/mongo/db/catalog/SConscript | 17 | ||||
-rw-r--r-- | src/mongo/db/catalog/clustered_collection_options.idl | 66 | ||||
-rw-r--r-- | src/mongo/db/catalog/clustered_collection_util.cpp | 79 | ||||
-rw-r--r-- | src/mongo/db/catalog/clustered_collection_util.h | 59 | ||||
-rw-r--r-- | src/mongo/db/catalog/collection_options.cpp | 36 | ||||
-rw-r--r-- | src/mongo/db/catalog/collection_options.h | 5 | ||||
-rw-r--r-- | src/mongo/db/catalog/create_collection.cpp | 77 | ||||
-rw-r--r-- | src/mongo/db/commands/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/commands/create.idl | 9 | ||||
-rw-r--r-- | src/mongo/db/commands/create_command.cpp | 18 | ||||
-rw-r--r-- | src/mongo/db/storage/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/storage/record_store_test_harness.cpp | 5 | ||||
-rw-r--r-- | src/mongo/dbtests/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/dbtests/query_stage_collscan.cpp | 4 | ||||
-rw-r--r-- | src/mongo/dbtests/validate_tests.cpp | 3 |
18 files changed, 528 insertions, 34 deletions
diff --git a/jstests/core/clustered_index_creation.js b/jstests/core/clustered_index_creation.js new file mode 100644 index 00000000000..df98c629bed --- /dev/null +++ b/jstests/core/clustered_index_creation.js @@ -0,0 +1,101 @@ +/** + * Tests the options used to create a clustered collection and verifies the options match in the + * listCollections output. + * + * @tags: [ + * requires_fcv_51, + * assumes_against_mongod_not_mongos, + * assumes_no_implicit_collection_creation_after_drop, + * does_not_support_stepdowns, + * ] + */ +(function() { +"use strict"; + +const clusteredIndexesEnabled = assert + .commandWorked(db.getMongo().adminCommand( + {getParameter: 1, featureFlagClusteredIndexes: 1})) + .featureFlagClusteredIndexes.value; + +if (!clusteredIndexesEnabled) { + jsTestLog('Skipping test because the clustered indexes feature flag is disabled'); + return; +} + +const validateListCollections = function(db, collName, creationOptions) { + const listColls = + assert.commandWorked(db.runCommand({listCollections: 1, filter: {name: collName}})); + const listCollsOptions = listColls.cursor.firstBatch[0].options; + assert(listCollsOptions.clusteredIndex); + + let expectedOptions = creationOptions; + + // If the creationOptions don't specify the name, expect the default. + if (!creationOptions.clusteredIndex.name) { + expectedOptions.clusteredIndex.name = "_id_1"; + } + + // If the creationOptions don't specify 'v', expect the default. + if (!creationOptions.clusteredIndex.v) { + expectedOptions.clusteredIndex.v = 2; + } + + assert.docEq(listCollsOptions.clusteredIndex, expectedOptions.clusteredIndex); +}; + +/** + * Creates, validates, and drops a clustered collection with the provided creationOptions. + */ +const runSuccessfulCreate = function(db, coll, creationOptions) { + assert.commandWorked(db.createCollection(coll.getName(), creationOptions)); + validateListCollections(testDB, coll.getName(), creationOptions); + coll.drop(); +}; +const testDB = db.getSiblingDB(jsTestName()); +const coll = testDB.coll; + +runSuccessfulCreate(testDB, coll, {clusteredIndex: {key: {_id: 1}, unique: true}}); + +runSuccessfulCreate( + testDB, coll, {clusteredIndex: {key: {_id: 1}, unique: true}, expireAfterSeconds: 5}); + +runSuccessfulCreate( + testDB, coll, {clusteredIndex: {key: {_id: 1}, name: "index_on_id", unique: true}}); + +runSuccessfulCreate( + testDB, coll, {clusteredIndex: {key: {_id: 1}, name: "index_on_id", unique: true, v: 2}}); + +assert.commandFailedWithCode( + testDB.createCollection(coll.getName(), {clusteredIndex: {key: {_id: 1}, unique: false}}), + 5979700); + +assert.commandFailedWithCode( + testDB.createCollection(coll.getName(), {clusteredIndex: {key: {randKey: 1}, unique: true}}), + 5979701); + +// Clustered index legacy format { clusteredIndex: <bool> } is only supported on certain internal +// namespaces (e.g time-series buckets collections). Additionally, collections that support the +// legacy format are prohibited from using the other format. +const bucketsCollName = 'system.buckets.' + coll.getName(); +assert.commandFailedWithCode( + testDB.createCollection(bucketsCollName, {clusteredIndex: {key: {_id: 1}, unique: true}}), + 5979703); +assert.commandFailedWithCode(testDB.createCollection(coll.getName(), {clusteredIndex: true}), + 5979703); + +assert.commandFailedWithCode( + testDB.createCollection(coll.getName(), + {clusteredIndex: {key: {_id: 1}, unique: true, randField: 1}}), + 40415); + +assert.commandFailedWithCode( + testDB.createCollection(coll.getName(), + {clusteredIndex: {key: {_id: 1}, unique: true, v: 12345}}), + 5979704); + +// Invalid 'expireAfterSeconds'. +assert.commandFailedWithCode( + testDB.createCollection( + coll.getName(), {clusteredIndex: {key: {_id: 1}, unique: true}, expireAfterSeconds: -10}), + ErrorCodes.InvalidOptions); +})(); diff --git a/jstests/core/timeseries/clustered_index_options.js b/jstests/core/timeseries/clustered_index_options.js index d923b03508d..6920fd11fcc 100644 --- a/jstests/core/timeseries/clustered_index_options.js +++ b/jstests/core/timeseries/clustered_index_options.js @@ -23,6 +23,8 @@ const tsColl = testDB.clustered_index_options; const tsCollName = tsColl.getName(); const bucketsCollName = 'system.buckets.' + tsCollName; +assert.commandWorked(testDB.dropDatabase()); + assert.commandWorked(testDB.createCollection(bucketsCollName, {clusteredIndex: false})); assert.commandWorked(testDB.dropDatabase()); @@ -50,8 +52,9 @@ res = assert.eq(options, res.cursor.firstBatch[0].options); assert.commandWorked(testDB.dropDatabase()); +// Fails with different error code depending on whether featureFlagClusteredIndexes is enabled. assert.commandFailedWithCode(testDB.createCollection(bucketsCollName, {clusteredIndex: {}}), - ErrorCodes.TypeMismatch); + [ErrorCodes.TypeMismatch, 40414]); assert.commandFailedWithCode(testDB.createCollection(bucketsCollName, {clusteredIndex: 'a'}), ErrorCodes.TypeMismatch); assert.commandFailedWithCode( @@ -59,14 +62,16 @@ assert.commandFailedWithCode( {clusteredIndex: true, idIndex: {key: {_id: 1}, name: '_id_'}}), ErrorCodes.InvalidOptions); -// Using the 'clusteredIndex' option on any namespace other than a buckets namespace should fail. +// Fails with different error code depending on whether featureFlagClusteredIndexes is enabled. assert.commandFailedWithCode(testDB.createCollection(tsCollName, {clusteredIndex: true}), - ErrorCodes.InvalidOptions); + [ErrorCodes.InvalidOptions, 5979703]); assert.commandFailedWithCode(testDB.createCollection('test', {clusteredIndex: true}), - ErrorCodes.InvalidOptions); + [ErrorCodes.InvalidOptions, 5979703]); // Using the 'expireAfterSeconds' option on any namespace other than a time-series namespace or a -// clustered time-series buckets namespace should fail. +// clustered time-series buckets namespace should fail (provdided featureFlagClusteredIndexes is +// disabled). Otherwise, collection creation must specify the clusteredIndex option to use +// expireAfterSeconds. assert.commandFailedWithCode(testDB.createCollection('test', {expireAfterSeconds: 10}), ErrorCodes.InvalidOptions); assert.commandFailedWithCode(testDB.createCollection(bucketsCollName, {expireAfterSeconds: 10}), diff --git a/jstests/multiVersion/timeseries_clustered_index_upgrade.js b/jstests/multiVersion/timeseries_clustered_index_upgrade.js new file mode 100644 index 00000000000..4725ab7c830 --- /dev/null +++ b/jstests/multiVersion/timeseries_clustered_index_upgrade.js @@ -0,0 +1,65 @@ +/** + * Ensures that it is safe to upgrade a timeseries collection from v5.0 to "latest" without + * breaking the output of listCollections for the clusteredIndex option. Additionally, that the + * collection can then be safely downgraded again. + * + * TODO SERVER-60219: Since this test is specific to the upgrade process from 5.0 - 5.*, it can be + * removed once 6.0 becomes last-lts. + */ +(function() { +'use strict'; +load('jstests/multiVersion/libs/multi_rs.js'); +load('jstests/core/timeseries/libs/timeseries.js'); +const tsCollName = 'tsColl'; +const bucketsCollName = 'system.buckets.' + tsCollName; + +const getTestDB = function(rst) { + return rst.getPrimary().getDB("test"); +}; + +// The clusteredIndex option for a time-series buckets collection should always be {clusteredIndex: +// true}, regardless of v5.0 vs v5.*. +const assertListCollectionsBucketOutput = function(db) { + const listColls = + assert.commandWorked(db.runCommand({listCollections: 1, filter: {name: bucketsCollName}})); + const options = listColls.cursor.firstBatch[0].options; + assert(options.clusteredIndex); + assert.eq(options.clusteredIndex, true); +}; + +const oldVersion = "last-lts"; +const nodes = { + n1: {binVersion: oldVersion}, + n2: {binVersion: oldVersion}, + n3: {binVersion: oldVersion} +}; + +const rst = new ReplSetTest({nodes: nodes}); + +rst.startSet(); +rst.initiate(); + +if (!TimeseriesTest.timeseriesCollectionsEnabled(rst.getPrimary())) { + jsTestLog('Skipping test because the time-series collection feature flag is disabled'); + rest.stopSet(); + return; +} + +let testDB = getTestDB(rst); + +assert.commandWorked(testDB.createCollection(tsCollName, {timeseries: {timeField: 'time'}})); +assertListCollectionsBucketOutput(testDB); + +jsTest.log("Upgrading replica set from last-lts to latest"); +rst.upgradeSet({binVersion: "latest"}); + +testDB = getTestDB(rst); +assertListCollectionsBucketOutput(testDB); + +jsTest.log("Downgrading replica set from latest to last-lts"); +rst.upgradeSet({binVersion: "last-lts"}); +testDB = getTestDB(rst); +assertListCollectionsBucketOutput(testDB); + +rst.stopSet(); +})(); diff --git a/src/mongo/db/catalog/SConscript b/src/mongo/db/catalog/SConscript index d6506264bce..4fc0369ef08 100644 --- a/src/mongo/db/catalog/SConscript +++ b/src/mongo/db/catalog/SConscript @@ -15,6 +15,20 @@ env.SConscript( ) env.Library( + target='clustered_collection_options', + source=[ + 'clustered_collection_options.idl', + 'clustered_collection_util.cpp', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/namespace_string', + '$BUILD_DIR/mongo/idl/basic_types', + '$BUILD_DIR/mongo/idl/idl_parser', + ], +) + +env.Library( target='collection_options', source=[ 'collection_options.cpp', @@ -29,6 +43,7 @@ env.Library( '$BUILD_DIR/mongo/idl/basic_types', '$BUILD_DIR/mongo/idl/feature_flag', '$BUILD_DIR/mongo/idl/idl_parser', + 'clustered_collection_options', ], ) @@ -352,6 +367,7 @@ env.Library( '$BUILD_DIR/mongo/db/views/views_mongod', 'catalog_helpers', 'catalog_stats', + 'clustered_collection_options', 'collection', 'collection_options', 'database_holder', @@ -460,6 +476,7 @@ env.Library( '$BUILD_DIR/mongo/db/ttl_collection_cache', '$BUILD_DIR/mongo/db/views/views', '$BUILD_DIR/mongo/db/write_ops', + 'clustered_collection_options', 'collection', 'collection_options', 'database_holder', diff --git a/src/mongo/db/catalog/clustered_collection_options.idl b/src/mongo/db/catalog/clustered_collection_options.idl new file mode 100644 index 00000000000..5a69785e0e6 --- /dev/null +++ b/src/mongo/db/catalog/clustered_collection_options.idl @@ -0,0 +1,66 @@ +# Copyright (C) 2021-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo" + +imports: + - "mongo/idl/basic_types.idl" + +structs: + ClusteredIndexSpec: + description: "The specifications for a given clusteredIndex" + strict: true + fields: + v: + description: 'Index spec version' + type: safeInt + default: 2 + key: + description: 'Key to index on' + type: object_owned + name: + description: 'Descriptive name for the index' + type: string + optional: true + unique: + type: safeBool + + ClusteredCollectionInfo: + description: "Information on how a collection is clustered. For internal use only" + strict: true + fields: + indexSpec: + type: ClusteredIndexSpec + legacyFormat: + description: 'Legacy format means the clustered information was specified as + {clusteredIndex: true} and the cluster key defaults to _id. This should + only be supported for certain internal collections (e.g: time-series + buckets collections)' + type: safeBool + diff --git a/src/mongo/db/catalog/clustered_collection_util.cpp b/src/mongo/db/catalog/clustered_collection_util.cpp new file mode 100644 index 00000000000..92f45007fc3 --- /dev/null +++ b/src/mongo/db/catalog/clustered_collection_util.cpp @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2021-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/catalog/clustered_collection_util.h" + +#include "mongo/db/namespace_string.h" + +namespace mongo { +namespace clustered_util { + +static constexpr StringData kDefaultClusteredIndexName = "_id_1"_sd; + +ClusteredCollectionInfo makeCanonicalClusteredInfoForLegacyFormat() { + auto indexSpec = ClusteredIndexSpec{BSON("_id" << 1), true}; + indexSpec.setName(kDefaultClusteredIndexName); + return ClusteredCollectionInfo(std::move(indexSpec), true); +} + +ClusteredCollectionInfo makeCanonicalClusteredInfo(const ClusteredIndexSpec& indexSpec) { + return ClusteredCollectionInfo(indexSpec, false); +} + +boost::optional<ClusteredCollectionInfo> parseClusteredInfo(const BSONElement& elem) { + uassert(5979702, + "'clusteredIndex' has to be a boolean or object.", + elem.type() == mongo::Bool || elem.type() == mongo::Object); + + bool isLegacyFormat = elem.type() == mongo::Bool; + if (isLegacyFormat) { + // Legacy format implies the collection was created with format {clusteredIndex: <bool>}. + // The legacy format is maintained for backward compatibility with time series buckets + // collection creation. + if (!elem.Bool()) { + // clusteredIndex was specified as false. + return boost::none; + } + return makeCanonicalClusteredInfoForLegacyFormat(); + } + + auto indexSpec = ClusteredIndexSpec::parse({"ClusteredUtil::parseClusteredInfo"}, elem.Obj()); + if (!indexSpec.getName()) { + indexSpec.setName(kDefaultClusteredIndexName); + } + + return makeCanonicalClusteredInfo(indexSpec); +} + +bool requiresLegacyFormat(const NamespaceString& nss) { + return nss.isTimeseriesBucketsCollection(); +} + +} // namespace clustered_util +} // namespace mongo diff --git a/src/mongo/db/catalog/clustered_collection_util.h b/src/mongo/db/catalog/clustered_collection_util.h new file mode 100644 index 00000000000..be73068e037 --- /dev/null +++ b/src/mongo/db/catalog/clustered_collection_util.h @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2021-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/catalog/clustered_collection_options_gen.h" +#include "mongo/db/operation_context.h" + +namespace mongo { +namespace clustered_util { + +/** + * Constructs ClusteredCollectionInfo assuming legacy format {clusteredIndex: <bool>}. The + * collection defaults to being clustered by '_id' + */ +ClusteredCollectionInfo makeCanonicalClusteredInfoForLegacyFormat(); + +/** + * Constructs ClusteredCollectionInfo according to the 'indexSpec'. Stores the information is + * provided in the non-legacy format. + */ +ClusteredCollectionInfo makeCanonicalClusteredInfo(const ClusteredIndexSpec& indexSpec); + +boost::optional<ClusteredCollectionInfo> parseClusteredInfo(const BSONElement& elem); + +/** + * Returns true if legacy format is required for the namespace. + */ +bool requiresLegacyFormat(const NamespaceString& nss); + +} // namespace clustered_util + +} // namespace mongo diff --git a/src/mongo/db/catalog/collection_options.cpp b/src/mongo/db/catalog/collection_options.cpp index 012f6f6c147..520874659d8 100644 --- a/src/mongo/db/catalog/collection_options.cpp +++ b/src/mongo/db/catalog/collection_options.cpp @@ -34,6 +34,7 @@ #include <algorithm> #include "mongo/base/string_data.h" +#include "mongo/db/catalog/clustered_collection_util.h" #include "mongo/db/catalog/collection_options_validation.h" #include "mongo/db/commands.h" #include "mongo/db/commands/create_gen.h" @@ -42,6 +43,7 @@ #include "mongo/db/query/query_feature_flags_gen.h" #include "mongo/idl/command_generic_argument.h" #include "mongo/util/str.h" +#include "mongo/util/visit_helper.h" namespace mongo { namespace { @@ -197,11 +199,11 @@ StatusWith<CollectionOptions> CollectionOptions::parse(const BSONObj& options, P collectionOptions.collation = e.Obj().getOwned(); } else if (fieldName == "clusteredIndex") { - if (e.type() != mongo::Bool) { - return Status(ErrorCodes::BadValue, "'clusteredIndex' has to be a boolean."); + try { + collectionOptions.clusteredIndex = clustered_util::parseClusteredInfo(e); + } catch (const DBException& ex) { + return ex.toStatus(); } - - collectionOptions.clusteredIndex = e.Bool(); } else if (fieldName == "expireAfterSeconds") { if (e.type() != mongo::NumberLong) { return {ErrorCodes::BadValue, "'expireAfterSeconds' must be a number."}; @@ -310,7 +312,19 @@ CollectionOptions CollectionOptions::fromCreateCommand(const CreateCommand& cmd) options.timeseries = std::move(*timeseries); } if (auto clusteredIndex = cmd.getClusteredIndex()) { - options.clusteredIndex = *clusteredIndex; + stdx::visit( + visit_helper::Overloaded{ + [&](bool isClustered) { + if (isClustered) { + options.clusteredIndex = + clustered_util::makeCanonicalClusteredInfoForLegacyFormat(); + } + }, + [&](const ClusteredIndexSpec clusteredIndexSpec) { + options.clusteredIndex = + clustered_util::makeCanonicalClusteredInfo(clusteredIndexSpec); + }}, + *clusteredIndex); } if (auto expireAfterSeconds = cmd.getExpireAfterSeconds()) { options.expireAfterSeconds = expireAfterSeconds; @@ -392,7 +406,13 @@ void CollectionOptions::appendBSON(BSONObjBuilder* builder, } if (clusteredIndex && shouldAppend(CreateCommand::kClusteredIndexFieldName)) { - builder->append(CreateCommand::kClusteredIndexFieldName, true); + if (clusteredIndex->getLegacyFormat()) { + builder->append(CreateCommand::kClusteredIndexFieldName, true); + } else { + // Only append the user defined collection options. + builder->append(CreateCommand::kClusteredIndexFieldName, + clusteredIndex->getIndexSpec().toBSON()); + } } if (expireAfterSeconds && shouldAppend(CreateCommand::kExpireAfterSecondsFieldName)) { @@ -492,7 +512,9 @@ bool CollectionOptions::matchesStorageOptions(const CollectionOptions& other, return false; } - if (clusteredIndex != other.clusteredIndex) { + if ((clusteredIndex && other.clusteredIndex && + clusteredIndex->toBSON().woCompare(other.clusteredIndex->toBSON()) != 0) || + (clusteredIndex == boost::none) != (other.clusteredIndex == boost::none)) { return false; } diff --git a/src/mongo/db/catalog/collection_options.h b/src/mongo/db/catalog/collection_options.h index ab7aa9e2eae..8051d4b18a5 100644 --- a/src/mongo/db/catalog/collection_options.h +++ b/src/mongo/db/catalog/collection_options.h @@ -34,6 +34,7 @@ #include <boost/optional.hpp> #include "mongo/base/status.h" +#include "mongo/db/catalog/clustered_collection_options_gen.h" #include "mongo/db/catalog/collection_options_gen.h" #include "mongo/db/jsobj.h" #include "mongo/db/timeseries/timeseries_gen.h" @@ -153,8 +154,8 @@ struct CollectionOptions { // The namespace's default collation. BSONObj collation; - // Whether this collection is clustered on _id. - bool clusteredIndex = false; + // Exists if the collection is clustered. + boost::optional<ClusteredCollectionInfo> clusteredIndex; // If present, the number of seconds after which old data should be deleted. Only for // collections which are clustered on _id. diff --git a/src/mongo/db/catalog/create_collection.cpp b/src/mongo/db/catalog/create_collection.cpp index 8df41cf3fcb..37cf63383f5 100644 --- a/src/mongo/db/catalog/create_collection.cpp +++ b/src/mongo/db/catalog/create_collection.cpp @@ -37,6 +37,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/bson/json.h" +#include "mongo/db/catalog/clustered_collection_util.h" #include "mongo/db/catalog/collection_catalog.h" #include "mongo/db/catalog/database_holder.h" #include "mongo/db/catalog/index_key_validate.h" @@ -52,6 +53,7 @@ #include "mongo/db/ops/insert.h" #include "mongo/db/query/collation/collator_factory_interface.h" #include "mongo/db/repl/replication_coordinator.h" +#include "mongo/db/storage/storage_parameters_gen.h" #include "mongo/db/timeseries/timeseries_options.h" #include "mongo/db/views/view_catalog.h" #include "mongo/idl/command_generic_argument.h" @@ -64,6 +66,40 @@ namespace { MONGO_FAIL_POINT_DEFINE(failTimeseriesViewCreation); MONGO_FAIL_POINT_DEFINE(failPreimagesCollectionCreation); +using IndexVersion = IndexDescriptor::IndexVersion; + +Status validateClusteredIndexSpec(OperationContext* opCtx, + const ClusteredIndexSpec& spec, + boost::optional<int64_t> expireAfterSeconds) { + if (!spec.getUnique()) { + return Status(ErrorCodes::Error(5979700), + "The clusteredIndex option requires unique: true to be specified"); + } + + if (SimpleBSONObjComparator::kInstance.evaluate(spec.getKey() != BSON("_id" << 1))) { + return Status(ErrorCodes::Error(5979701), + "The clusteredIndex option is only supported for key: {_id: 1}"); + } + + if (expireAfterSeconds) { + // Not included in the indexSpec itself. + auto status = index_key_validate::validateExpireAfterSeconds(*expireAfterSeconds); + if (!status.isOK()) { + return status; + } + } + + auto versionAsInt = spec.getV(); + const IndexVersion indexVersion = static_cast<IndexVersion>(versionAsInt); + if (indexVersion != IndexVersion::kV2) { + return {ErrorCodes::Error(5979704), + str::stream() << "Invalid clusteredIndex specification " << spec.toBSON() + << "; cannot create a clusteredIndex with v=" << versionAsInt}; + } + + return Status::OK(); +} + void _createSystemDotViewsIfNecessary(OperationContext* opCtx, const Database* db) { // Create 'system.views' in a separate WUOW if it does not exist. if (!CollectionCatalog::get(opCtx)->lookupCollectionByNamespace(opCtx, @@ -257,7 +293,9 @@ Status _createTimeseries(OperationContext* opCtx, index_key_validate::validateExpireAfterSeconds(*expireAfterSeconds)); bucketsOptions.expireAfterSeconds = expireAfterSeconds; } - bucketsOptions.clusteredIndex = true; + + bucketsOptions.clusteredIndex = + clustered_util::makeCanonicalClusteredInfoForLegacyFormat(); if (auto coll = CollectionCatalog::get(opCtx)->lookupCollectionByNamespace(opCtx, bucketsNs)) { @@ -387,15 +425,36 @@ Status _createCollection(OperationContext* opCtx, str::stream() << "A view already exists. NS: " << nss); } - if (collectionOptions.clusteredIndex && !nss.isTimeseriesBucketsCollection()) { - return Status( - ErrorCodes::InvalidOptions, - "The 'clusteredIndex' option is only supported on time-series buckets collections"); - } - if (collectionOptions.clusteredIndex && idIndex && !idIndex->isEmpty()) { - return Status(ErrorCodes::InvalidOptions, - "The 'clusteredIndex' option is not supported with the 'idIndex' option"); + if (auto clusteredIndex = collectionOptions.clusteredIndex) { + bool clusteredIndexesEnabled = + feature_flags::gClusteredIndexes.isEnabled(serverGlobalParams.featureCompatibility); + if (!clusteredIndexesEnabled && !clustered_util::requiresLegacyFormat(nss)) { + // The 'clusteredIndex' option is only supported in legacy format for specific + // internal collections when the gClusteredIndexes flag is disabled. + return Status(ErrorCodes::InvalidOptions, + str::stream() + << "The 'clusteredIndex' option is not supported for namespace " + << nss); + } + + if ((nss.isTimeseriesBucketsCollection()) != (clusteredIndex->getLegacyFormat())) { + return Status(ErrorCodes::Error(5979703), + "The 'clusteredIndex' legacy format {clusteredIndex: <bool>} is only " + "supported for specific internal collections and vice versa"); + } + + if (idIndex && !idIndex->isEmpty()) { + return Status( + ErrorCodes::InvalidOptions, + "The 'clusteredIndex' option is not supported with the 'idIndex' option"); + } + + auto clusteredIndexStatus = validateClusteredIndexSpec( + opCtx, clusteredIndex->getIndexSpec(), collectionOptions.expireAfterSeconds); + if (!clusteredIndexStatus.isOK()) { + return clusteredIndexStatus; + } } diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript index 4d75bf91168..10d151a21fe 100644 --- a/src/mongo/db/commands/SConscript +++ b/src/mongo/db/commands/SConscript @@ -275,6 +275,7 @@ env.Library( LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/auth/authprivilege', + '$BUILD_DIR/mongo/db/catalog/clustered_collection_options', '$BUILD_DIR/mongo/db/catalog/collection_options', '$BUILD_DIR/mongo/db/query/query_knobs', '$BUILD_DIR/mongo/db/server_options', diff --git a/src/mongo/db/commands/create.idl b/src/mongo/db/commands/create.idl index 81491a0ad76..a5de04e7dcb 100644 --- a/src/mongo/db/commands/create.idl +++ b/src/mongo/db/commands/create.idl @@ -35,6 +35,7 @@ imports: - "mongo/db/auth/access_checks.idl" - "mongo/db/auth/action_type.idl" - "mongo/db/catalog/collection_options.idl" + - "mongo/db/catalog/clustered_collection_options.idl" - "mongo/db/timeseries/timeseries.idl" structs: @@ -168,8 +169,12 @@ commands: type: TimeseriesOptions optional: true clusteredIndex: - description: "Specifies whether this collection should be clustered on _id." - type: safeBool + description: "Specifies whether this collection should have a clusteredIndex. + Boolean is accepted as the legacy clustered index format for specific internal + collections - and implies clustering by _id. Otherwise, clusters according to + the ClusteredIndexSpec." + type: + variant: [safeBool, ClusteredIndexSpec] optional: true expireAfterSeconds: description: "The number of seconds after which old data should be deleted." diff --git a/src/mongo/db/commands/create_command.cpp b/src/mongo/db/commands/create_command.cpp index 038f7e607be..5df1f38f891 100644 --- a/src/mongo/db/commands/create_command.cpp +++ b/src/mongo/db/commands/create_command.cpp @@ -206,11 +206,19 @@ public: } if (cmd.getExpireAfterSeconds()) { - uassert(ErrorCodes::InvalidOptions, - "'expireAfterSeconds' is only supported on time-series collections", - cmd.getTimeseries() || - (cmd.getClusteredIndex() && - cmd.getNamespace().isTimeseriesBucketsCollection())); + if (feature_flags::gClusteredIndexes.isEnabled( + serverGlobalParams.featureCompatibility)) { + uassert(ErrorCodes::InvalidOptions, + "'expireAfterSeconds' is only supported on time-series collections or " + "when the 'clusteredIndex' option is specified", + cmd.getTimeseries() || cmd.getClusteredIndex()); + } else { + uassert(ErrorCodes::InvalidOptions, + "'expireAfterSeconds' is only supported on time-series collections", + cmd.getTimeseries() || + (cmd.getClusteredIndex() && + cmd.getNamespace().isTimeseriesBucketsCollection())); + } } // Validate _id index spec and fill in missing fields. diff --git a/src/mongo/db/storage/SConscript b/src/mongo/db/storage/SConscript index 3d69616ae75..32f7e3801ac 100644 --- a/src/mongo/db/storage/SConscript +++ b/src/mongo/db/storage/SConscript @@ -242,6 +242,7 @@ env.Library( 'record_store_test_updatewithdamages.cpp', ], LIBDEPS=[ + '$BUILD_DIR/mongo/db/catalog/clustered_collection_options', '$BUILD_DIR/mongo/db/catalog/collection_options', '$BUILD_DIR/mongo/db/record_id_helpers', '$BUILD_DIR/mongo/db/service_context', diff --git a/src/mongo/db/storage/record_store_test_harness.cpp b/src/mongo/db/storage/record_store_test_harness.cpp index 495cdd32a60..c6913ff6b3c 100644 --- a/src/mongo/db/storage/record_store_test_harness.cpp +++ b/src/mongo/db/storage/record_store_test_harness.cpp @@ -32,6 +32,7 @@ #include "mongo/db/storage/record_store_test_harness.h" +#include "mongo/db/catalog/clustered_collection_util.h" #include "mongo/db/record_id_helpers.h" #include "mongo/db/storage/record_store.h" #include "mongo/unittest/unittest.h" @@ -411,7 +412,7 @@ TEST(RecordStoreTestHarness, ClusteredRecordStore) { const auto harnessHelper = newRecordStoreHarnessHelper(); const std::string ns = "test.system.buckets.a"; CollectionOptions options; - options.clusteredIndex = true; + options.clusteredIndex = clustered_util::makeCanonicalClusteredInfoForLegacyFormat(); std::unique_ptr<RecordStore> rs = harnessHelper->newNonCappedRecordStore(ns, options); invariant(rs->keyFormat() == KeyFormat::String); @@ -516,7 +517,7 @@ TEST(RecordStoreTestHarness, ClusteredRecordStoreSeekNear) { const auto harnessHelper = newRecordStoreHarnessHelper(); const std::string ns = "test.system.buckets.a"; CollectionOptions options; - options.clusteredIndex = true; + options.clusteredIndex = clustered_util::makeCanonicalClusteredInfoForLegacyFormat(); std::unique_ptr<RecordStore> rs = harnessHelper->newNonCappedRecordStore(ns, options); invariant(rs->keyFormat() == KeyFormat::String); diff --git a/src/mongo/dbtests/SConscript b/src/mongo/dbtests/SConscript index 1e59959297d..037a9c9178e 100644 --- a/src/mongo/dbtests/SConscript +++ b/src/mongo/dbtests/SConscript @@ -139,6 +139,7 @@ env.Program( "$BUILD_DIR/mongo/client/replica_set_monitor_protocol_test_util", "$BUILD_DIR/mongo/db/auth/authmongod", "$BUILD_DIR/mongo/db/bson/dotted_path_support", + "$BUILD_DIR/mongo/db/catalog/clustered_collection_options", "$BUILD_DIR/mongo/db/catalog/collection_validation", "$BUILD_DIR/mongo/db/catalog/index_key_validate", "$BUILD_DIR/mongo/db/catalog/multi_index_block", diff --git a/src/mongo/dbtests/query_stage_collscan.cpp b/src/mongo/dbtests/query_stage_collscan.cpp index 5094317fd33..487a16f4445 100644 --- a/src/mongo/dbtests/query_stage_collscan.cpp +++ b/src/mongo/dbtests/query_stage_collscan.cpp @@ -37,6 +37,7 @@ #include <memory> #include "mongo/client/dbclient_cursor.h" +#include "mongo/db/catalog/clustered_collection_util.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/database.h" #include "mongo/db/client.h" @@ -180,7 +181,8 @@ public: WriteUnitOfWork wuow(&_opCtx); CollectionOptions collOptions; - collOptions.clusteredIndex = true; + collOptions.clusteredIndex = + clustered_util::makeCanonicalClusteredInfoForLegacyFormat(); const bool createIdIndex = false; db->createCollection(&_opCtx, ns, collOptions, createIdIndex); wuow.commit(); diff --git a/src/mongo/dbtests/validate_tests.cpp b/src/mongo/dbtests/validate_tests.cpp index cd589503c0e..801f1b40ddc 100644 --- a/src/mongo/dbtests/validate_tests.cpp +++ b/src/mongo/dbtests/validate_tests.cpp @@ -31,6 +31,7 @@ #include <cstdint> +#include "mongo/db/catalog/clustered_collection_util.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/collection_validation.h" #include "mongo/db/catalog/index_catalog.h" @@ -73,7 +74,7 @@ public: CollectionOptions options; if (clustered) { - options.clusteredIndex = true; + options.clusteredIndex = clustered_util::makeCanonicalClusteredInfoForLegacyFormat(); } const bool createIdIndex = !clustered; |