SERVER-111411: Fix resharding with forceDistribution on time series collections (#50466)

Co-authored-by: Abdul Qadeer <abdul.qadeer@mongodb.com>
GitOrigin-RevId: 1a2c62dc3bbf411f6c1b50b95fda1b332a49b9bd
This commit is contained in:
Sam Frank 2026-03-27 15:40:52 -04:00 committed by MongoDB Bot
parent 5f7e594d07
commit 5cc1d48094
10 changed files with 521 additions and 54 deletions

View File

@ -528,6 +528,8 @@ last-continuous:
ticket: SERVER-112273
- test_file: jstests/replsets/election_term_over_int_max.js
ticket: SERVER-119413
- test_file: jstests/sharding/resharding_timeseries/reshard_timeseries_force_redistribution.js
ticket: SERVER-111411
- test_file: jstests/replsets/oplog_fetch_lag_metric.js
ticket: SERVER-116300
suites: null
@ -1115,6 +1117,8 @@ last-lts:
ticket: SERVER-112273
- test_file: jstests/replsets/election_term_over_int_max.js
ticket: SERVER-119413
- test_file: jstests/sharding/resharding_timeseries/reshard_timeseries_force_redistribution.js
ticket: SERVER-111411
- test_file: jstests/replsets/oplog_fetch_lag_metric.js
ticket: SERVER-116300
suites: null

View File

@ -102,6 +102,8 @@ export var ReshardingTest = class {
this._primaryShardName = undefined;
/** @private */
this._underlyingSourceNs = undefined;
/** @private */
this._timeseriesMetaField = undefined;
// Properties set by startReshardingInBackground() and withReshardingInBackground().
/** @private */
@ -336,24 +338,7 @@ export var ReshardingTest = class {
this._primaryShardName = primaryShardName;
let tempCollNamePrefix = "system.resharding";
// TODO SERVER-101784 simplify once only viewless timeseries collections exist.
if (collOptions.timeseries !== undefined && !areViewlessTimeseriesEnabled(this._st.s)) {
let bucketUUID = getUUIDFromListCollections(
sourceDB, getTimeseriesBucketsColl(sourceCollection.getName()));
assert.neq(bucketUUID, null, `can't find ns: ${this._ns} after creating chunks`);
this._sourceCollectionUUID = bucketUUID;
tempCollNamePrefix = getTimeseriesBucketsColl("resharding");
this._underlyingSourceNs =
`${sourceDB.getName()}.${getTimeseriesBucketsColl(sourceCollection.getName())}`;
} else {
this._sourceCollectionUUID = getUUIDFromListCollections(sourceDB, collName);
this._underlyingSourceNs = this._ns;
}
const tempCollNamePrefix = this._setupTimeseriesState(sourceDB, collName, collOptions);
const sourceCollectionUUIDString = extractUUIDFromObject(this._sourceCollectionUUID);
this._tempNs = `${sourceDB.getName()}.${tempCollNamePrefix}.${sourceCollectionUUIDString}`;
@ -391,27 +376,9 @@ export var ReshardingTest = class {
CreateShardedCollectionUtil.shardCollectionWithChunks(
sourceCollection, shardKeyPattern, chunks, collOptions);
let tempCollNamePrefix = "system.resharding";
// TODO SERVER-101784 simplify once only viewless timeseries collections exist.
if (collOptions.timeseries !== undefined && !areViewlessTimeseriesEnabled(this._st.s)) {
let bucketUUID = getUUIDFromListCollections(
sourceDB, getTimeseriesBucketsColl(sourceCollection.getName()));
assert.neq(bucketUUID, null, `can't find ns: ${this._ns} after creating chunks`);
this._sourceCollectionUUID = bucketUUID;
tempCollNamePrefix = getTimeseriesBucketsColl("resharding");
this._underlyingSourceNs =
`${sourceDB.getName()}.${getTimeseriesBucketsColl(sourceCollection.getName())}`;
} else {
this._sourceCollectionUUID =
getUUIDFromListCollections(sourceDB, sourceCollection.getName());
this._underlyingSourceNs = this._ns;
}
const tempCollNamePrefix =
this._setupTimeseriesState(sourceDB, sourceCollection.getName(), collOptions);
const sourceCollectionUUIDString = extractUUIDFromObject(this._sourceCollectionUUID);
this._tempNs = `${sourceDB.getName()}.${tempCollNamePrefix}.${sourceCollectionUUIDString}`;
return sourceCollection;
@ -1038,6 +1005,64 @@ export var ReshardingTest = class {
}
}
/**
* Sets up internal state for timeseries collections, including UUID, namespace, and metaField.
* Handles both viewless and non-viewless timeseries collections.
* @private
* @returns {string} The temp collection name prefix to use.
*/
_setupTimeseriesState(sourceDB, collName, collOptions) {
let tempCollNamePrefix = "system.resharding";
// TODO SERVER-101784 simplify once only viewless timeseries collections exist.
if (collOptions.timeseries !== undefined && !areViewlessTimeseriesEnabled(this._st.s)) {
let bucketUUID =
getUUIDFromListCollections(sourceDB, getTimeseriesBucketsColl(collName));
assert.neq(bucketUUID, null, `can't find ns: ${this._ns} after creating chunks`);
this._sourceCollectionUUID = bucketUUID;
this._timeseriesMetaField = collOptions.timeseries.metaField;
tempCollNamePrefix = getTimeseriesBucketsColl("resharding");
this._underlyingSourceNs =
`${sourceDB.getName()}.${getTimeseriesBucketsColl(collName)}`;
} else {
this._sourceCollectionUUID = getUUIDFromListCollections(sourceDB, collName);
this._underlyingSourceNs = this._ns;
// For viewless timeseries, still store metaField for shard key translation.
if (collOptions.timeseries !== undefined) {
this._timeseriesMetaField = collOptions.timeseries.metaField;
}
}
return tempCollNamePrefix;
}
/**
* Translates a user-provided shard key to the internal format for timeseries collections.
* For timeseries collections, the metaField name (e.g., "metadata") is replaced with "meta".
* For non-timeseries collections, returns the shard key unchanged.
* @private
*/
_translateTimeseriesShardKey(shardKey) {
if (this._timeseriesMetaField === undefined) {
return shardKey;
}
const translatedKey = {};
for (const [field, value] of Object.entries(shardKey)) {
if (field === this._timeseriesMetaField) {
translatedKey["meta"] = value;
} else if (field.startsWith(this._timeseriesMetaField + ".")) {
translatedKey["meta" + field.substring(this._timeseriesMetaField.length)] = value;
} else {
translatedKey[field] = value;
}
}
return translatedKey;
}
/** @private */
_checkCoordinatorPostState(expectedErrorCode) {
assert.eq(
@ -1071,12 +1096,17 @@ export var ReshardingTest = class {
`didn't find config.collections entry for ${this._underlyingSourceNs}`);
if (expectedErrorCode === ErrorCodes.OK) {
assert.eq(this._newShardKey,
collEntry.key,
"shard key pattern didn't change despite resharding having succeeded");
assert.neq(this._sourceCollectionUUID,
collEntry.uuid,
"collection UUID didn't change despite resharding having succeeded");
const expectedShardKey = this._translateTimeseriesShardKey(this._newShardKey);
assert.eq(
expectedShardKey,
collEntry.key,
"shard key pattern didn't change despite resharding having succeeded",
);
assert.neq(
this._sourceCollectionUUID,
collEntry.uuid,
"collection UUID didn't change despite resharding having succeeded",
);
} else {
assert.eq(this._currentShardKey,
collEntry.key,

View File

@ -0,0 +1,63 @@
/**
* Tests that resharding a timeseries collection with forceRedistribution works correctly
* when using the same shard key.
*
* @tags: [
* requires_fcv_80,
* featureFlagReshardingForTimeseries,
* ]
*/
import {ReshardingTest} from "jstests/sharding/libs/resharding_test_fixture.js";
const dbName = jsTestName();
const collName = "coll";
const ns = `${dbName}.${collName}`;
const reshardingTest = new ReshardingTest({numDonors: 2, numRecipients: 2, reshardInPlace: true});
reshardingTest.setup();
const donorShardNames = reshardingTest.donorShardNames;
const timeseriesInfo = {
timeField: "ts",
metaField: "metadata",
};
// This will be translated internally to {"meta.x": 1}.
const shardKeyPattern = {
"metadata.x": 1,
};
const coll = reshardingTest.createShardedCollection({
ns: ns,
shardKeyPattern: shardKeyPattern,
chunks: [
{min: {"meta.x": MinKey}, max: {"meta.x": 0}, shard: donorShardNames[0]},
{min: {"meta.x": 0}, max: {"meta.x": MaxKey}, shard: donorShardNames[1]},
],
collOptions: {
timeseries: timeseriesInfo,
},
});
assert.commandWorked(
coll.insert([
{data: 1, ts: new Date(), metadata: {x: -1, y: -1}},
{data: 2, ts: new Date(), metadata: {x: 1, y: 1}},
]),
);
reshardingTest.withReshardingInBackground(
{
newShardKeyPattern: shardKeyPattern,
newChunks: [
{min: {"meta.x": MinKey}, max: {"meta.x": 0}, shard: donorShardNames[0]},
{min: {"meta.x": 0}, max: {"meta.x": MaxKey}, shard: donorShardNames[1]},
],
forceRedistribution: true,
},
() => {},
);
reshardingTest.teardown();

View File

@ -1685,6 +1685,7 @@ mongo_cc_unit_test(
"session_catalog_migration_destination_test.cpp",
"session_catalog_migration_source_test.cpp",
"shard_key_index_util_test.cpp",
"shard_key_util_test.cpp",
"shard_local_test.cpp",
"shard_metadata_util_test.cpp",
"shard_server_catalog_cache_loader_test.cpp",

View File

@ -139,12 +139,24 @@ ExecutorFuture<void> ReshardCollectionCoordinator::_runImpl(
<< "' not found in cluster catalog",
cmOld.hasRoutingTable());
auto provenance = _doc.getProvenance();
auto currentShardKey = cmOld.getShardKeyPattern().getKeyPattern().toBSON();
BSONObj translatedKey;
if (cmOld.isTimeseriesCollection() &&
resharding::isOrdinaryReshardCollection(provenance)) {
auto tsOptions = cmOld.getTimeseriesFields().get().getTimeseriesOptions();
translatedKey =
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, _doc.getKey());
}
StateDoc newDoc(_doc);
newDoc.setOldShardKey(cmOld.getShardKeyPattern().getKeyPattern().toBSON());
newDoc.setOldCollectionUUID(cmOld.getUUID());
_updateStateDocument(opCtx, std::move(newDoc));
ConfigsvrReshardCollection configsvrReshardCollection(nss(), _doc.getKey());
auto finalShardKey = translatedKey.isEmpty() ? _doc.getKey() : translatedKey;
ConfigsvrReshardCollection configsvrReshardCollection(nss(), finalShardKey);
configsvrReshardCollection.setDbName(nss().dbName());
configsvrReshardCollection.setUnique(_doc.getUnique());
configsvrReshardCollection.setCollation(_doc.getCollation());
@ -184,7 +196,6 @@ ExecutorFuture<void> ReshardCollectionCoordinator::_runImpl(
_doc.getPerformVerification());
configsvrReshardCollection.setPerformVerification(_doc.getPerformVerification());
auto provenance = _doc.getProvenance();
if (resharding::isMoveCollection(provenance)) {
uassert(ErrorCodes::NamespaceNotFound,
str::stream()
@ -230,7 +241,7 @@ ExecutorFuture<void> ReshardCollectionCoordinator::_runImpl(
"when using the forceRedistribution option. The "
"forceRedistribution option is meant for redistributing the "
"collection to a different set of shards.",
cmOld.getShardKeyPattern().isShardKey(_doc.getKey()));
cmOld.getShardKeyPattern().isShardKey(finalShardKey));
}
}

View File

@ -514,6 +514,10 @@ bool isUnshardCollection(const boost::optional<ReshardingProvenanceEnum>& proven
return provenance && provenance.get() == ReshardingProvenanceEnum::kUnshardCollection;
}
bool isOrdinaryReshardCollection(const boost::optional<ReshardingProvenanceEnum>& provenance) {
return provenance && provenance.get() == ReshardingProvenanceEnum::kReshardCollection;
}
std::shared_ptr<ThreadPool> makeThreadPoolForMarkKilledExecutor(const std::string& poolName) {
return std::make_shared<ThreadPool>([&] {
ThreadPool::Options options;
@ -561,17 +565,18 @@ ReshardingCoordinatorDocument createReshardingCoordinatorDoc(
auto existingUUID = collEntry.getUuid();
auto shardKeySpec = request.getKey();
// moveCollection/unshardCollection are called with _id as the new shard key since
// that's an acceptable value for tracked unsharded collections so we can skip this.
// TODO: SERVER-119524 Remove this after 8.3 becomes last LTS.
// We need to perform translation of shard key for timeseries collections if the shard key is
// not already translated to raw key format to provide backwards compatibility.
if (collEntry.getTimeseriesFields() &&
(!setProvenance ||
(*request.getProvenance() == ReshardingProvenanceEnum::kReshardCollection))) {
auto tsOptions = collEntry.getTimeseriesFields().get().getTimeseriesOptions();
shardkeyutil::validateTimeseriesShardKey(
tsOptions.getTimeField(), tsOptions.getMetaField(), request.getKey());
shardKeySpec =
uassertStatusOK(timeseries::createBucketsShardKeySpecFromTimeseriesShardKeySpec(
tsOptions, request.getKey()));
auto isTranslated = shardkeyutil::isRawTimeseriesShardKey(tsOptions, request.getKey());
if (!isTranslated) {
shardKeySpec =
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, request.getKey());
}
}
auto tempReshardingNss = resharding::constructTemporaryReshardingNss(nss, collEntry.getUuid());

View File

@ -412,6 +412,11 @@ void validateShardDistribution(const std::vector<ShardKeyRange>& shardDistributi
OperationContext* opCtx,
const ShardKeyPattern& keyPattern);
/**
* Returns true if the provenance is reshardcollection.
*/
bool isOrdinaryReshardCollection(const boost::optional<ReshardingProvenanceEnum>& provenance);
/**
* Returns true if the provenance is moveCollection or balancerMoveCollection.
*/

View File

@ -352,6 +352,35 @@ void validateTimeseriesShardKey(StringData timeFieldName,
}
}
BSONObj validateAndTranslateTimeseriesShardKey(const TimeseriesOptions& tsOptions,
const BSONObj& tsShardKey) {
shardkeyutil::validateTimeseriesShardKey(
tsOptions.getTimeField(), tsOptions.getMetaField(), tsShardKey);
return uassertStatusOK(
timeseries::createBucketsShardKeySpecFromTimeseriesShardKeySpec(tsOptions, tsShardKey));
}
bool isRawTimeseriesShardKey(const TimeseriesOptions& tsOptions, const BSONObj& tsShardKey) {
if (tsShardKey.isEmpty()) {
return false;
}
const auto& timeFieldName = tsOptions.getTimeField();
const auto& metaFieldName = tsOptions.getMetaField();
for (const auto& elem : tsShardKey) {
const auto& fieldName = elem.fieldNameStringData();
if (fieldName == timeFieldName ||
(metaFieldName &&
(fieldName == *metaFieldName || fieldName.starts_with(*metaFieldName + ".")))) {
return false;
}
}
// After checking if any field matches user-facing field names, we need to check if it is a
// valid bucket format key to reject invalid keys.
return timeseries::createTimeseriesIndexFromBucketsIndexSpec(tsOptions, tsShardKey).has_value();
}
// TODO: SERVER-64187 move calls to validateShardKeyIsNotEncrypted into
// validateShardKeyIndexExistsOrCreateIfPossible
void validateShardKeyIsNotEncrypted(OperationContext* opCtx,

View File

@ -242,5 +242,20 @@ void validateTimeseriesShardKey(StringData timeFieldName,
boost::optional<StringData> metaFieldName,
const BSONObj& shardKeyPattern);
/**
* Validates that 'tsShardKey' uses the user-facing time-series field names (timeField and
* metaField), then translates it to the internal buckets collection format.
* Throws if the shard key contains fields other than the defined timeField or metaField.
*/
BSONObj validateAndTranslateTimeseriesShardKey(const TimeseriesOptions& tsOptions,
const BSONObj& tsShardKey);
/**
* Returns true if the given shard key is a valid key already in the internal buckets collection
* raw key format. Returns false if the key uses user-facing field names (requiring translation)
* or if the key is not a recognized timeseries shard key.
*/
bool isRawTimeseriesShardKey(const TimeseriesOptions& tsOptions, const BSONObj& tsShardKey);
} // namespace shardkeyutil
} // namespace mongo

View File

@ -0,0 +1,304 @@
/**
* Copyright (C) 2026-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/s/shard_key_util.h"
#include "mongo/base/string_data.h"
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobj.h"
#include "mongo/db/timeseries/timeseries_gen.h"
#include "mongo/unittest/assert.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/assert_util.h"
#include <string>
#include <boost/optional/optional.hpp>
namespace mongo {
namespace {
TimeseriesOptions makeTimeseriesOptions(StringData timeField,
boost::optional<StringData> metaField) {
TimeseriesOptions options{std::string{timeField}};
if (metaField) {
options.setMetaField(*metaField);
}
return options;
}
// Tests for isRawTimeseriesShardKey
TEST(IsRawTimeseriesShardKeyTest, UserFacingTimeFieldKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("time" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, UserFacingMetaFieldKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("hostId" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, UserFacingMetaSubfieldKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("hostId.x" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, UserFacingCompoundKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(
shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("hostId" << 1 << "time" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedMetaFieldKeyIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("meta" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedMetaSubfieldKeyIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("meta.x" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedTimeFieldKeyIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("control.min.time" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedCompoundKeyIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(
tsOptions, BSON("meta" << 1 << "control.min.time" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, MetaFieldNamedMetaIsHandledCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("meta"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("meta" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TimeFieldOnlyNoMetaFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("ts", boost::none);
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("ts" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedTimeFieldNoMetaFieldIsTranslated) {
auto tsOptions = makeTimeseriesOptions("ts", boost::none);
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("control.min.ts" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, HashedMetaFieldKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("hostId.x" << "hashed")));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedHashedMetaFieldKeyIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("meta.x" << "hashed")));
}
TEST(IsRawTimeseriesShardKeyTest, InvalidIdFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("_id" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, InvalidArbitraryFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("someRandomField" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, EmptyKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSONObj()));
}
TEST(IsRawTimeseriesShardKeyTest, ControlMaxTimeFieldIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("control.max.time" << -1)));
}
TEST(IsRawTimeseriesShardKeyTest, ControlWithoutMinMaxIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("control" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, ControlMinWithWrongFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(
shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("control.min.wrongField" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, MixedValidAndInvalidFieldsIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("meta" << 1 << "_id" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, MixedUserFacingAndTranslatedFieldsIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(
shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("hostId" << 1 << "meta" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, MixedTranslatedTimeAndInvalidFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(
tsOptions, BSON("control.min.time" << 1 << "_id" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, MixedValidMetaAndInvalidControlFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(
tsOptions, BSON("meta" << 1 << "control.min.wrongField" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, TranslatedDescendingCompoundKeyIsTranslated) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(
tsOptions, BSON("meta" << 1 << "control.max.time" << -1 << "control.min.time" << -1)));
}
TEST(IsRawTimeseriesShardKeyTest, NoMetaFieldWithInvalidFieldIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("ts", boost::none);
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("_id" << 1)));
}
TEST(IsRawTimeseriesShardKeyTest, NoMetaFieldWithMetaKeyIsNotTranslated) {
auto tsOptions = makeTimeseriesOptions("ts", boost::none);
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, BSON("meta" << 1)));
}
// Tests for validateAndTranslateTimeseriesShardKey
TEST(ValidateAndTranslateTimeseriesShardKeyTest, MetaFieldKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto result =
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("hostId" << 1));
ASSERT_BSONOBJ_EQ(result, BSON("meta" << 1));
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, MetaSubfieldKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto result =
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("hostId.x" << 1));
ASSERT_BSONOBJ_EQ(result, BSON("meta.x" << 1));
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, TimeFieldKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto result =
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("time" << 1));
ASSERT_BSONOBJ_EQ(result, BSON("control.min.time" << 1));
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, CompoundKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto result = shardkeyutil::validateAndTranslateTimeseriesShardKey(
tsOptions, BSON("hostId" << 1 << "time" << 1));
ASSERT_BSONOBJ_EQ(result, BSON("meta" << 1 << "control.min.time" << 1));
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, HashedMetaFieldKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto result = shardkeyutil::validateAndTranslateTimeseriesShardKey(
tsOptions, BSON("hostId.x" << "hashed"));
ASSERT_BSONOBJ_EQ(result, BSON("meta.x" << "hashed"));
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, DescendingTimeFieldKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto result =
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("time" << -1));
ASSERT_BSONOBJ_EQ(result, BSON("control.max.time" << -1 << "control.min.time" << -1));
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, InvalidFieldThrows5914001) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_THROWS_CODE(
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("_id" << 1)),
AssertionException,
5914001);
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, InvalidArbitraryFieldThrows5914001) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_THROWS_CODE(shardkeyutil::validateAndTranslateTimeseriesShardKey(
tsOptions, BSON("someRandomField" << 1)),
AssertionException,
5914001);
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, TimeFieldNotAtEndThrows5914000) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_THROWS_CODE(shardkeyutil::validateAndTranslateTimeseriesShardKey(
tsOptions, BSON("time" << 1 << "hostId" << 1)),
AssertionException,
5914000);
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, TimeFieldWithHashedThrows880031) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
ASSERT_THROWS_CODE(
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("time" << "hashed")),
AssertionException,
880031);
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, NoMetaFieldWithMetaKeyThrows5914001) {
auto tsOptions = makeTimeseriesOptions("time", boost::none);
ASSERT_THROWS_CODE(
shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("someField" << 1)),
AssertionException,
5914001);
}
TEST(ValidateAndTranslateTimeseriesShardKeyTest, NoMetaFieldTimeKeyTranslatesCorrectly) {
auto tsOptions = makeTimeseriesOptions("ts", boost::none);
auto result = shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, BSON("ts" << 1));
ASSERT_BSONOBJ_EQ(result, BSON("control.min.ts" << 1));
}
TEST(TimeseriesShardKeyValidationFlowTest, InvalidKeyIsRejectedByValidationFlow) {
auto tsOptions = makeTimeseriesOptions("time", StringData("hostId"));
auto invalidKey = BSON("_id" << 1);
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, invalidKey));
ASSERT_THROWS_CODE(shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, invalidKey),
AssertionException,
5914001);
auto userFacingKey = BSON("hostId" << 1);
ASSERT_FALSE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, userFacingKey));
auto result = shardkeyutil::validateAndTranslateTimeseriesShardKey(tsOptions, userFacingKey);
ASSERT_BSONOBJ_EQ(result, BSON("meta" << 1));
auto translatedKey = BSON("meta" << 1);
ASSERT_TRUE(shardkeyutil::isRawTimeseriesShardKey(tsOptions, translatedKey));
}
} // namespace
} // namespace mongo