SERVER-110919 Ban hint on '_id_' for timeseries collections (#41961)

GitOrigin-RevId: 4d73214d504aa32e91911ae0fe40fbe87af9a039
This commit is contained in:
Adithi Raghavan 2025-11-06 12:15:07 -05:00 committed by MongoDB Bot
parent 0c4afc24f0
commit 911c390cb7
4 changed files with 144 additions and 1 deletions

View File

@ -44,6 +44,7 @@ suites:
- jstests/core/timeseries/write/timeseries_delete_with_meta_concurrent.js
- jstests/core/timeseries/write/timeseries_findAndModify_deletes_hints.js
- jstests/core/timeseries/write/timeseries_findAndModify_updates_hints.js
- jstests/core/timeseries/write/timeseries_hint_id.js
- jstests/core/timeseries/write/timeseries_insert.js
- jstests/core/timeseries/write/timeseries_insert_after_delete.js
- jstests/core/timeseries/write/timeseries_insert_after_update.js

View File

@ -45,6 +45,7 @@ suites:
- jstests/core/timeseries/write/timeseries_delete_with_meta_concurrent.js
- jstests/core/timeseries/write/timeseries_findAndModify_deletes_hints.js
- jstests/core/timeseries/write/timeseries_findAndModify_updates_hints.js
- jstests/core/timeseries/write/timeseries_hint_id.js
- jstests/core/timeseries/write/timeseries_insert.js
- jstests/core/timeseries/write/timeseries_insert_after_delete.js
- jstests/core/timeseries/write/timeseries_insert_after_update.js

View File

@ -0,0 +1,131 @@
/**
* Tests running the find and delete commands with a hint on the _id index on a timeseries
* collection. Verifies that commands specifying a hint on the _id index via "_id_" always fail.
* @tags: [
* # This is a backwards-breaking change.
* requires_fcv_83,
* does_not_support_stepdowns,
* # Retryable deletes, for example, not supported on timeseries collections
* does_not_support_retryable_writes,
* requires_non_retryable_commands,
* requires_non_retryable_writes,
* # We need a timeseries collection.
* requires_timeseries,
* ]
*/
const timeFieldName = "time";
const metaFieldName = "tag";
const collName = jsTestName();
const dbName = jsTestName();
const testDB = db.getSiblingDB(dbName);
const coll = testDB.getCollection(collName);
assert.commandWorked(
testDB.createCollection(coll.getName(), {timeseries: {timeField: timeFieldName, metaField: metaFieldName}}),
);
const objA = {
[timeFieldName]: ISODate(),
"measurement": {"A": "cpu"},
[metaFieldName]: {a: "A"},
};
const objB = {
[timeFieldName]: ISODate(),
"measurement": {"A": "cpu"},
[metaFieldName]: {b: "B"},
};
const objC = {
[timeFieldName]: ISODate(),
"measurement": {"A": "cpu"},
[metaFieldName]: {c: "C"},
};
assert.commandWorked(coll.insert([objA, objB, objC]));
function runCommandOnTSCollection(commandWithHintObjectValue, commandWithHintStringValue) {
// No user-facing index exists on the _id field. It only exists on the underlying buckets
// collection.
assert.commandFailedWithCode(testDB.runCommand(commandWithHintObjectValue), ErrorCodes.BadValue);
assert.commandFailedWithCode(testDB.runCommand(commandWithHintStringValue), ErrorCodes.BadValue);
// Create an index on the _id field of the user-facing view collection.
assert.commandWorked(coll.createIndex({_id: 1}));
// This hint will be rewritten as the key pattern (ex:
// "hint":{"control.min._id":1,"control.max._id":1}) for the buckets collection.
assert.commandWorked(testDB.runCommand(commandWithHintObjectValue));
assert.commandFailedWithCode(testDB.runCommand(commandWithHintStringValue), ErrorCodes.BadValue);
// Drop the index on the _id field of the user-facing view collection.
assert.commandWorked(coll.dropIndex({_id: 1}));
}
(function runFindCommand() {
runCommandOnTSCollection({find: collName, hint: {_id: 1}}, {find: collName, hint: "_id_"});
})();
(function runDeleteCommand() {
runCommandOnTSCollection(
{delete: collName, deletes: [{q: {metaFieldName: "b"}, limit: 0, hint: {_id: 1}}]},
{delete: collName, deletes: [{q: {metaFieldName: "c"}, limit: 0, hint: "_id_"}]},
);
assert.commandWorked(coll.insert([objB, objC]));
})();
(function runAggregateCommand() {
assert.commandWorked(coll.insert([objA]));
const matchPipeline = [{$match: {metaFieldName: "a"}}, {$sort: {_id: 1}}, {$project: {_id: 0, time: 0}}];
runCommandOnTSCollection(
{
aggregate: collName,
pipeline: matchPipeline,
cursor: {},
allowDiskUse: true,
hint: {_id: 1},
},
{
aggregate: collName,
pipeline: matchPipeline,
cursor: {},
allowDiskUse: true,
hint: "_id_",
},
);
})();
(function runUpdateCommand() {
runCommandOnTSCollection(
{
update: collName,
updates: [
{
q: {tag: "b"},
u: {$set: {tag: {b: "RAM"}}},
hint: {_id: 1},
multi: true,
},
],
},
{
update: collName,
updates: [
{
q: {tag: "c"},
u: {$set: {tag: {c: "L1"}}},
hint: "_id_",
multi: true,
},
],
},
);
})();
(function runCountCommand() {
runCommandOnTSCollection(
{count: collName, query: {metaFieldName: "a"}, hint: {_id: 1}},
{count: collName, query: {metaFieldName: "a"}, hint: "_id_"},
);
})();

View File

@ -44,6 +44,7 @@
#include "mongo/bson/bsonobj.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/bson/bsontypes.h"
#include "mongo/db/index/index_constants.h"
#include "mongo/db/index_names.h"
#include "mongo/db/local_catalog/clustered_collection_options_gen.h"
#include "mongo/db/matcher/expression_algo.h"
@ -978,7 +979,16 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
boost::optional<BSONObj> hintedIndexBson = boost::none;
if (!params.indexFiltersApplied && !params.querySettingsApplied) {
if (auto hintObj = query.getFindCommandRequest().getHint(); !hintObj.isEmpty()) {
hintedIndexBson = hintObj;
if (params.mainCollectionInfo.stats.isTimeseries &&
hintObj.firstElement().valueStringDataSafe() == IndexConstants::kIdIndexName) {
// The index name '_id_' is reserved for the _id index on the underlying buckets
// collection of a timeseries collection. The user will never be able to specify the
// _id index name '_id_' in the hint.
return Status(ErrorCodes::BadValue,
"cannot hint on '_id_' for timeseries collections");
} else {
hintedIndexBson = hintObj;
}
}
}