summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/core/clustered_index_creation.js101
-rw-r--r--jstests/core/timeseries/clustered_index_options.js15
-rw-r--r--jstests/multiVersion/timeseries_clustered_index_upgrade.js65
-rw-r--r--src/mongo/db/catalog/SConscript17
-rw-r--r--src/mongo/db/catalog/clustered_collection_options.idl66
-rw-r--r--src/mongo/db/catalog/clustered_collection_util.cpp79
-rw-r--r--src/mongo/db/catalog/clustered_collection_util.h59
-rw-r--r--src/mongo/db/catalog/collection_options.cpp36
-rw-r--r--src/mongo/db/catalog/collection_options.h5
-rw-r--r--src/mongo/db/catalog/create_collection.cpp77
-rw-r--r--src/mongo/db/commands/SConscript1
-rw-r--r--src/mongo/db/commands/create.idl9
-rw-r--r--src/mongo/db/commands/create_command.cpp18
-rw-r--r--src/mongo/db/storage/SConscript1
-rw-r--r--src/mongo/db/storage/record_store_test_harness.cpp5
-rw-r--r--src/mongo/dbtests/SConscript1
-rw-r--r--src/mongo/dbtests/query_stage_collscan.cpp4
-rw-r--r--src/mongo/dbtests/validate_tests.cpp3
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;