SERVER-89953 listIndexes should list the collation if it's simple (#49113)

GitOrigin-RevId: 6577e498b0ecff23de975e98dd7e36d2759f8857
This commit is contained in:
Silvia Surroca 2026-04-15 15:23:27 +02:00 committed by MongoDB Bot
parent 40e73bbe54
commit e291f89d5c
51 changed files with 660 additions and 140 deletions

View File

@ -2701,8 +2701,7 @@ tasks:
resmoke_args: >-
--mongodSetParameters='{logComponentVerbosity: {command: 2}}'
--runNoFeatureFlagTests
jstestfuzz_vars: --metaSeed 1726779665485 --jstestfuzzGitRev d4c83dd
jstestfuzz_vars: --metaSeed 1726779665485 --jstestfuzzGitRev 68fe9b4
# jstestfuzz standalone update time-series generational fuzzer ##
- <<: *jstestfuzz_template
name: update_timeseries_fuzzer_gen

View File

@ -7,6 +7,7 @@
// server-3253 Unsharded support for $out
import {anyEq, assertErrorCode, collectionExists} from "jstests/aggregation/extras/utils.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
const testDb = db.getSiblingDB("unsharded_out");
let input = testDb.unsharded_out_in;
@ -19,13 +20,19 @@ inputDoesntExist.drop(); // never created
output.drop();
function getOutputIndexes() {
return output.getIndexes().sort(function (a, b) {
if (a.name < b.name) {
return -1;
} else {
return 1;
}
});
return output
.getIndexes()
.map(function (index) {
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
return IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, index);
})
.sort(function (a, b) {
if (a.name < b.name) {
return -1;
} else {
return 1;
}
});
}
function test(input, pipeline, expected) {

View File

@ -10,6 +10,7 @@
import {validateClusteredCollectionHint} from "jstests/libs/clustered_collections/clustered_collection_hint_common.js";
import {assertDropCollection} from "jstests/libs/collection_drop_recreate.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {getWinningPlanFromExplain} from "jstests/libs/query/analyze_plan.js";
const collatedName = "clustered_collection_with_collation";
@ -56,7 +57,7 @@ const expectedCollation = {
const indexes = collated.getIndexes();
assert.eq(0, bsonWoCompare(indexes[0].collation, expectedCollation), "Default index doesn't match expected collation");
// No collation spec when it's set to "simple".
// Collation spec for "simple" is now explicitly included.
assertDropCollection(db, "simpleCollation");
assert.commandWorked(
db.createCollection("simpleCollation", {
@ -64,8 +65,9 @@ assert.commandWorked(
collation: {locale: "simple"},
}),
);
const indexSpec = db.simpleCollation.getIndexes()[0];
assert(!indexSpec.hasOwnProperty("collation"), 'Default index has collation for "simple" locale');
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
const indexSpec = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, db.simpleCollation.getIndexes()[0]);
assert.eq(indexSpec.collation.locale, "simple", "Default index should have simple collation");
const insertDocuments = function (coll) {
assert.commandWorked(coll.insert({_id: 5}));

View File

@ -6,7 +6,6 @@
* non-replicated collections.
*
* @tags: [
* requires_fcv_53,
* does_not_support_stepdowns,
* # Write and read on "local" db with multi clients cannot match the expected response.
* multi_clients_incompatible,
@ -16,6 +15,7 @@ import {ClusteredCollectionUtil} from "jstests/libs/clustered_collections/cluste
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
import {PersistenceProviderUtil} from "jstests/libs/server-rss/persistence_provider_util.js";
import {add2dsphereVersionIfNeeded} from "jstests/libs/query/geo_index_version_helpers.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
const validateCompoundSecondaryIndexes = function (db, coll, clusterKey) {
const clusterKeyField = Object.keys(clusterKey)[0];
@ -44,15 +44,23 @@ const overrideIndexType = function (clusterKey, indexType) {
const validateCreateIndexOnClusterKey = function (db, collName, fullCreateOptions) {
const clusterKey = fullCreateOptions.clusteredIndex.key;
const listIndexes0 = assert.commandWorked(db[collName].runCommand("listIndexes"));
const listIndexesClusteredIndex = listIndexes0.cursor.firstBatch[0];
let listIndexes0 = db[collName].getIndexes();
const listIndexesClusteredIndex = listIndexes0[0];
// Expect listIndexes to append the 'clustered' field to it's clusteredIndex output.
assert.docEq(listIndexesClusteredIndex.key, clusterKey);
assert.eq(listIndexesClusteredIndex.clustered, true);
// no-op with the 'clustered' option.
assert.commandWorked(db[collName].runCommand({createIndexes: collName, indexes: [listIndexesClusteredIndex]}));
// createIndexes should be a no-op with the 'clustered' option.
// Remove the 'collation' field from the clustered index spec, since specifying a collation is
// not allowed for clustered indexes. This is because clustered indexes are always getting the
// collection default collation.
let clusteredIndexSpec = {...listIndexesClusteredIndex};
if (clusteredIndexSpec.collation) {
delete clusteredIndexSpec.collation;
}
assert.commandWorked(db[collName].runCommand({createIndexes: collName, indexes: [clusteredIndexSpec]}));
// no-op without the 'clustered' option.
assert.commandWorked(db[collName].createIndex(clusterKey));
@ -72,11 +80,14 @@ const validateCreateIndexOnClusterKey = function (db, collName, fullCreateOption
);
// The listIndexes output should be unchanged.
const listIndexes1 = assert.commandWorked(db[collName].runCommand("listIndexes"));
assert.eq(listIndexes1.cursor.firstBatch.length, listIndexes0.cursor.firstBatch.length);
for (let i = 0; i < listIndexes1.cursor.firstBatch.length; ++i) {
assert.docEq(listIndexes1.cursor.firstBatch[i], listIndexes0.cursor.firstBatch[i]);
}
let listIndexes1 = db[collName].getIndexes();
// Standardize index specs to account for possible differences in listIndexes output across FCVs.
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
listIndexes1 = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, listIndexes1);
listIndexes0 = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, listIndexes0);
assert.sameMembers(listIndexes1, listIndexes0);
// It's possible to create 'hashed','2d','2dsphere' and 'text' indexes on the cluster key.
assert.commandWorked(db[collName].createIndex(overrideIndexType(clusterKey, "hashed")));
@ -86,8 +97,8 @@ const validateCreateIndexOnClusterKey = function (db, collName, fullCreateOption
);
assert.commandWorked(db[collName].createIndex(overrideIndexType(clusterKey, "text")));
const finalIndexes = assert.commandWorked(db[collName].runCommand("listIndexes"));
assert.eq(finalIndexes.cursor.firstBatch.length, 5);
const finalIndexes = db[collName].getIndexes();
assert.eq(finalIndexes.length, 5);
};
// It is illegal to drop the clusteredIndex. Verify that the various ways of dropping the

View File

@ -16,6 +16,8 @@
// transitioning_replicaset_incompatible,
// ]
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
const sourceColl = db.irap_cmd;
const adminDB = db.getSiblingDB("admin");
const destDB = db.getSiblingDB("irap_out_db");
@ -41,8 +43,8 @@ let commandObj = {
// Destination has an extra index.
assert.commandFailedWithCode(adminDB.runCommand(commandObj), ErrorCodes.CommandFailed);
let destIndexes = assert.commandWorked(destDB.runCommand({"listIndexes": destColl.getName()}));
commandObj.indexes = new DBCommandCursor(db, destIndexes).toArray();
let destIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(destColl.getIndexes());
commandObj.indexes = destIndexes;
jsTestLog("Testing against destination collection with indexes: " + tojson(commandObj.indexes));
assert.commandWorked(adminDB.runCommand(commandObj));
@ -55,8 +57,10 @@ assert.commandFailedWithCode(adminDB.runCommand(commandObj), ErrorCodes.CommandF
destColl.drop();
assert.commandWorked(destDB.runCommand({"create": destColl.getName(), capped: true, size: 256, max: 2}));
destIndexes = assert.commandWorked(destDB.runCommand({"listIndexes": destColl.getName()}));
commandObj.indexes = new DBCommandCursor(db, destIndexes).toArray();
destIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(
destDB.getCollection(destColl.getName()).getIndexes(),
);
commandObj.indexes = destIndexes;
// Source is missing collection options.
assert.commandFailedWithCode(adminDB.runCommand(commandObj), ErrorCodes.CommandFailed);

View File

@ -15,6 +15,7 @@
import {getTimeseriesCollForRawOps, kRawOperationSpec} from "jstests/core/libs/raw_operation_utils.js";
import {isShardedTimeseries} from "jstests/core/timeseries/libs/viewless_timeseries_util.js";
import {getPlanStage, getPlanStages, getRejectedPlan, getRejectedPlans} from "jstests/libs/query/analyze_plan.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
const coll = db[jsTestName()];
const timeField = "time";
@ -42,6 +43,7 @@ function resetCollection(collation) {
"v": 2,
"key": {"control.min.time": 1, "control.max.time": 1},
"name": "time_1",
"collation": {"locale": "simple"},
};
extraBucketIndexes.push(addCollation(extraBucketIndexesShardedSpec, collation));
}
@ -51,15 +53,24 @@ function resetCollection(collation) {
"v": 2,
"key": {"meta": 1, "control.min.time": 1, "control.max.time": 1},
"name": "m_1_time_1",
"collation": {"locale": "simple"},
};
extraBucketIndexes.push(addCollation(extraBucketIndexesSpec, collation));
}
resetCollection();
// Check that there is no collation in the default index.
assert.eq(coll.getIndexes()[0].collation, null);
assert.sameMembers(getTimeseriesCollForRawOps(coll).getIndexes(kRawOperationSpec), extraBucketIndexes);
let index = coll.getIndexes()[0];
let actualIndexes = getTimeseriesCollForRawOps(coll).getIndexes(kRawOperationSpec);
// Check that the default index has simple collation.
// TODO (SERVER-122417) Remove these workarounds once v9.0 branches out.
index = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, index);
actualIndexes = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, actualIndexes);
assert.eq(index.collation.locale, "simple");
assert.sameMembers(actualIndexes, extraBucketIndexes);
assert.commandWorked(
coll.insert([
@ -204,9 +215,19 @@ assert.commandFailedWithCode(coll.createIndex({a: 1}, {partialFilterExpression:
}
assert.commandWorked(coll.dropIndex({a: 1}));
// Check that there is no collation in the default index.
assert.eq(coll.getIndexes()[0].collation, null);
assert.sameMembers(getTimeseriesCollForRawOps(coll).getIndexes(kRawOperationSpec), extraBucketIndexes);
// Check that the default index has simple collation.
let index = coll.getIndexes()[0];
let actualIndexes = getTimeseriesCollForRawOps(coll).getIndexes(kRawOperationSpec);
// Set the collation to simple if it is not present, which happens on older versions.
// TODO (SERVER-122417) Remove these workarounds once v9.0 branches out.
index = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, index);
actualIndexes = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, actualIndexes);
assert.eq(index.collation.locale, "simple");
assert.sameMembers(actualIndexes, extraBucketIndexes);
assert.gt(ixscanInWinningPlan, 0);
}
@ -222,7 +243,12 @@ assert.commandWorked(
},
),
);
const actualBucketIndexes = getTimeseriesCollForRawOps(coll).getIndexes(kRawOperationSpec);
let actualBucketIndexes = getTimeseriesCollForRawOps(coll).getIndexes(kRawOperationSpec);
// Set the collation to simple if it is not present, which happens on older versions.
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
actualBucketIndexes = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, actualBucketIndexes);
assert.sameMembers(
actualBucketIndexes,
extraBucketIndexes.concat([
@ -230,6 +256,7 @@ assert.sameMembers(
"v": 2,
"key": {"control.min.a": 1, "control.max.a": 1},
"name": "a_1",
"collation": {"locale": "simple"},
"partialFilterExpression": {
// Meta predicates are pushed down verbatim.
@ -257,6 +284,7 @@ assert.sameMembers(
{b: {$gte: 0}},
],
},
"collation": {"locale": "simple"},
v: 2,
},
},

View File

@ -32,9 +32,9 @@ const bucketReopeningsFailedCounters = Object.freeze({
export var TimeseriesTest = class {
static verifyAndDropIndex(coll, shouldHaveOriginalSpec, indexName) {
const checkIndexSpec = function (spec, userIndex) {
assert(spec.hasOwnProperty("v"));
assert(spec.hasOwnProperty("name"));
assert(spec.hasOwnProperty("key"));
assert(spec.hasOwnProperty("v"), indexName);
assert(spec.hasOwnProperty("name"), indexName);
assert(spec.hasOwnProperty("key"), indexName);
if (userIndex) {
assert(!spec.hasOwnProperty("originalSpec"));
@ -42,13 +42,13 @@ export var TimeseriesTest = class {
}
if (shouldHaveOriginalSpec) {
assert(spec.hasOwnProperty("originalSpec"));
assert.eq(spec.v, spec.originalSpec.v);
assert.eq(spec.name, spec.originalSpec.name);
assert.neq(spec.key, spec.originalSpec.key);
assert.eq(spec.collation, spec.originalSpec.collation);
assert(spec.hasOwnProperty("originalSpec"), indexName);
assert.eq(spec.v, spec.originalSpec.v, indexName);
assert.eq(spec.name, spec.originalSpec.name, indexName);
assert.neq(spec.key, spec.originalSpec.key, indexName);
assert.eq(spec.collation, spec.originalSpec.collation, indexName);
} else {
assert(!spec.hasOwnProperty("originalSpec"));
assert(!spec.hasOwnProperty("originalSpec"), indexName);
}
};
let sawIndex = false;

View File

@ -21,6 +21,7 @@
* ]
*/
import {TimeseriesAggTests} from "jstests/core/timeseries/libs/timeseries_agg_helpers.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {
runningWithViewlessTimeseriesUpgradeDowngrade,
isViewfulTimeseriesOnlySuite,
@ -108,7 +109,10 @@ function runOutAndCompareResults({
}
let containsDefaultIndex = false;
for (const index of outColl.getIndexes()) {
for (let index of outColl.getIndexes()) {
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
index = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(testDB, index);
if (index == timeseriesDefaultIndex() || bsonUnorderedFieldsCompare(index, timeseriesDefaultIndex()) == 0) {
containsDefaultIndex = true;
break;
@ -179,6 +183,7 @@ function timeseriesDefaultIndex() {
[timeField]: 1,
},
"name": metaField + "_1_" + timeField + "_1",
"collation": {"locale": "simple"},
};
}

View File

@ -8,6 +8,7 @@
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
let kDbName = db.getName();
@ -402,7 +403,11 @@ db.foo.drop();
assert.commandWorked(db.createCollection("foo", {collation: {locale: "en_US"}}));
assert.commandWorked(db.adminCommand({shardCollection: kDbName + ".foo", key: {a: 1}, collation: {locale: "simple"}}));
let indexSpec = getIndexSpecByName(db.foo, "a_1");
assert(!indexSpec.hasOwnProperty("collation"));
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
indexSpec = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, indexSpec);
assert.eq(indexSpec.collation.locale, "simple");
jsTestLog(
"shardCollection should succeed for the key pattern {a: 1} if there are two indexes on {a: 1} and one has the simple collation.",
@ -448,7 +453,11 @@ assert.commandWorked(db.createCollection("foo"));
assert.commandWorked(sh.shardCollection(kDbName + ".foo", {a: 1}));
indexSpec = getIndexSpecByName(db.foo, "a_1");
assert(!indexSpec.hasOwnProperty("unique"), tojson(indexSpec));
assert(!indexSpec.hasOwnProperty("collation"), tojson(indexSpec));
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
indexSpec = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, indexSpec);
assert.eq(indexSpec.collation.locale, "simple", tojson(indexSpec));
jsTestLog('shardCollection() propagates the value for "unique".');
db.foo.drop();
@ -477,7 +486,11 @@ assert.commandWorked(db.createCollection("foo"));
assert.commandFailed(sh.shardCollection(kDbName + ".foo", {a: 1}, false, {collation: {locale: "en_US"}}));
assert.commandWorked(sh.shardCollection(kDbName + ".foo", {a: 1}, false, {collation: {locale: "simple"}}));
indexSpec = getIndexSpecByName(db.foo, "a_1");
assert(!indexSpec.hasOwnProperty("collation"), tojson(indexSpec));
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
indexSpec = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, indexSpec);
assert.eq(indexSpec.collation.locale, "simple", tojson(indexSpec));
db.foo.drop();
assert.commandWorked(db.createCollection("foo", {collation: {locale: "en_US"}}));
@ -485,4 +498,8 @@ assert.commandFailed(sh.shardCollection(kDbName + ".foo", {a: 1}));
assert.commandFailed(sh.shardCollection(kDbName + ".foo", {a: 1}, false, {collation: {locale: "en_US"}}));
assert.commandWorked(sh.shardCollection(kDbName + ".foo", {a: 1}, false, {collation: {locale: "simple"}}));
indexSpec = getIndexSpecByName(db.foo, "a_1");
assert(!indexSpec.hasOwnProperty("collation"), tojson(indexSpec));
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
indexSpec = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, indexSpec);
assert.eq(indexSpec.collation.locale, "simple", tojson(indexSpec));

View File

@ -14,6 +14,7 @@
* ]
*/
import {getRandomShardName} from "jstests/libs/cluster_helpers/sharded_cluster_fixture_helpers.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
const coll = db[jsTestName()];
@ -22,8 +23,9 @@ coll.drop();
// Create a timeseries collection and validate we have the default {m: 1, t: 1} index
assert.commandWorked(db.createCollection(coll.getName(), {timeseries: {timeField: "t", metaField: "m"}}));
{
const index = coll.getIndexByKey({m: 1, t: 1});
assert(index && !index.collation, tojson(index));
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
const index = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, coll.getIndexByKey({m: 1, t: 1}));
assert(index && index.collation && index.collation.locale === "simple", tojson(index));
}
// Drop the default index and re-create it using a non-default collation

View File

@ -111,3 +111,6 @@ filters:
- "release_memory_util.js":
approvers:
- 10gen/query-execution
- "index_catalog_helpers.js":
approvers:
- 10gen/server-catalog-and-routing-shard-catalog

View File

@ -25,6 +25,7 @@ import {
getTimeseriesBucketsColl,
} from "jstests/core/timeseries/libs/viewless_timeseries_util.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
import {getRawOperationSpec} from "jstests/libs/raw_operation_utils.js";
import {PersistenceProviderUtil} from "jstests/libs/server-rss/persistence_provider_util.js";
@ -303,6 +304,15 @@ function mapListCatalogToListIndexesEntry(listCatalogEntry) {
? Math.floor(mdIndexSpec.expireAfterSeconds)
: 2147483647,
}),
// Adding the simple collation to the $listCatalog entry to be able to compare the index specs.
// TODO (SERVER-119573) Remove this once listCatalog returns the simple collation explicitly.
...(mdIndexSpec.collation === undefined && {collation: {locale: "simple"}}),
...(mdIndexSpec.originalSpec !== undefined && {
originalSpec: {
...mdIndexSpec.originalSpec,
...(mdIndexSpec.originalSpec.collation === undefined && {collation: {locale: "simple"}}),
},
}),
};
});
@ -324,6 +334,9 @@ function mapListCatalogToListIndexesEntry(listCatalogEntry) {
unique: true,
}),
...(mdOptions.collation !== undefined && {collation: mdOptions.collation}),
// Adding the simple collation to the $listCatalog entry to be able to compare the index specs.
// TODO (SERVER-119573) Remove this once listCatalog returns the simple collation explicitly.
...(mdOptions.collation === undefined && {collation: {locale: "simple"}}),
...(mdOptions.expireAfterSeconds !== undefined && {expireAfterSeconds: mdOptions.expireAfterSeconds}),
clustered: true,
});
@ -432,7 +445,7 @@ function isViewListCatalogEntry(listCatalogEntry) {
return !listCatalogEntry.md?.options.uuid;
}
function validateListCatalogToListIndexesConsistency(listCatalog, listIndexes, shouldAssert) {
function validateListCatalogToListIndexesConsistency(db, listCatalog, listIndexes, shouldAssert) {
// Sorting function to ignore irrelevant ordering differences while comparing.
function sortCollectionIndexesInPlace(indexList) {
indexList.sort((a, b) => a.name.localeCompare(b.name)); // Sort by collection.
@ -457,7 +470,8 @@ function validateListCatalogToListIndexesConsistency(listCatalog, listIndexes, s
// TODO (SERVER-97749): Don't delete 'background' field once we
// handle it properly
delete index.background;
return index;
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
return IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, index);
}),
],
})),
@ -484,6 +498,7 @@ function validateListCatalogToListIndexesConsistency(listCatalog, listIndexes, s
}
function validateCatalogListOperationsConsistency(
db,
listCatalog,
listCollections,
listIndexes,
@ -498,7 +513,7 @@ function validateCatalogListOperationsConsistency(
isDbReadOnly,
ignoreRecordIdsReplicatedOption,
shouldAssert,
) && validateListCatalogToListIndexesConsistency(listCatalog, listIndexes, shouldAssert)
) && validateListCatalogToListIndexesConsistency(db, listCatalog, listIndexes, shouldAssert)
);
}
@ -567,6 +582,7 @@ export function assertCatalogListOperationsConsistencyForCollection(collection)
listCatalog = filterListCatalogEntriesFromShardsWithoutChunks(db, listCatalog);
validateCatalogListOperationsConsistency(
db,
listCatalog,
listCollections,
listIndexes,
@ -799,7 +815,7 @@ export function assertCatalogListOperationsConsistencyForDb(db, tenantId) {
}
if (
collIndexes !== null &&
!validateListCatalogToListIndexesConsistency(catalogInfoForIndexes, collIndexes, shouldAssert)
!validateListCatalogToListIndexesConsistency(db, catalogInfoForIndexes, collIndexes, shouldAssert)
) {
jsTest.log.info("$listCatalog/listIndexes consistency check failed, retrying...");
return false;

View File

@ -1,3 +1,5 @@
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
/**
* Utilities for testing clustered collections.
*/
@ -67,7 +69,17 @@ export var ClusteredCollectionUtil = class {
// The clusteredIndex should appear in listIndexes with additional "clustered" field.
static validateListIndexes(db, collName, createOptions) {
const fullCreateOptions = ClusteredCollectionUtil.constructFullCreateOptions(createOptions);
const listIndexes = assert.commandWorked(db[collName].runCommand("listIndexes"));
// TODO (SERVER-119573) Remove this workaround once listIndexes output matches storage format.
if (!fullCreateOptions.clusteredIndex.collation) {
fullCreateOptions.clusteredIndex.collation = {locale: "simple"};
}
let index = db[collName].getIndexes()[0];
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
index = IndexCatalogHelpers.addSimpleCollationToIndexIfMissing(db, index);
let extraData = {clustered: true};
// ttl is not stored on the clusteredIndex but on the collection info. Therefore, we have to
// add it back in this check to match the getIndexes output.
@ -75,7 +87,7 @@ export var ClusteredCollectionUtil = class {
extraData.expireAfterSeconds = createOptions.expireAfterSeconds;
}
const expectedListIndexesOutput = Object.extend(extraData, fullCreateOptions.clusteredIndex);
assert.docEq(expectedListIndexesOutput, listIndexes.cursor.firstBatch[0]);
assert.docEq(expectedListIndexesOutput, index);
}
static testBasicClusteredCollection(db, collName, clusterKey) {

View File

@ -1,3 +1,6 @@
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
/**
* Helper functions that help test things to do with the index catalog.
*/
@ -46,9 +49,10 @@ export var IndexCatalogHelpers = (function () {
const foundByKeyPatternAndCollation = foundByKeyPattern.filter((spec) => {
if (collation.locale === "simple") {
// The simple collation is not explicitly stored in the index catalog, so we expect
// the "collation" field to be absent.
return !spec.hasOwnProperty("collation");
// The simple collation is not returned by listIndexes on older versions.
// On newer versions, the simple collation is always returned by listIndexes.
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
return !spec.hasOwnProperty("collation") || spec.collation.locale === "simple";
}
return bsonWoCompare(spec.collation, collation) === 0;
});
@ -82,10 +86,97 @@ export var IndexCatalogHelpers = (function () {
);
}
/**
* Converts the index specs returned by listIndexes to the storage index format by stripping out
* the 'collation' field if it is a simple collation.
*
* As of SERVER-89953, every index spec returned by listIndexes includes a 'collation' field,
* even when it is just the default "simple" collation. However, the format stored in the catalog
* omits the 'collation' field for simple collations.
*
* TODO (SERVER-119573): Remove this utility once listIndexes output is consistent with storage.
*/
function convertListIndexesResponseToStorageIndexFormat(indexes) {
return indexes.map((index) => {
if (index.collation && index.collation.locale === "simple") {
const {collation, ...rest} = index;
return rest;
}
return index;
});
}
/**
* Returns true if listIndexes always includes the simple collation in index specs.
*
* In FCV upgrade/downgrade suites, listIndexes may not include the simple collation if the feature
* flag was not enabled in the lastLTS FCV. This function handles both stable and upgrade/downgrade
* suite contexts.
*
* TODO (SERVER-122417): Remove this function once v9.0 branches out.
*/
function listIndexesIncludesSimpleCollation(db) {
if (isStableFCVSuite()) {
return FeatureFlagUtil.isPresentAndEnabled(db, "ListIndexesAlwaysIncludesSimpleCollation");
}
// In FCV upgrade/downgrade suite, the flag is reliably enabled only if it has been enabled since
// lastLTS (e.g., flag released in FCV 9.0, running binary 9.1 with FCV 9.0 - 9.1
// upgrade/downgrade suite --> always includes simple collation).
const flagDoc = FeatureFlagUtil.getFeatureFlagDoc(db.getMongo(), "ListIndexesAlwaysIncludesSimpleCollation");
return flagDoc && flagDoc.value && MongoRunner.compareBinVersions(lastLTSFCV, flagDoc.version) >= 0;
}
/**
* Ensures an index spec includes {locale: "simple"} collation if missing from listIndexes output.
*
* listIndexes may omit the explicit simple collation in downgrade/upgrade FCV suites where the
* relevant feature flag was not enabled in the lastLTS FCV. This helper adds it if needed to
* standardize index specs for comparison and correctness across test environments.
*
* TODO (SERVER-122417): Remove this function once v9.0 branches out.
*/
function addSimpleCollationToIndexIfMissing(db, index) {
if (listIndexesIncludesSimpleCollation(db)) {
return index;
}
let indexWithSimpleCollation = {...index};
if (!index.collation) {
indexWithSimpleCollation.collation = {locale: "simple"};
}
if (index.originalSpec && !index.originalSpec.collation) {
indexWithSimpleCollation.originalSpec.collation = {locale: "simple"};
}
return indexWithSimpleCollation;
}
/**
* Ensures the given list of index specs includes {locale: "simple"} collation if missing from
* listIndexes output.
*
* listIndexes may omit the explicit simple collation in downgrade/upgrade FCV suites where the
* relevant feature flag was not enabled in the lastLTS FCV. This helper adds it if needed to
* standardize index specs for comparison and correctness across test environments.
*
* TODO (SERVER-122417): Remove this function once v9.0 branches out.
*/
function addSimpleCollationToIndexesIfMissing(db, indexes) {
if (listIndexesIncludesSimpleCollation(db)) {
return indexes;
}
return indexes.map((index) => addSimpleCollationToIndexIfMissing(db, index));
}
return {
findByName: getIndexSpecByName,
findByKeyPattern: getIndexSpecByKeyPattern,
createSingleIndex: createSingleIndex,
createIndexAndVerifyWithDrop: createIndexAndVerifyWithDrop,
// TODO (SERVER-119573): Remove this utility once listIndexes output is consistent with storage.
convertListIndexesResponseToStorageIndexFormat: convertListIndexesResponseToStorageIndexFormat,
// TODO (SERVER-122417): Remove these utilities once v9.0 branches out.
addSimpleCollationToIndexIfMissing: addSimpleCollationToIndexIfMissing,
addSimpleCollationToIndexesIfMissing: addSimpleCollationToIndexesIfMissing,
};
})();

View File

@ -10,6 +10,7 @@
*/
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {getTimeseriesBucketsColl} from "jstests/core/timeseries/libs/viewless_timeseries_util.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
if (lastLTSFCV != "8.0") {
print("Skipping test because last LTS FCV is no longer 8.0");
@ -31,6 +32,13 @@ assert.commandWorked(coll.insertOne({t: ISODate(), m: 123}));
const expectedViewlessMetadata = coll.getMetadata();
const expectedIndexes = coll.getIndexes();
if (!FeatureFlagUtil.isEnabled(db, "ListIndexesAlwaysIncludesSimpleCollation")) {
expectedIndexes.map((index) => {
if (!index.collation) {
index.collation = {locale: "simple"};
}
});
}
// Check that the collection can be converted to viewful timeseries and back to viewless.
function assertValidTimeseriesCollection(nodeColl, {expectViewlessFormat}) {
@ -55,7 +63,16 @@ function assertValidTimeseriesCollection(nodeColl, {expectViewlessFormat}) {
assert.docEq(expectedViewfulMetadata, nodeColl.getMetadata());
assert.eq(expectedViewlessMetadata.info.uuid, getTimeseriesBucketsColl(nodeColl).getUUID());
}
assert.docEq(expectedIndexes, nodeColl.getIndexes());
let actualIndexes = nodeColl.getIndexes();
if (!FeatureFlagUtil.isEnabled(db, "ListIndexesAlwaysIncludesSimpleCollation")) {
actualIndexes.map((index) => {
if (!index.collation) {
index.collation = {locale: "simple"};
}
});
}
assert.docEq(expectedIndexes, actualIndexes);
// The data is still accessible
assert.eq(2, nodeColl.countDocuments({}));

View File

@ -37,15 +37,23 @@ const validateCompoundSecondaryIndexes = function (db, coll, clusterKey) {
const validateCreateIndexOnClusterKey = function (db, collName, fullCreateOptions) {
const clusterKey = fullCreateOptions.clusteredIndex.key;
const listIndexes0 = assert.commandWorked(db[collName].runCommand("listIndexes"));
const listIndexesClusteredIndex = listIndexes0.cursor.firstBatch[0];
const listIndexes0 = db[collName].getIndexes();
const listIndexesClusteredIndex = listIndexes0[0];
// Expect listIndexes to append the 'clustered' field to it's clusteredIndex output.
assert.docEq(listIndexesClusteredIndex.key, clusterKey);
assert.eq(listIndexesClusteredIndex.clustered, true);
// no-op with the 'clustered' option.
assert.commandWorked(db[collName].runCommand({createIndexes: collName, indexes: [listIndexesClusteredIndex]}));
// Remove the 'collation' field from the clustered index spec, since specifying a collation is
// not allowed for clustered indexes. This is because clustered indexes are always getting the
// collection's default collation.
let clusteredIndexSpec = {...listIndexesClusteredIndex};
if (clusteredIndexSpec.collation) {
delete clusteredIndexSpec.collation;
}
assert.commandWorked(db[collName].runCommand({createIndexes: collName, indexes: [clusteredIndexSpec]}));
// no-op without the 'clustered' option.
assert.commandWorked(db[collName].createIndex(clusterKey));
@ -65,9 +73,9 @@ const validateCreateIndexOnClusterKey = function (db, collName, fullCreateOption
);
// The listIndexes output should be unchanged.
const listIndexes1 = assert.commandWorked(db[collName].runCommand("listIndexes"));
assert.eq(listIndexes1.cursor.firstBatch.length, 1);
assert.docEq(listIndexes1.cursor.firstBatch[0], listIndexes0.cursor.firstBatch[0]);
const listIndexes1 = db[collName].getIndexes();
assert.eq(listIndexes1.length, 1);
assert.docEq(listIndexes1[0], listIndexes0[0]);
};
// It is illegal to drop the clusteredIndex. Verify that the various ways of dropping the

View File

@ -5,6 +5,7 @@
*/
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
const st = new ShardingTest({shards: 2});
@ -37,7 +38,7 @@ function makeCorrectCommand(sourceColl, destColl) {
internalRenameIfOptionsAndIndexesMatch: 1,
from: sourceColl.getFullName(),
to: destColl.getFullName(),
indexes: destColl.getIndexes(),
indexes: IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(destColl.getIndexes()),
collectionOptions: collectionOptions,
databaseVersion: dbVersion,
};

View File

@ -27,6 +27,7 @@ function testOplogEntryIdIndexSpec(collectionName, idIndexSpec) {
assert.commandWorked(primaryDB.createCollection("without_version"));
let allIndexes = primaryDB.without_version.getIndexes();
allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(allIndexes);
let spec = IndexCatalogHelpers.findByKeyPattern(allIndexes, {_id: 1});
assert.neq(null, spec, "_id index not found: " + tojson(allIndexes));
assert.eq(2, spec.v, "Expected primary to build a v=2 _id index: " + tojson(spec));
@ -34,6 +35,7 @@ testOplogEntryIdIndexSpec("without_version", spec);
assert.commandWorked(primaryDB.createCollection("version_v2", {idIndex: {key: {_id: 1}, name: "_id_", v: 2}}));
allIndexes = primaryDB.version_v2.getIndexes();
allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(allIndexes);
spec = IndexCatalogHelpers.findByKeyPattern(allIndexes, {_id: 1});
assert.neq(null, spec, "_id index not found: " + tojson(allIndexes));
assert.eq(2, spec.v, "Expected primary to build a v=2 _id index: " + tojson(spec));
@ -41,6 +43,7 @@ testOplogEntryIdIndexSpec("version_v2", spec);
assert.commandWorked(primaryDB.createCollection("version_v1", {idIndex: {key: {_id: 1}, name: "_id_", v: 1}}));
allIndexes = primaryDB.version_v1.getIndexes();
allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(allIndexes);
spec = IndexCatalogHelpers.findByKeyPattern(allIndexes, {_id: 1});
assert.neq(null, spec, "_id index not found: " + tojson(allIndexes));
assert.eq(1, spec.v, "Expected primary to build a v=1 _id index: " + tojson(spec));
@ -51,16 +54,19 @@ rst.awaitReplication();
// Verify that the secondary built _id indexes with the same version as on the primary.
allIndexes = secondaryDB.without_version.getIndexes();
allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(allIndexes);
spec = IndexCatalogHelpers.findByKeyPattern(allIndexes, {_id: 1});
assert.neq(null, spec, "_id index not found: " + tojson(allIndexes));
assert.eq(2, spec.v, "Expected secondary to build a v=2 _id index when explicitly requested: " + tojson(spec));
allIndexes = secondaryDB.version_v2.getIndexes();
allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(allIndexes);
spec = IndexCatalogHelpers.findByKeyPattern(allIndexes, {_id: 1});
assert.neq(null, spec, "_id index not found: " + tojson(allIndexes));
assert.eq(2, spec.v, "Expected secondary to build a v=2 _id index when explicitly requested: " + tojson(spec));
allIndexes = secondaryDB.version_v1.getIndexes();
allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(allIndexes);
spec = IndexCatalogHelpers.findByKeyPattern(allIndexes, {_id: 1});
assert.neq(null, spec, "_id index not found: " + tojson(allIndexes));
assert.eq(1, spec.v, "Expected secondary to implicitly build a v=1 _id index: " + tojson(spec));

View File

@ -16,7 +16,7 @@ const oplogColl = primary.getDB("local").oplog.rs;
function testOplogEntryContainsIndexInfoObj(coll, keyPattern, indexOptions) {
assert.commandWorked(coll.createIndex(keyPattern, indexOptions));
const allIndexes = coll.getIndexes();
const allIndexes = IndexCatalogHelpers.convertListIndexesResponseToStorageIndexFormat(coll.getIndexes());
const indexSpec = IndexCatalogHelpers.findByKeyPattern(allIndexes, keyPattern);
assert.neq(null, indexSpec, "Index with key pattern " + tojson(keyPattern) + " not found: " + tojson(allIndexes));

View File

@ -132,6 +132,10 @@ config.getCollectionInfos().forEach(function (c) {
delete i.key;
delete i.ns;
delete i.v;
if (i.clustered) {
// Can't specify the collation for a clustered index.
delete i.collation;
}
assert.commandWorked(configCopy.getCollection(c.name).createIndex(key, i));
});
});

View File

@ -6,6 +6,7 @@
* (SERVER-89744 for more info).
*
*/
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {ReshardingTest} from "jstests/sharding/libs/resharding_test_fixture.js";
const reshardingTest = new ReshardingTest();
@ -31,13 +32,16 @@ const collection = reshardingTest.createShardedCollection({
chunks: [{min: {oldKey: MinKey}, max: {oldKey: MaxKey}, shard: donorShardNames[0]}],
shardCollOptions: {collation: {locale: "simple"}},
});
const db = collection.getDB();
const idxSimpleCollationName = "idxSimpleCollation";
assert.commandWorked(collection.createIndex({x: 1}, {name: idxSimpleCollationName, collation: {locale: "simple"}}));
const idx2Name = "idx2";
assert.commandWorked(collection.createIndex({x: 1}, {name: idx2Name}));
const preReshardingIndexes = collection.getIndexes();
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
const preReshardingIndexes = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, collection.getIndexes());
const preIdxDict = {};
preReshardingIndexes.forEach(function (idx) {
preIdxDict[idx.name] = idx;
@ -52,11 +56,12 @@ reshardingTest.withReshardingInBackground({
const postReshardingIndexes = collection.getIndexes();
assert.eq(postReshardingIndexes.length, 5);
for (const postIdxSpec of postReshardingIndexes) {
// TODO (SERVER-122417) Remove this workaround once v9.0 branches out.
const normalizedPostIndexes = IndexCatalogHelpers.addSimpleCollationToIndexesIfMissing(db, postReshardingIndexes);
for (const postIdxSpec of normalizedPostIndexes) {
if ("newKey" in postIdxSpec.key) {
// the index collation for post resharding key should be {locale: "simple"}. listIndexes
// omits this collation.
assert.eq(postIdxSpec.collation, undefined);
// the index collation for post resharding key should be {locale: "simple"}.
assert.eq(postIdxSpec.collation.locale, "simple");
} else {
assert.eq(postIdxSpec, preIdxDict[postIdxSpec.name]);
}

View File

@ -204,6 +204,7 @@ function timeseriesDefaultIndex() {
[timeField]: 1,
},
"name": metaField + "_1_" + timeField + "_1",
"collation": {"locale": "simple"},
};
}

View File

@ -185,8 +185,6 @@ public:
/**
* Sets a RequestMetadataWriter on this connection.
*
* TODO: support multiple metadata writers.
*/
virtual void setRequestMetadataWriter(rpc::RequestMetadataWriter writer);
@ -198,8 +196,6 @@ public:
/**
* Sets a ReplyMetadataReader on this connection.
*
* TODO: support multiple metadata readers.
*/
virtual void setReplyMetadataReader(rpc::ReplyMetadataReader reader);
@ -465,8 +461,17 @@ public:
/**
* Lists indexes on the collection 'nsOrUuid'.
*
* Includes in-progress indexes.
*
* Returned index specifications are not normalized: the 'collation' field is always included,
* even when its value is simple. In contrast, in normalized index specifications, the simple
* collation is typically omitted.
* For cloning purposes, these index specs can be used directly
* with a createIndexes-style command, since normalization will occur automatically on the
* server. However, they should not be used to create a new index if the normalization step is
* bypassed.
*
* If 'includeBuildUUIDs' is true, in-progress index specs will have the following format:
* {
* spec: <BSONObj>

View File

@ -631,7 +631,20 @@ Status DefaultClonerImpl::copyDb(OperationContext* opCtx,
auto indexSpecs = uassertStatusOK(
getConn()->runExhaustiveCursorCommand(nss.dbName(), listIndexesCmd.toBSON()));
collectionIndexSpecs[params.collectionName] = indexSpecs;
// The listIndexes command always includes a 'collation' field in its output as of
// SERVER-89953.
// However, here we are expecting a normalized index specification, in which case the
// 'collation' field should be omitted when it represents the simple collation. Apply
// normalization here to be able to clone the indexes using the expected format.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
std::list<BSONObj> normalizedIndexSpecs;
for (const auto& indexSpec : indexSpecs) {
normalizedIndexSpecs.emplace_back(
IndexCatalog::normalizeIndexSpecFromListIndexes(indexSpec));
}
collectionIndexSpecs[params.collectionName] = normalizedIndexSpecs;
if (params.idIndexSpec.isEmpty()) {
params.idIndexSpec = _getIdIndexSpec(indexSpecs);

View File

@ -2112,9 +2112,16 @@ OptionsAndIndexes CreateCollectionCoordinator::_getCollectionOptionsAndIndexes(
listIndexes.toBSON(),
Milliseconds(-1));
indexes = std::move(uassertStatusOK(swIndexResponse).docs);
// The listIndexes command always includes a 'collation' field in its output as of SERVER-89953.
// However, the participant shards are expecting a normalized index specification, in which case
// the 'collation' field should be omitted when it represents the simple collation. Apply
// normalization here to be able to recreate the indexes on the participant shards.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
const auto normalizedIndexes = IndexCatalog::normalizeIndexSpecsFromListIndexes(
std::move(uassertStatusOK(swIndexResponse).docs));
return {optionsBob.obj(), std::move(indexes), idIndex};
return {optionsBob.obj(), std::move(normalizedIndexes), idIndex};
}
void CreateCollectionCoordinator::_createCollectionOnParticipants(

View File

@ -504,7 +504,7 @@ void checkExpectedTargetIndexesMatch(OperationContext* opCtx,
const NamespaceString targetNss,
const std::vector<BSONObj>& expectedIndexes) {
sharding::router::CollectionRouter router(opCtx, targetNss);
const auto currentIndexes = router.routeWithRoutingContext(
auto currentIndexes = router.routeWithRoutingContext(
"checking indexes prerequisites within rename collection coordinator",
[&](OperationContext* opCtx, RoutingContext& routingCtx) {
const auto response = loadIndexesFromAuthoritativeShard(opCtx, routingCtx, targetNss);
@ -514,6 +514,15 @@ void checkExpectedTargetIndexesMatch(OperationContext* opCtx,
}
return uassertStatusOK(response).docs;
});
// The listIndexes command always includes a 'collation' field in its output as of SERVER-89953.
// However, we are expecting a normalized index specification, in which case the 'collation'
// field should be omitted when it represents the simple collation. Apply normalization here to
// be able to compare the index specs apropiately.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
currentIndexes = IndexCatalog::normalizeIndexSpecsFromListIndexes(currentIndexes);
uassertStatusOK(checkTargetCollectionIndexesMatch(targetNss, expectedIndexes, currentIndexes));
}
} // namespace

View File

@ -441,8 +441,16 @@ void validateShardKeyIsNotEncrypted(OperationContext* opCtx,
std::vector<BSONObj> ValidationBehaviorsShardCollection::loadIndexes(
const NamespaceString& nss) const {
return listIndexesEmptyListIfMissing(
const auto indexes = listIndexesEmptyListIfMissing(
_opCtx, nss, ListIndexesInclude::kNothing, /*isRawDataRequest=*/true);
// The listIndexes command always includes a 'collation' field in its output as of SERVER-89953.
// However, the caller expects a normalized index specification, in which case the 'collation'
// field should be omitted when it represents the simple collation. Apply normalization here to
// ensure compatibility with storage index specs.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
return IndexCatalog::normalizeIndexSpecsFromListIndexes(indexes);
}
void ValidationBehaviorsShardCollection::verifyUsefulNonMultiKeyIndex(

View File

@ -62,6 +62,13 @@ class MONGO_MOD_NEEDS_REPLACEMENT ShardKeyValidationBehaviors {
public:
virtual ~ShardKeyValidationBehaviors() {}
/*
* Returns index specifications for the collection 'nss'.
* Returned indexes are in normalized form: if the index uses the simple collation, the
* 'collation' field is omitted. Note that because of this normalization, these index specs
* cannot be passed directly to a createIndexes user-like command because attempting to
* normalize an already-normalized spec is not supported.
*/
virtual std::vector<BSONObj> loadIndexes(const NamespaceString& nss) const = 0;
virtual void verifyUsefulNonMultiKeyIndex(const NamespaceString& nss,

View File

@ -325,30 +325,34 @@ TEST_F(ConfigInitializationTest, BuildsNecessaryIndexes) {
->initializeConfigDatabaseIfNeeded(operationContext()));
std::vector<BSONObj> expectedChunksIndexes = std::vector<BSONObj>{
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key" << BSON("uuid" << 1 << "min" << 1) << "name"
<< "uuid_1_min_1"
<< "unique" << true),
<< "unique" << true << "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key" << BSON("uuid" << 1 << "shard" << 1 << "min" << 1) << "name"
<< "uuid_1_shard_1_min_1"
<< "unique" << true),
<< "unique" << true << "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key" << BSON("uuid" << 1 << "lastmod" << 1) << "name"
<< "uuid_1_lastmod_1"
<< "unique" << true),
<< "unique" << true << "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key" << BSON("uuid" << 1 << "shard" << 1 << "onCurrentShardSince" << 1)
<< "name"
<< "uuid_1_shard_1_onCurrentShardSince_1")};
<< "uuid_1_shard_1_onCurrentShardSince_1" << "collation"
<< BSON("locale" << "simple"))};
auto expectedShardsIndexes = std::vector<BSONObj>{
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "unique" << true << "key" << BSON("host" << 1) << "name"
<< "host_1")};
<< "host_1" << "collation" << BSON("locale" << "simple"))};
auto expectedTagsIndexes = std::vector<BSONObj>{
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "unique" << true << "key" << BSON("ns" << 1 << "min" << 1) << "name"
<< "ns_1_min_1"),
<< "ns_1_min_1" << "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key" << BSON("ns" << 1 << "tag" << 1) << "name"
<< "ns_1_tag_1")};
<< "ns_1_tag_1" << "collation" << BSON("locale" << "simple"))};
auto foundChunksIndexes =
assertGet(getIndexes(operationContext(), NamespaceString::kConfigsvrChunksNamespace));
@ -362,12 +366,13 @@ TEST_F(ConfigInitializationTest, BuildsNecessaryIndexes) {
assertBSONObjsSame(expectedTagsIndexes, foundTagsIndexes);
auto expectedPlacementHistoryIndexes = std::vector<BSONObj>{
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "unique" << true << "key" << BSON("nss" << 1 << "timestamp" << -1)
<< "name"
<< "nss_1_timestamp_-1"),
<< "nss_1_timestamp_-1" << "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key" << BSON("timestamp" << -1 << "nss" << 1) << "name"
<< "timestamp_-1_nss_1")};
<< "timestamp_-1_nss_1" << "collation" << BSON("locale" << "simple"))};
auto foundPlacementHistoryIndexes = assertGet(
getIndexes(operationContext(), NamespaceString::kConfigsvrPlacementHistoryNamespace));
assertBSONObjsSame(expectedPlacementHistoryIndexes, foundPlacementHistoryIndexes);

View File

@ -63,9 +63,10 @@ TEST_F(ConfigIndexTest, CompatibleIndexAlreadyExists) {
->initializeConfigDatabaseIfNeeded(operationContext()));
auto expectedShardsIndexes = std::vector<BSONObj>{
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "unique" << true << "key" << BSON("host" << 1) << "name"
<< "host_1")};
<< "host_1" << "collation" << BSON("locale" << "simple"))};
auto foundShardsIndexes =

View File

@ -288,7 +288,13 @@ public:
/**
* Returns the complete set of index specifications for the given namespace.
*
* For timeseries collections, returns index specs in their raw format.
*
* Returned indexes are in normalized form: if the index uses the simple collation, the
* 'collation' field is omitted. Note that because of this normalization, these index specs
* cannot be passed directly to a createIndexes user-like command because attempting to
* normalize an already-normalized spec is not supported.
*/
virtual std::vector<BSONObj> getIndexSpecs(OperationContext* opCtx,
const NamespaceString& ns,

View File

@ -112,11 +112,20 @@ std::unique_ptr<Pipeline> NonShardServerProcessInterface::preparePipelineForExec
std::vector<BSONObj> NonShardServerProcessInterface::getIndexSpecs(OperationContext* opCtx,
const NamespaceString& ns,
bool includeBuildUUIDs) {
return listIndexesEmptyListIfMissing(opCtx,
ns,
includeBuildUUIDs ? ListIndexesInclude::kBuildUUID
: ListIndexesInclude::kNothing,
/*isRawDataRequest=*/true);
const auto indexes = listIndexesEmptyListIfMissing(
opCtx,
ns,
includeBuildUUIDs ? ListIndexesInclude::kBuildUUID : ListIndexesInclude::kNothing,
/*isRawDataRequest=*/true);
// The listIndexes command always includes a 'collation' field in its output as of SERVER-89953.
// However, the caller expects a normalized index specification, in which case the 'collation'
// field should be omitted when it represents the simple collation. Apply normalization here to
// ensure compatibility with storage index specs.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
return IndexCatalog::normalizeIndexSpecsFromListIndexes(indexes);
}
std::vector<FieldPath> NonShardServerProcessInterface::collectDocumentKeyFieldsActingAsRouter(

View File

@ -433,7 +433,7 @@ std::vector<BSONObj> ShardServerProcessInterface::getIndexSpecs(OperationContext
const NamespaceString& ns,
bool includeBuildUUIDs) {
sharding::router::CollectionRouter router(opCtx, ns);
return router.routeWithRoutingContext(
const auto indexes = router.routeWithRoutingContext(
"ShardServerProcessInterface::getIndexSpecs",
[&](OperationContext* opCtx, RoutingContext& routingCtx) -> std::vector<BSONObj> {
StatusWith<Shard::QueryResponse> response =
@ -444,6 +444,14 @@ std::vector<BSONObj> ShardServerProcessInterface::getIndexSpecs(OperationContext
uassertStatusOK(response);
return response.getValue().docs;
});
// The listIndexes command always includes a 'collation' field in its output as of SERVER-89953.
// However, the caller expects a normalized index specification, in which case the 'collation'
// field should be omitted when it represents the simple collation. Apply normalization here to
// ensure compatibility with storage index specs.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
return IndexCatalog::normalizeIndexSpecsFromListIndexes(indexes);
}
std::vector<DatabaseName> ShardServerProcessInterface::getAllDatabases(

View File

@ -294,8 +294,16 @@ BaseCloner::AfterStageBehavior CollectionCloner::listIndexesStage() {
const auto storageEngine = getGlobalServiceContext()->getStorageEngine();
// Parse the index specs into their respective state, ready or unfinished.
for (auto&& spec : indexSpecs) {
// Sanitize storage engine options to remove options which might not apply to this node. See
// SERVER-68122.
// The listIndexes command always includes a 'collation' field in its output as of
// SERVER-89953. However, here we are expecting a normalized index specification, in which
// case the 'collation' field should be omitted when it represents the simple collation.
// Apply normalization here to be able to clone the indexes using the expected format.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs compatible
// with the storage format.
spec = IndexCatalog::normalizeIndexSpecFromListIndexes(spec);
// Sanitize storage engine options to remove options which might not apply to this node.
// See SERVER-68122.
if (auto storageEngineElem = spec.getField(IndexDescriptor::kStorageEngineFieldName)) {
auto sanitizedStorageEngineOpts =
storageEngine->getSanitizedStorageOptionsForSecondaryReplication(

View File

@ -446,8 +446,8 @@ public:
return _coll->getCompletedIndexCount();
}
BSONObj getIndexSpec(StringData indexName) const override {
return _coll->getIndexSpec(indexName);
BSONObj getIndexSpec(StringData indexName, bool expandSimpleCollation) const override {
return _coll->getIndexSpec(indexName, expandSimpleCollation);
}
void getAllIndexes(std::vector<std::string>* names) const override {

View File

@ -926,6 +926,15 @@ MigrationDestinationManager::IndexesAndIdIndex MigrationDestinationManager::getC
continue;
}
// The listIndexes command always includes a 'collation' field in its output. However,
// unless 'expandSimpleCollation' is true, the caller expects a normalized index
// specification, in which case the 'collation' field should be omitted when it
// represents the simple collation. Apply normalization here to ensure compatibility
// with storage index specs.
// TODO (SERVER-119573): Remove this normalization once listIndexes returns specs
// compatible with the storage format.
spec = IndexCatalog::normalizeIndexSpecFromListIndexes(spec);
if (auto indexNameElem = spec[IndexDescriptor::kIndexNameFieldName];
indexNameElem.type() == BSONType::string &&
indexNameElem.valueStringData() == IndexConstants::kIdIndexName) {
@ -1044,15 +1053,25 @@ namespace {
*/
void _dropLocalIndexes(OperationContext* opCtx,
const NamespaceString& nss,
const std::vector<BSONObj>& indexSpecs) {
const std::vector<BSONObj>& donorIndexSpecs) {
// Determine which indexes exist on the local collection that don't exist on the donor's
// collection.
DBDirectClient client(opCtx);
auto indexes = listIndexesEmptyListIfMissing(
opCtx, nss, ListIndexesInclude::kNothing, /*isRawDataRequest=*/true);
for (auto&& recipientIndex : indexes) {
// The listIndexes command always includes a 'collation' field in each index spec as of
// SERVER-89953, even if it has a simple collation. In contrast, `donorIndexSpecs` are
// normalized, which means that the 'collation' field is omitted when it is a simple collation.
// Therefore, we normalize the listIndexes output here to enable direct comparison between both
// formats.
// TODO (SERVER-119573): Remove this normalization once listIndexes output matches storage
// format.
const auto& normalizedSpecs = IndexCatalog::normalizeIndexSpecsFromListIndexes(indexes);
for (auto&& recipientIndex : normalizedSpecs) {
bool dropIndex = true;
for (auto&& donorIndex : indexSpecs) {
for (auto&& donorIndex : donorIndexSpecs) {
if (recipientIndex.woCompare(donorIndex) == 0) {
dropIndex = false;
break;

View File

@ -1342,7 +1342,7 @@ ReshardingRecipientService::RecipientStateMachine::_buildIndexThenTransitionToAp
.then([this, factory](const ReplIndexBuildState::IndexCatalogStats& stats) {
if (auto opCtx = _makeOperationContext(factory);
resharding::gReshardingIndexVerification.load()) {
auto [sourceIdxSpecs, _] = _externalState->getCollectionIndexes(
auto [normalizedSourceIdxSpecs, _] = _externalState->getCollectionIndexes(
opCtx.get(),
_metadata.getSourceNss(),
_metadata.getSourceUUID(),
@ -1355,10 +1355,22 @@ ReshardingRecipientService::RecipientStateMachine::_buildIndexThenTransitionToAp
_metadata.getTempReshardingNss(),
ListIndexesInclude::kNothing,
/*isRawDataRequest=*/true);
resharding::verifyIndexSpecsMatch(sourceIdxSpecs.cbegin(),
sourceIdxSpecs.cend(),
tempCollIdxSpecs.cbegin(),
tempCollIdxSpecs.cend());
// The listIndexes command always includes a 'collation' field in each index
// spec as of SERVER-89953, even if it represents the simple collation. In
// contrast, getCollectionIndexes() returns normalized specs where the
// 'collation' field is omitted when it is a simple collation. Therefore, we
// normalize the listIndexes output here to enable direct comparison between
// both formats.
// TODO (SERVER-119573): Remove this normalization once listIndexes output
// matches storage format.
const auto& normalizedTempCollIdxSpecs =
IndexCatalog::normalizeIndexSpecsFromListIndexes(tempCollIdxSpecs);
resharding::verifyIndexSpecsMatch(normalizedSourceIdxSpecs.cbegin(),
normalizedSourceIdxSpecs.cend(),
normalizedTempCollIdxSpecs.cbegin(),
normalizedTempCollIdxSpecs.cend());
}
_transitionToApplying(factory);
});

View File

@ -156,9 +156,18 @@ protected:
BSON("level" << "local"
<< "afterClusterTime" << kDefaultFetchTimestamp));
return BSON(
"ok" << 1 << "cursor"
<< BSON("id" << 0LL << "ns" << nss.ns_forTest() << "firstBatch" << indexDocs));
// Ensure listIndexes always returns a 'collation' field.
std::vector<BSONObj> indexDocsWithCollation;
for (auto&& indexDoc : indexDocs) {
if (!indexDoc.hasField("collation")) {
indexDocsWithCollation.emplace_back(
indexDoc.addFields(BSON("collation" << BSON("locale" << "simple"))));
}
}
return BSON("ok" << 1 << "cursor"
<< BSON("id" << 0LL << "ns" << nss.ns_forTest() << "firstBatch"
<< indexDocsWithCollation));
});
}
@ -354,15 +363,17 @@ TEST_F(RecipientServiceExternalStateTest, CreateLocalReshardingCollectionBasic)
kReshardingTimestamp);
const std::vector<BSONObj> indexes = {
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key"
<< BSON("a" << 1 << "b"
<< "hashed")
<< "name"
<< "indexOne")};
<< "indexOne" << "collation" << BSON("locale" << "simple"))};
// When creating collection, only _id index should be created.
const std::vector<BSONObj> expectedIndexes = {
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName)};
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple"))};
auto future = launchAsync([&] {
expectRefreshReturnForOriginalColl(
kOrigNss, kShardKey, kOrigUUID, kOrigEpoch, kOrigTimestamp);
@ -418,15 +429,17 @@ TEST_F(RecipientServiceExternalStateTest,
kReshardingTimestamp);
const std::vector<BSONObj> indexes = {
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName),
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple")),
BSON("v" << 2 << "key"
<< BSON("a" << 1 << "b"
<< "hashed")
<< "name"
<< "indexOne")};
<< "indexOne" << "collation" << BSON("locale" << "simple"))};
// When creating collection, only _id index should be created.
const std::vector<BSONObj> expectedIndexes = {
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName)};
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple"))};
auto future = launchAsync([&] {
expectRefreshReturnForOriginalColl(
@ -495,9 +508,7 @@ TEST_F(RecipientServiceExternalStateTest,
<< "hashed")
<< "name"
<< "indexOne")};
// When creating collection, only _id index should be created.
const std::vector<BSONObj> expectedIndexes = {
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName)};
// Create the collection and indexes to simulate retrying after a failover. Only include the id
// index, because it is needed to create the collection.
CollectionOptionsAndIndexes optionsAndIndexes = {
@ -530,6 +541,11 @@ TEST_F(RecipientServiceExternalStateTest,
future.default_timed_get();
// When creating collection, only _id index should be created.
const std::vector<BSONObj> expectedIndexes = {
BSON("v" << 2 << "key" << BSON("_id" << 1) << "name" << IndexConstants::kIdIndexName
<< "collation" << BSON("locale" << "simple"))};
verifyCollectionAndIndexes(kReshardingNss, kReshardingUUID, expectedIndexes);
}

View File

@ -301,6 +301,13 @@ feature_flags:
cpp_varname: gFeatureFlagQEPrefixSuffixSearch
default: false
fcv_gated: true
# TODO (SERVER-122417): Remove this feature flag once v9.0 branches out.
featureFlagListIndexesAlwaysIncludesSimpleCollation:
description: "Feature flag to enable the listIndexes command to include the collation field in the index specs, even when it is the simple collation."
cpp_varname: feature_flags::gFeatureFlagListIndexesAlwaysIncludesSimpleCollation
default: true
version: 9.0
fcv_gated: true
featureFlagIncludeDeltaHeaderForPhylog:
description: "Enable including delta headers for phylog pages."
cpp_varname: gFeatureFlagIncludeDeltaHeaderForPhylog

View File

@ -35,6 +35,8 @@
#include "mongo/bson/bsontypes.h"
#include "mongo/db/index/index_constants.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/query/collation/collation_spec.h"
#include "mongo/db/shard_role/shard_catalog/index_descriptor.h"
#include "mongo/idl/idl_parser.h"
#include "mongo/util/assert_util.h"
@ -108,12 +110,16 @@ bool requiresLegacyFormat(const NamespaceString& nss, const CollectionOptions& c
BSONObj formatClusterKeyForListIndexes(const ClusteredCollectionInfo& collInfo,
const BSONObj& collation,
const boost::optional<int64_t>& expireAfterSeconds) {
const boost::optional<int64_t>& expireAfterSeconds,
bool extendSimpleCollation) {
BSONObjBuilder bob;
collInfo.getIndexSpec().serialize(&bob);
if (!collation.isEmpty()) {
bob.append("collation", collation);
} else if (extendSimpleCollation) {
bob.append(IndexDescriptor::kCollationFieldName, CollationSpec::kSimpleSpec);
}
if (expireAfterSeconds) {
bob.append("expireAfterSeconds", expireAfterSeconds.value());
}

View File

@ -77,7 +77,8 @@ MONGO_MOD_PUBLIC bool requiresLegacyFormat(const NamespaceString& nss,
MONGO_MOD_PUBLIC BSONObj
formatClusterKeyForListIndexes(const ClusteredCollectionInfo& collInfo,
const BSONObj& collation,
const boost::optional<int64_t>& expireAfterSeconds);
const boost::optional<int64_t>& expireAfterSeconds,
bool extendSimpleCollation = false);
/**
* Returns true if the BSON object matches the collection's cluster key. Caller's should ensure

View File

@ -632,7 +632,8 @@ public:
virtual int getCompletedIndexCount() const = 0;
virtual BSONObj getIndexSpec(StringData indexName) const = 0;
virtual BSONObj getIndexSpec(StringData indexName,
bool expandSimpleCollation = false) const = 0;
virtual void getAllIndexes(std::vector<std::string>* names) const = 0;

View File

@ -1965,13 +1965,43 @@ int CollectionImpl::getCompletedIndexCount() const {
return num;
}
BSONObj CollectionImpl::getIndexSpec(StringData indexName) const {
BSONObj CollectionImpl::getIndexSpec(StringData indexName, bool expandSimpleCollation) const {
int offset = _metadata->findIndexOffset(indexName);
invariant(offset >= 0,
str::stream() << "cannot get index spec for " << indexName << " @ " << getCatalogId()
<< " : " << _metadata->toBSON());
return _metadata->indexes[offset].spec;
const auto& spec = _metadata->indexes[offset].spec;
if (!expandSimpleCollation) {
return spec;
}
// TODO (SERVER-119573): Remove manual addition of the 'collation' field here once index specs
// in the catalog always include the 'collation' metadata. At that point, explicit insertion
// will no longer be needed.
BSONObjBuilder bob;
// Ensure top-level spec has a simple collation if missing.
if (!spec.hasField(IndexDescriptor::kCollationFieldName)) {
bob.append(IndexDescriptor::kCollationFieldName, CollationSpec::kSimpleSpec);
}
// If 'originalSpec' is present, ensure it contains the 'collation' field setting it to a simple
// collation when missing.
if (spec.hasField(IndexDescriptor::kOriginalSpecFieldName)) {
BSONObj originalSpec = spec[IndexDescriptor::kOriginalSpecFieldName].Obj();
if (originalSpec.hasField(IndexDescriptor::kCollationFieldName)) {
bob.append(IndexDescriptor::kOriginalSpecFieldName, originalSpec);
} else {
bob.append(IndexDescriptor::kOriginalSpecFieldName,
originalSpec.addFields(BSON(IndexDescriptor::kCollationFieldName
<< CollationSpec::kSimpleSpec)));
}
}
bob.appendElementsUnique(spec);
return bob.obj();
}
void CollectionImpl::getAllIndexes(std::vector<std::string>* names) const {

View File

@ -385,7 +385,7 @@ public:
int getCompletedIndexCount() const final;
BSONObj getIndexSpec(StringData indexName) const final;
BSONObj getIndexSpec(StringData indexName, bool expandSimpleCollation) const final;
void getAllIndexes(std::vector<std::string>* names) const final;

View File

@ -445,7 +445,7 @@ public:
MONGO_UNREACHABLE;
}
BSONObj getIndexSpec(StringData indexName) const override {
BSONObj getIndexSpec(StringData indexName, bool expandSimpleCollation) const override {
MONGO_UNREACHABLE;
}

View File

@ -30,6 +30,7 @@
#include "mongo/db/shard_role/shard_catalog/index_catalog.h"
#include "mongo/bson/simple_bsonobj_comparator.h"
#include "mongo/db/shard_role/shard_catalog/collection.h"
#include "mongo/util/assert_util.h"
@ -93,6 +94,50 @@ std::vector<BSONObj> IndexCatalog::normalizeIndexSpecs(OperationContext* opCtx,
return results;
}
std::vector<BSONObj> IndexCatalog::normalizeIndexSpecsFromListIndexes(
const std::vector<BSONObj>& indexSpecs) {
std::vector<BSONObj> results;
results.reserve(indexSpecs.size());
for (const auto& indexSpec : indexSpecs) {
results.emplace_back(normalizeIndexSpecFromListIndexes(indexSpec));
}
return results;
}
BSONObj IndexCatalog::normalizeIndexSpecFromListIndexes(const BSONObj& indexSpec) {
auto removeSimpleCollationFromBsonObj = [](BSONObj obj) -> BSONObj {
if (obj.hasField(IndexDescriptor::kCollationFieldName) &&
SimpleBSONObjComparator::kInstance.evaluate(
obj[IndexDescriptor::kCollationFieldName].Obj() == CollationSpec::kSimpleSpec)) {
return obj.removeField(IndexDescriptor::kCollationFieldName);
}
return obj;
};
BSONObjBuilder bob;
// Remove simple collation under 'spec' field
constexpr auto kSpecFieldName = "spec"_sd;
if (indexSpec.hasField(kSpecFieldName)) {
bob.append(kSpecFieldName,
removeSimpleCollationFromBsonObj(indexSpec[kSpecFieldName].Obj()));
}
// Remove simple collation under 'originalSpec' field
if (indexSpec.hasField(IndexDescriptor::kOriginalSpecFieldName)) {
bob.append(IndexDescriptor::kOriginalSpecFieldName,
removeSimpleCollationFromBsonObj(
indexSpec[IndexDescriptor::kOriginalSpecFieldName].Obj()));
}
// Remove top-level simple collation if exists
bob.appendElementsUnique(removeSimpleCollationFromBsonObj(indexSpec));
return bob.obj();
}
Status IndexCatalog::canCreateIndex(OperationContext* opCtx,
const CollectionPtr& collection,
const BSONObj& indexSpec) const {

View File

@ -559,7 +559,17 @@ public:
* collation spec in cases where the index spec specifies a collation, and will add the
* collection-default collation, if present, in cases where collation is omitted. If the index
* spec omits the collation and the collection does not have a default, the collation field is
* omitted from the spec.
* omitted from the spec. Additionally, if the resolved collation is {locale: "simple"}, the
* returned normalized spec will not include a collation field.
*
* Clarification: For a non-normalized index spec, if the collation field is missing, it is
* treated as the collection's default collation. For a normalized index spec, if the collation
* field is missing, it indicates a simple collation.
* TODO (SERVER-119573): Review and update the above paragraph if collation normalization logic
* changes.
*
* WARNING: Do not call this function on an already normalized spec: it may incorrectly apply
* the collections default collation when a simple collation was explicitly requested.
*
* This function throws on error.
*/
@ -570,6 +580,24 @@ public:
const CollectionPtr& collection,
const std::vector<BSONObj>& indexSpecs);
/**
* Helper function that converts index specs returned by listIndexes into normalized index specs
* for storage in the catalog. Index specs from listIndexes always include a collation field,
* but normalized specs should omit the collation field when it represents the simple collation
* (i.e., {locale: "simple"}).
* In summary, this function strips the collation field if it is {locale: "simple"}.
* TODO (SERVER-119573): Remove this methods once the catalog stores simple collation
* explicitly.
*
* WARNING: Do not call this function on an already normalized spec: it may incorrectly apply
* the collections default collation when a simple collation was explicitly requested.
*/
// TODO (SERVER-122859): Remove this helper function once IndexBuildsCoordinator is updated to
// always expect an explicit collation in index specs, including when the collation is simple.
static std::vector<BSONObj> normalizeIndexSpecsFromListIndexes(
const std::vector<BSONObj>& indexSpecs);
static BSONObj normalizeIndexSpecFromListIndexes(const BSONObj& indexSpec);
/**
* Checks if the given index spec could be created in this catalog after normalizing it,
* returning an error with a reason if it cannot.

View File

@ -41,6 +41,7 @@
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/server_feature_flags_gen.h"
#include "mongo/db/shard_role/shard_catalog/clustered_collection_options_gen.h"
#include "mongo/db/shard_role/shard_catalog/clustered_collection_util.h"
#include "mongo/db/shard_role/shard_catalog/collection.h"
@ -88,6 +89,11 @@ std::vector<BSONObj> listIndexesInLock(OperationContext* opCtx,
const bool convertBucketsIndexesToTimeseriesIndexes =
collection->isTimeseriesCollection() && !isRawDataRequest;
const bool expandSimpleCollation =
feature_flags::gFeatureFlagListIndexesAlwaysIncludesSimpleCollation.isEnabled(
VersionContext::getDecoration(opCtx),
serverGlobalParams.featureCompatibility.acquireFCVSnapshot());
if (collection->isClustered() && !collection->isTimeseriesCollection()) {
BSONObj collation;
if (auto collator = collection->getDefaultCollator()) {
@ -96,7 +102,8 @@ std::vector<BSONObj> listIndexesInLock(OperationContext* opCtx,
auto clusteredSpec = clustered_util::formatClusterKeyForListIndexes(
collection->getClusteredInfo().value(),
collation,
collection->getCollectionOptions().expireAfterSeconds);
collection->getCollectionOptions().expireAfterSeconds,
expandSimpleCollation);
if (additionalInclude == ListIndexesInclude::kIndexBuildInfo) {
indexSpecs.push_back(BSON("spec"_sd << clusteredSpec));
} else {
@ -104,7 +111,7 @@ std::vector<BSONObj> listIndexesInLock(OperationContext* opCtx,
}
}
for (size_t i = 0; i < indexNames.size(); i++) {
auto spec = collection->getIndexSpec(indexNames[i]);
auto spec = collection->getIndexSpec(indexNames[i], expandSimpleCollation);
if (convertBucketsIndexesToTimeseriesIndexes) {
auto timeseriesSpec = timeseries::createTimeseriesIndexFromBucketsIndex(
*collection->getTimeseriesOptions(), spec);

View File

@ -48,6 +48,12 @@ enum class MONGO_MOD_NEEDS_REPLACEMENT ListIndexesInclude { kNothing, kBuildUUID
/**
* Returns the list of indexes for the given collection.
*
* Returned index specifications are not normalized: the 'collation' field is always included, even
* when its value is simple. In contrast, in normalized index specifications, the simple collation
* is typically omitted. For cloning purposes, these index specs can be used directly with a
* createIndexes-style command, since normalization will occur automatically on the server. However,
* they should not be used to create a new index if the normalization step is bypassed.
*
* If 'isRawDataRequest' is true, indexes on timeseries collections are returned as raw bucket
* indexes (i.e. using internal bucket field names such as {control.min.x: 1, control.max.x: 1})
* rather than being translated to their user-visible timeseries form (e.g. {x: 1}).
@ -64,6 +70,12 @@ MONGO_MOD_PRIVATE std::vector<BSONObj> listIndexesInLock(
*
* If the collection does not exist, returns an empty list.
*
* Returned index specifications are not normalized: the 'collation' field is always included, even
* when its value is simple. In contrast, in normalized index specifications, the simple collation
* is typically omitted. For cloning purposes, these index specs can be used directly with a
* createIndexes-style command, since normalization will occur automatically on the server. However,
* they should not be used to create a new index if the normalization step is bypassed.
*
* If 'isRawDataRequest' is true, indexes on timeseries collections are returned as raw bucket
* indexes (i.e. using internal bucket field names such as {control.min.x: 1, control.max.x: 1})
* rather than being translated to their user-visible timeseries form (e.g. {x: 1}).

View File

@ -389,8 +389,19 @@ Status renameCollectionWithinDB(OperationContext* opCtx,
// Check target collection indexes match expected.
const auto currentIndexes = listIndexesEmptyListIfMissing(
opCtx, target, ListIndexesInclude::kNothing, /*isRawDataRequest=*/true);
// The listIndexes command always includes a 'collation' field in each index spec as of
// SERVER-89953, even if it has a simple collation. In contrast, `options.originalIndexes`
// are normalized specs, which means that the 'collation' field is omitted when it is a
// simple collation. Therefore, we normalize the listIndexes output here to enable direct
// comparison between both formats.
// TODO (SERVER-119573): Remove this normalization once listIndexes output matches storage
// format.
const auto normalizedCurrentIndexes =
IndexCatalog::normalizeIndexSpecsFromListIndexes(currentIndexes);
status = checkTargetCollectionIndexesMatch(
target, options.originalIndexes.get(), currentIndexes);
target, options.originalIndexes.get(), normalizedCurrentIndexes);
if (!status.isOK()) {
return status;

View File

@ -426,7 +426,7 @@ public:
return 0;
}
BSONObj getIndexSpec(StringData indexName) const final {
BSONObj getIndexSpec(StringData indexName, bool expandSimpleCollation) const final {
return BSONObj();
}