SERVER-116179 Bump 2dsphereIndexVersion and check for v4 indexes persisted as v3 (#47621)

GitOrigin-RevId: ddc6b380801bc7cc06f81d52fbb40fef2f6fd573
This commit is contained in:
Finley Lau 2026-02-06 09:30:49 -06:00 committed by MongoDB Bot
parent 72b619cab5
commit 406cb06ea4
26 changed files with 971 additions and 91 deletions

1
.github/CODEOWNERS vendored
View File

@ -1208,6 +1208,7 @@ WORKSPACE.bazel @10gen/devprod-build @svc-auto-approve-bot
/jstests/multiVersion/genericSetFCVUsage/**/cluster_parameters_disabled_correctly_after_downgrade.js @10gen/server-catalog-and-routing-routing-and-topology @svc-auto-approve-bot
/jstests/multiVersion/genericSetFCVUsage/**/priority_port_downgrade_checks.js @10gen/server-catalog-and-routing-routing-and-topology @svc-auto-approve-bot
/jstests/multiVersion/genericSetFCVUsage/**/*upgrade_downgrade_viewless_timeseries_*.js @10gen/server-catalog-and-routing-ddl @svc-auto-approve-bot
/jstests/multiVersion/genericSetFCVUsage/**/s2_index_v4_downgrade.js @10gen/query-integration-features @svc-auto-approve-bot
# The following patterns are parsed from ./jstests/multiVersion/genericSetFCVUsage/collection_write_path/OWNERS.yml
/jstests/multiVersion/genericSetFCVUsage/collection_write_path/**/* @10gen/server-collection-write-path @svc-auto-approve-bot

View File

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

View File

@ -8,6 +8,8 @@
* ]
*/
import {ChangeStreamTest} from "jstests/libs/query/change_stream_util.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
const testDB = db.getSiblingDB(jsTestName());
@ -112,7 +114,17 @@ function runTest(startChangeStream, pipeline, insertDataBeforeCreateIndex) {
testCreateIndexAndDropIndex({f: "2d"}, options);
// Test createIndex() for a 2dsphere index with various options, followed by dropIndex().
options = {name: "2dsphere", "2dsphereIndexVersion": 3};
// The 2dsphere index version depends on whether featureFlag2dsphereIndexVersion4 is enabled:
// - If enabled: defaults to version 4, which is included in change stream events.
// - If disabled: defaults to version 3, which is included in change stream events.
options = {name: "2dsphere"};
var twoDSphereIndexVersion = FeatureFlagUtil.isPresentAndEnabled(db, "2dsphereIndexVersion4") ? 4 : 3;
if (!isStableFCVSuite()) {
// If we are upgrading/downgrading the FCV, avoid having to drop any v4 indexes by pinning the version to 3
// TODO SERVER-118561 Remove this when 9.0 is last LTS.
twoDSphereIndexVersion = 3;
}
options["2dsphereIndexVersion"] = twoDSphereIndexVersion;
testCreateIndexAndDropIndex({f: "2dsphere"}, options);
// Test createIndexes() to create two sparse indexes (with one index being a compound index),

View File

@ -16,6 +16,8 @@ import {
getTimeseriesBucketsColl,
} from "jstests/core/timeseries/libs/viewless_timeseries_util.js";
import {assertCatalogListOperationsConsistencyForCollection} from "jstests/libs/catalog_list_operations_consistency_validator.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
// Validate catalog list operations consistency after each command,
// so that if an inconsistency is introduced, we fail immediately.
@ -130,13 +132,19 @@ createIndexAndCheckConsistency(db.collection_simple, {f2dNonIntBits: "2d"}, {bit
createIndexAndCheckConsistency(db.collection_simple, {f2dSphere: "2dsphere"});
createIndexAndCheckConsistency(db.timeseries_simple, {"timestamp": 1});
var twoDSphereIndexVersion = FeatureFlagUtil.isPresentAndEnabled(db, "2dsphereIndexVersion4") ? 4 : 3;
if (!isStableFCVSuite()) {
// If we are upgrading/downgrading the FCV, avoid having to drop any v4 indexes by pinning the version to 3
// TODO SERVER-118561 Remove this when 9.0 is last LTS.
twoDSphereIndexVersion = 3;
}
// TODO(SERVER-97084): Remove when options for index plugins are denied in basic indexes.
createIndexAndCheckConsistency(
db.collection_simple,
{fUnrelatedIndexPluginOptions: 1},
{
textIndexVersion: 3,
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": FeatureFlagUtil.isPresentAndEnabled(db, "2dsphereIndexVersion4") ? 4 : 3,
bits: 26,
min: -180,
max: 180,

View File

@ -7,9 +7,27 @@
// Tests 2dsphere index option "2dsphereIndexVersion". Verifies that GeoJSON objects that are new
// in version 2 are not allowed in version 1.
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
let coll = db.getCollection("geo_s2indexversion1");
coll.drop();
/**
* Helper function to get the appropriate 2dsphere index version based on feature flag.
* Returns version 4 if the feature flag is enabled and we're in a stable FCV suite.
* Returns version 3 otherwise (to avoid having to drop v4 indexes during FCV transitions).
*/
function get2dsphereIndexVersion() {
const version = FeatureFlagUtil.isPresentAndEnabled(db, "2dsphereIndexVersion4") ? 4 : 3;
if (!isStableFCVSuite()) {
// If we are upgrading/downgrading the FCV, avoid having to drop any v4 indexes by pinning the version to 3.
// TODO SERVER-118561 Remove this when 9.0 is last LTS.
return 3;
}
return version;
}
//
// Index build should fail for invalid values of "2dsphereIndexVersion".
//
@ -23,7 +41,7 @@ res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": 0});
assert.commandFailed(res);
coll.drop();
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": 4});
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": 5});
assert.commandFailed(res);
coll.drop();
@ -71,16 +89,51 @@ res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": NumberLong(2)
assert.commandWorked(res);
coll.drop();
//
// {2dsphereIndexVersion: 3} should be the default for new indexes.
//
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": 3});
assert.commandWorked(res);
coll.drop();
res = coll.createIndex({geo: "2dsphere"});
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": NumberInt(3)});
assert.commandWorked(res);
coll.drop();
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": NumberLong(3)});
assert.commandWorked(res);
coll.drop();
const kDefault2dSphereIndexVersion = get2dsphereIndexVersion();
if (kDefault2dSphereIndexVersion == 4) {
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": 4});
assert.commandWorked(res);
coll.drop();
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": NumberInt(4)});
assert.commandWorked(res);
coll.drop();
res = coll.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": NumberLong(4)});
assert.commandWorked(res);
coll.drop();
}
//
// The default 2dsphere index version should match what the feature flag determines.
// Avoid allowing default version in background fcv upgrade/downgrade tests.
//
const options = {
name: "geo_2dsphere",
};
// TODO SERVER-118561 Remove this when 9.0 is last LTS.
if (!isStableFCVSuite()) {
options["2dsphereIndexVersion"] = get2dsphereIndexVersion(); // Note this will always be v3.
}
res = coll.createIndex({geo: "2dsphere"}, options);
assert.commandWorked(res);
let specObj = coll.getIndexes().filter(function (z) {
return z.name == "geo_2dsphere";
})[0];
assert.eq(3, specObj["2dsphereIndexVersion"]);
assert.eq(kDefault2dSphereIndexVersion, specObj["2dsphereIndexVersion"]);
coll.drop();
//

View File

@ -1,7 +1,7 @@
// This test verifies that queries on GeoJSON work regardless of extra fields and their
// position in respect to the GeoJSON fields.
// @tags: [
// requires_fcv_82,
// requires_fcv_83,
// # geoNear requires setup such as a optimizations to be enabled for TS. See timeseries_geonear.js
// exclude_from_timeseries_crud_passthrough,
// ]

View File

@ -10,6 +10,7 @@
* ]
*/
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
// TODO (SERVER-117130): Remove the mongos pinning once the related issue is resolved.
// When a database is dropped, a stale router will report "database not found" error for
@ -20,6 +21,21 @@ if (TestData.pauseMigrationsDuringMultiUpdates) {
const collection = db.index_key_expression;
/**
* Helper function to get the appropriate 2dsphere index version based on feature flag.
* Returns version 4 if the feature flag is enabled and we're in a stable FCV suite.
* Returns version 3 otherwise (to avoid having to drop v4 indexes during FCV transitions).
*/
function get2dsphereIndexVersion() {
const version = FeatureFlagUtil.isPresentAndEnabled(db, "2dsphereIndexVersion4") ? 4 : 3;
if (!isStableFCVSuite()) {
// If we are upgrading/downgrading the FCV, avoid having to drop any v4 indexes by pinning the version to 3.
// TODO SERVER-118561 Remove this when 9.0 is last LTS.
return 3;
}
return version;
}
/**
* Returns the hash of the provided BSON element that is compatible with 'hashed' indexes.
*/
@ -27,6 +43,9 @@ function getHash(bsonElement) {
return assert.commandWorked(db.runCommand({_hashBSONElement: bsonElement, seed: 0})).out;
}
// Get the appropriate 2dsphere index version for this test run.
const twoDSphereIndexVersion = get2dsphereIndexVersion();
// A dictionary consisting of various test scenarios that must be run against the
// '$_internalIndexKey'.
//
@ -594,7 +613,7 @@ const testScenarios = [
},
spec: {
key: {"data.geo": "2dsphere_bucket"},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedIndexKeys: [
@ -618,7 +637,7 @@ const testScenarios = [
},
spec: {
key: {"data.geo1": "2dsphere_bucket", "data.geo2": "2dsphere_bucket"},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedIndexKeys: [
@ -656,7 +675,7 @@ const testScenarios = [
},
spec: {
key: {"data.geo2": "2dsphere_bucket", "data.geo1": "2dsphere_bucket"},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedIndexKeys: [
@ -694,7 +713,7 @@ const testScenarios = [
},
spec: {
key: {"data.geo1": "2dsphere_bucket", "data.geo2": 1},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedIndexKeys: [
@ -726,7 +745,7 @@ const testScenarios = [
},
spec: {
key: {"data.none1": "2dsphere_bucket"},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedIndexKeys: [],
@ -738,7 +757,7 @@ const testScenarios = [
},
spec: {
key: {"data.geo": "2dsphere_bucket"},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedErrorCode: 183934, // Can't extract geo keys: unknown GeoJSON type.
@ -754,7 +773,7 @@ const testScenarios = [
},
spec: {
key: {"data.geo": "2dsphere_bucket"},
"2dsphereIndexVersion": 3,
"2dsphereIndexVersion": twoDSphereIndexVersion,
name: "2dsphereBucketIndex",
},
expectedErrorCode: 6540600, // Time-series bucket documents must have 'control' object present.

View File

@ -94,6 +94,31 @@ TimeseriesTest.run((insert) => {
return new Date(date - (date % 60000));
};
/**
* Helper function to check if an index spec contains a 2dsphere index.
*/
const has2dsphereIndex = function (spec) {
for (const key in spec) {
if (spec[key] === "2dsphere") {
return true;
}
}
return false;
};
/**
* Helper function to add 2dsphereIndexVersion: 3 to options if the spec contains a 2dsphere index.
* TODO SERVER-118561 Remove this and use the default server-selected version when 9.0 is last LTS.
*/
const add2dsphereVersionIfNeeded = function (spec, options = {}) {
if (has2dsphereIndex(spec) && TestData.isRunningFCVUpgradeDowngradeSuite) {
// Pin the index version to v3 when upgrading/downgrading FCV during the test run,
// such that we don't need to drop v4 indexes to downgrade the FCV.
options["2dsphereIndexVersion"] = 3;
}
return options;
};
/**
* Tests time-series
* - createIndex
@ -140,7 +165,10 @@ TimeseriesTest.run((insert) => {
// Insert data on the time-series collection and index it.
assert.commandWorked(insert(coll, doc), "failed to insert doc: " + tojson(doc));
assert.commandWorked(coll.createIndex(spec), "failed to create index: " + tojson(spec));
assert.commandWorked(
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec)),
"failed to create index: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);
assertIndexNotHidden(coll, spec, bucketSpec);
@ -205,14 +233,20 @@ TimeseriesTest.run((insert) => {
);
// Check that we are able to drop the index by name (single name and array of names).
assert.commandWorked(coll.createIndex(spec, {name: "myindex1"}), "failed to create index: " + tojson(spec));
assert.commandWorked(
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec, {name: "myindex1"})),
"failed to create index: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);
assertIndexNotHidden(coll, spec, bucketSpec);
assert.commandWorked(coll.dropIndex("myindex1"), "failed to drop index: myindex1");
assertIndexNotExists(coll, spec, bucketSpec);
assert.commandWorked(coll.createIndex(spec, {name: "myindex2"}), "failed to create index: " + tojson(spec));
assert.commandWorked(
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec, {name: "myindex2"})),
"failed to create index: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);
assertIndexNotHidden(coll, spec, bucketSpec);
@ -220,7 +254,10 @@ TimeseriesTest.run((insert) => {
assertIndexNotExists(coll, spec, bucketSpec);
// Check that we are able to hide and unhide the index by name.
assert.commandWorked(coll.createIndex(spec, {name: "hide1"}), "failed to create index: " + tojson(spec));
assert.commandWorked(
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec, {name: "hide1"})),
"failed to create index: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);
assertIndexNotHidden(coll, spec, bucketSpec);
@ -247,7 +284,10 @@ TimeseriesTest.run((insert) => {
assertIndexNotExists(coll, spec, bucketSpec);
// Check that we are able to hide and unhide the index by key.
assert.commandWorked(coll.createIndex(spec, {name: "hide2"}), "failed to create index: " + tojson(spec));
assert.commandWorked(
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec, {name: "hide2"})),
"failed to create index: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);
assertIndexNotHidden(coll, spec, bucketSpec);
@ -273,7 +313,7 @@ TimeseriesTest.run((insert) => {
// Check that we are able to create the index as hidden.
assert.commandWorked(
coll.createIndex(spec, {name: "hide3", hidden: true}),
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec, {name: "hide3", hidden: true})),
"failed to create index: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);
@ -297,7 +337,7 @@ TimeseriesTest.run((insert) => {
// Check that user hints on queries will be allowed and will reference the raw indexes on
// the buckets directly.
assert.commandWorked(
coll.createIndex(spec, {name: "index_for_hint_test"}),
coll.createIndex(spec, add2dsphereVersionIfNeeded(spec, {name: "index_for_hint_test"})),
"failed to create index index_for_hint_test: " + tojson(spec),
);
assertIndexExists(coll, spec, bucketSpec);

View File

@ -19,6 +19,8 @@
import {getTimeseriesCollForRawOps, kRawOperationSpec} from "jstests/core/libs/raw_operation_utils.js";
import {TimeseriesTest} from "jstests/core/timeseries/libs/timeseries.js";
import {getAggPlanStage} from "jstests/libs/query/analyze_plan.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {isStableFCVSuite} from "jstests/libs/feature_compatibility_version.js";
TimeseriesTest.run((insert) => {
const testdb = db.getSiblingDB(jsTestName() + "_db");
@ -51,17 +53,38 @@ TimeseriesTest.run((insert) => {
// Create a 2dsphere index on the time-series collection.
const twoDSphereTimeseriesIndexSpec = {"location": "2dsphere"};
const twoDSphereTimeseriesIndexName = "location_2dsphere";
const options = {
name: twoDSphereTimeseriesIndexName,
};
// TODO SERVER-118561 Remove this and use the default server-selected version when 9.0 is last LTS.
if (TestData.isRunningFCVUpgradeDowngradeSuite) {
// Pin the index version to v3 when upgrading/downgrading FCV during the test run,
// such that we don't need to drop v4 indexes to downgrade the FCV.
// Otherwise, use the default version.
options["2dsphereIndexVersion"] = 3;
}
assert.commandWorked(
timeseriescoll.createIndex(twoDSphereTimeseriesIndexSpec, {
name: twoDSphereTimeseriesIndexName,
"2dsphereIndexVersion": 3,
}),
timeseriescoll.createIndex(twoDSphereTimeseriesIndexSpec, options),
"Failed to create a 2dsphere index with: " + tojson(twoDSphereTimeseriesIndexSpec),
);
// Verify that the 2dsphereIndexVersion field is visible on the collection.
const created = timeseriescoll.getIndexes().filter((idx) => idx.name === twoDSphereTimeseriesIndexName)[0];
assert.eq(created["2dsphereIndexVersion"], 3, "Created index does not have version field.");
// Build set of allowed versions. Skip v3 if feature flag is enabled and we're in a stable FCV suite.
const allowedVersions = new Set([4]);
const shouldSkipV3 = FeatureFlagUtil.isPresentAndEnabled(testdb, "2dsphereIndexVersion4") && isStableFCVSuite();
if (!shouldSkipV3) {
// TODO SERVER-118561 We can remove this when 9.0 becomes last LTS.
allowedVersions.add(3);
}
assert(
allowedVersions.has(created["2dsphereIndexVersion"]),
"Created index does not have valid version field. Expected one of: " +
Array.from(allowedVersions).join(", ") +
", got: " +
created["2dsphereIndexVersion"],
);
// Insert a 2dsphere index usable document.
const twoDSphereDocs = [
{

View File

@ -42,3 +42,6 @@ filters:
- "*upgrade_downgrade_viewless_timeseries_*.js":
approvers:
- 10gen/server-catalog-and-routing-ddl
- "s2_index_v4_downgrade.js":
approvers:
- 10gen/query-integration-features

View File

@ -0,0 +1,215 @@
/**
* Tests that downgrading FCV below 8.3 fails when v4 2dsphere indexes exist.
* This test covers both sharded clusters and replica sets.
*
* This test:
* 1. Starts a sharded cluster and replica set with latest version
* 2. Creates some v4 2dsphere indexes
* 3. Attempts to downgrade FCV to 8.0
* 4. Validates that the FCV downgrade fails with CannotDowngrade error
* 5. Drops the v4 indexes
* 6. Attempts to downgrade FCV again and validates that it succeeds
* 7. Tests that v3 2dsphere indexes can be created on 8.3 binary with FCV 8.0
*
* TODO SERVER-118561 Remove this test file when 9.0 is last LTS.
*/
import "jstests/multiVersion/libs/verify_versions.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
const targetDowngradeVersion = "8.0";
/**
* Runs the v4 2dsphere index downgrade test against the provided connection.
* @param {Mongo} conn - The connection to run the test against (mongos or replica set primary)
* @param {string} testName - The name of the test for logging purposes
*/
function runS2IndexV4DowngradeTest(conn, testName) {
jsTest.log.info(
`Starting test: ${testName} - downgrading FCV below 8.3 should fail when v4 2dsphere indexes exist`,
);
const testDB = conn.getDB(jsTestName() + "_" + testName.replace(/\s+/g, "_"));
const coll = testDB.getCollection("geo_coll");
// Verify we're on latest FCV.
const adminDB = conn.getDB("admin");
checkFCV(adminDB, latestFCV);
jsTest.log.info(`[${testName}] Creating v4 2dsphere indexes`);
// Create a collection and insert some documents with geo data.
assert.commandWorked(
coll.insert([
{_id: 1, location: {type: "Point", coordinates: [40, -70]}},
{_id: 2, location: {type: "Point", coordinates: [41, -71]}},
{_id: 3, location: {type: "Point", coordinates: [42, -72]}},
]),
);
// Create multiple v4 2dsphere indexes.
assert.commandWorked(
coll.createIndex({location: "2dsphere"}, {"2dsphereIndexVersion": 4}),
"Failed to create first v4 2dsphere index",
);
// Create another v4 index on a different collection.
const coll2 = testDB.getCollection("geo_coll2");
assert.commandWorked(coll2.insert([{_id: 1, geo: {type: "Point", coordinates: [50, -80]}}]));
assert.commandWorked(
coll2.createIndex({geo: "2dsphere"}, {"2dsphereIndexVersion": 4}),
"Failed to create second v4 2dsphere index",
);
// Verify the indexes were created with version 4.
const indexes1 = coll.getIndexes();
const locationIndex = indexes1.find((idx) => idx.name === "location_2dsphere");
assert.neq(null, locationIndex, "location_2dsphere index not found");
assert.eq(4, locationIndex["2dsphereIndexVersion"], "Index should have version 4");
const indexes2 = coll2.getIndexes();
const geoIndex = indexes2.find((idx) => idx.name === "geo_2dsphere");
assert.neq(null, geoIndex, "geo_2dsphere index not found");
assert.eq(4, geoIndex["2dsphereIndexVersion"], "Index should have version 4");
jsTest.log.info(`[${testName}] Attempting to downgrade FCV to ${targetDowngradeVersion}`);
const downgradeFCV = binVersionToFCV(targetDowngradeVersion);
// Attempt to downgrade FCV - this should fail due to v4 indexes.
jsTest.log.info(`[${testName}] Setting FCV to ${downgradeFCV} - this should fail due to v4 indexes`);
const fcvResult = adminDB.runCommand({
setFeatureCompatibilityVersion: downgradeFCV,
confirm: true,
});
// The downgrade should fail with CannotDowngrade error.
assert.commandFailedWithCode(
fcvResult,
ErrorCodes.CannotDowngrade,
"FCV downgrade should have failed due to v4 indexes",
);
jsTest.log.info(`[${testName}] FCV downgrade failed as expected: ${tojson(fcvResult)}`);
assert(
fcvResult.errmsg.includes("2dsphere") || fcvResult.errmsg.includes("version 4"),
"Error message should mention 2dsphere indexes or version 4",
);
// Verify FCV is still at latest (downgrade should not have proceeded).
checkFCV(adminDB, latestFCV);
// Verify indexes still exist.
assert.neq(
null,
coll.getIndexes().find((idx) => idx.name === "location_2dsphere"),
);
assert.neq(
null,
coll2.getIndexes().find((idx) => idx.name === "geo_2dsphere"),
);
jsTest.log.info(`[${testName}] Dropping v4 2dsphere indexes`);
// Drop the v4 indexes.
assert.commandWorked(coll.dropIndex("location_2dsphere"), "Failed to drop location_2dsphere index");
assert.commandWorked(coll2.dropIndex("geo_2dsphere"), "Failed to drop geo_2dsphere index");
// Verify indexes are dropped.
assert.eq(
null,
coll.getIndexes().find((idx) => idx.name === "location_2dsphere"),
);
assert.eq(
null,
coll2.getIndexes().find((idx) => idx.name === "geo_2dsphere"),
);
jsTest.log.info(
`[${testName}] Attempting to downgrade FCV to ${targetDowngradeVersion} again after dropping indexes`,
);
// Now attempt to downgrade FCV again - this should succeed.
const fcvResult2 = adminDB.runCommand({
setFeatureCompatibilityVersion: downgradeFCV,
confirm: true,
});
// The downgrade should succeed now that v4 indexes are removed.
assert.commandWorked(fcvResult2, "FCV downgrade should succeed after dropping v4 indexes");
// Verify FCV was downgraded.
checkFCV(adminDB, downgradeFCV);
jsTest.log.info(
`[${testName}] Test completed successfully: FCV downgrade correctly failed with v4 indexes and succeeded after dropping them`,
);
// Test that v3 2dsphere indexes can be created on 8.3 binary with FCV 8.0.
jsTest.log.info(`[${testName}] Testing v3 2dsphere index creation with FCV ${downgradeFCV}`);
const coll3 = testDB.getCollection("geo_coll_v3");
// Insert test documents with geo data.
assert.commandWorked(
coll3.insert([
{_id: 1, location: {type: "Point", coordinates: [40, -70]}},
{_id: 2, location: {type: "Point", coordinates: [41, -71]}},
{_id: 3, location: {type: "Point", coordinates: [42, -72]}},
]),
);
// Create a v3 2dsphere index (new default value) - this should succeed on 8.3 binary with FCV 8.0.
assert.commandWorked(
coll3.createIndex({location: "2dsphere"}),
`Failed to create v3 2dsphere index with FCV ${downgradeFCV}`,
);
// Verify the index was created with version 3.
const indexes3 = coll3.getIndexes();
const locationIndexV3 = indexes3.find((idx) => idx.name === "location_2dsphere");
assert.neq(null, locationIndexV3, "location_2dsphere index not found");
assert.eq(3, locationIndexV3["2dsphereIndexVersion"], "Index should have version 3");
jsTest.log.info(`[${testName}] Successfully created v3 2dsphere index on 8.3 binary with FCV 8.0`);
// Verify the index works by running a simple query.
const result = coll3
.find({
location: {
$near: {
$geometry: {type: "Point", coordinates: [40, -70]},
$maxDistance: 1000000,
},
},
})
.toArray();
assert.gte(result.length, 1, "Query using v3 2dsphere index should return results");
jsTest.log.info(`[${testName}] v3 2dsphere index creation works on 8.3 binary with FCV 8.0`);
// Clean up the test database.
assert.commandWorked(testDB.dropDatabase());
}
// Test with sharded cluster.
(function testShardedCluster() {
const st = new ShardingTest({shards: 1, mongos: 1});
try {
runS2IndexV4DowngradeTest(st.s, "sharded cluster");
} finally {
st.stop();
}
})();
// Test with replica set.
(function testReplicaSet() {
const rst = new ReplSetTest({nodes: 3});
rst.startSet();
rst.initiate();
try {
runS2IndexV4DowngradeTest(rst.getPrimary(), "replica set");
} finally {
rst.stopSet();
}
})();

View File

@ -35,7 +35,51 @@ const vectorSearchIndexDef = {
const adminDB = testDb.getMongo().getDB("admin");
/**
* Drops all v4 2dsphere indexes from all collections in all databases.
* This is necessary before downgrading FCV below 8.3, as v4 indexes are not supported.
*/
function dropAllV4_2dsphereIndexes() {
const mongo = adminDB.getMongo();
const dbNames = mongo.getDBNames();
for (const dbName of dbNames) {
const db = mongo.getDB(dbName);
// Use getCollectionInfos to get only collections (not views)
const collInfos = db.getCollectionInfos({type: "collection"});
for (const collInfo of collInfos) {
const collName = collInfo.name;
// Skip system collections
if (collName.startsWith("system.")) {
continue;
}
const coll = db.getCollection(collName);
const indexes = coll.getIndexes();
for (const index of indexes) {
// Check if this is a v4 2dsphere index
if (index.key) {
const keyFields = Object.keys(index.key);
const has2dsphere = keyFields.some((field) => index.key[field] === "2dsphere");
if (has2dsphere && index["2dsphereIndexVersion"] === 4) {
jsTest.log.info(
"Dropping v4 2dsphere index: " + index.name + " on collection " + dbName + "." + collName,
);
assert.commandWorked(coll.dropIndex(index.name));
}
}
}
}
}
}
function upgradeDowngradeFCV(commands) {
// Drop any v4 2dsphere indexes before downgrading, as they are not supported below FCV 8.3.
dropAllV4_2dsphereIndexes();
// Downgrade to lastLTSFCV (8.0).
assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: lastLTSFCV, confirm: true}));

View File

@ -62,6 +62,8 @@
#include "mongo/db/global_catalog/ddl/sharding_ddl_util.h"
#include "mongo/db/global_catalog/ddl/shardsvr_join_ddl_coordinators_request_gen.h"
#include "mongo/db/global_catalog/type_shard_identity.h"
#include "mongo/db/index_builds/index_builds_coordinator.h"
#include "mongo/db/index_names.h"
#include "mongo/db/logical_time.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
@ -1552,6 +1554,48 @@ private:
"Please run ReplSetReconfig to remove priority ports prior to downgrade",
replConfig.getCountOfMembersWithPriorityPort() == 0);
}
// Check for v4 2dsphere indexes when downgrading below 8.3
if (feature_flags::gFeatureFlag2dsphereIndexVersion4
.isDisabledOnTargetFCVButEnabledOnOriginalFCV(requestedVersion, originalVersion)) {
static const std::string kIndexVersionFieldName("2dsphereIndexVersion");
for (const auto& dbName : DatabaseHolder::get(opCtx)->getNames()) {
Lock::DBLock dbLock(opCtx, dbName, MODE_IS);
catalog::forEachCollectionFromDb(
opCtx, dbName, MODE_IS, [&](const Collection* collection) -> bool {
auto indexCatalog = collection->getIndexCatalog();
auto indexIterator = indexCatalog->getIndexIterator(
IndexCatalog::InclusionPolicy::kReady |
IndexCatalog::InclusionPolicy::kUnfinished);
while (indexIterator->more()) {
const IndexCatalogEntry* entry = indexIterator->next();
const IndexDescriptor* descriptor = entry->descriptor();
const BSONObj& infoObj = descriptor->infoObj();
// Check if this is a 2dsphere index
if (descriptor->getAccessMethodName() == IndexNames::GEO_2DSPHERE ||
descriptor->getAccessMethodName() ==
IndexNames::GEO_2DSPHERE_BUCKET) {
BSONElement versionElt = infoObj[kIndexVersionFieldName];
if (versionElt.isNumber() && versionElt.numberInt() == 4) {
uasserted(
ErrorCodes::CannotDowngrade,
fmt::format(
"Cannot downgrade the cluster when there are 2dsphere "
"indexes with version 4. Version 4 indexes require "
"FCV 8.3 or higher. Please drop the index(es) before "
"downgrading. First detected index: {} on collection "
"{} (UUID: {}).",
descriptor->indexName(),
collection->ns().toStringForErrorMsg(),
collection->uuid().toString()));
}
}
}
return true;
});
}
}
}
// Remove cluster parameters from the clusterParameters collections which are not enabled on

View File

@ -1081,6 +1081,48 @@ Status GeometryContainer::parseFromQuery(const BSONElement& elem) {
return status;
}
Status GeometryContainer::parseForS2Version(const BSONElement& elem,
bool skipValidation,
boost::optional<S2IndexVersion> indexVersion) {
bool isLegacyIndex = indexVersion && *indexVersion < S2_INDEX_VERSION_4;
auto tryGeoJSON = [&]() {
return parseFromGeoJSON(skipValidation);
};
auto tryLegacyPoint = [&]() {
_point.reset(new PointWithCRS());
return GeoParser::parseLegacyPoint(elem, _point.get(), true);
};
uassert(11617900,
"2dsphere index field must be an array or an object",
elem.type() == BSONType::object || elem.type() == BSONType::array);
if (elem.type() == BSONType::array) {
// For arrays, only try legacy point parsing, regardless of index version.
// { location: [1, 2] } or { location: [1, 2, 3] }
return tryLegacyPoint();
}
// Element is an object.
// We may have legacy point and/or GeoJSON. Indexes pre-V4 were generated by
// attempting legacy point parsing first if a legacy point existed in the BSON, even if GeOJSON
// also existed.
if (isLegacyIndex) {
return elem.Obj().firstElement().isNumber() ? tryLegacyPoint() : tryGeoJSON();
}
// In indexes V4 and later, prioritize using the GeoJSON in the BSON, rather than the legacy
// point. Fallback to the legacy parser only if necessary (if the GeoJSON is not present).
Status status = tryGeoJSON();
if (status == ErrorCodes::BadValue) {
Status legacyStatus = tryLegacyPoint();
// If legacy parsing succeeds, use it; otherwise keep the original GeoJSON error.
if (legacyStatus.isOK()) {
status = legacyStatus;
}
}
return status;
}
// Examples:
// { location: <GeoJSON> }
// { location: [1, 2] }
@ -1089,46 +1131,16 @@ Status GeometryContainer::parseFromQuery(const BSONElement& elem) {
//
// "elem" is the element that contains geo data. e.g. "location": [1, 2]
// We need the type information to determine whether it's legacy point.
Status GeometryContainer::parseFromStorage(const BSONElement& elem, bool skipValidation) {
Status GeometryContainer::parseFromStorage(const BSONElement& elem,
bool skipValidation,
boost::optional<S2IndexVersion> indexVersion) {
if (!elem.isABSONObj()) {
return Status(ErrorCodes::BadValue,
str::stream() << "geo element must be an array or object: " << elem);
}
_geoElm = elem;
Status status = Status::OK();
if (BSONType::object == elem.type()) {
// GeoJSON
// { location: { type: “Point”, coordinates: [...] } }
status = parseFromGeoJSON(skipValidation);
// It's possible that we are dealing with a legacy point. e.g
// { location: {x: 1, y: 2, type: “Point” } }
// { location: {x: 1, y: 2} }
if (status == ErrorCodes::BadValue) {
// We must reset _point each time we attempt to re-parse, since it may retain info from
// previous attempts.
_point.reset(new PointWithCRS());
Status legacyParsingStatus = GeoParser::parseLegacyPoint(elem, _point.get(), true);
if (legacyParsingStatus.isOK()) {
status = legacyParsingStatus;
} else {
// Return the original error status, as we may be dealing with an invalid GeoJSON
// document. e.g. {type: "Point", coordinates: "hello"}
return status;
}
}
} else {
// Legacy point
// { location: [1, 2] }
// { location: [1, 2, 3] }
// Allow more than two dimensions or extra fields, like [1, 2, 3]
// We must reset _point each time we attempt to re-parse, since it may retain info from
// previous attempts.
_point.reset(new PointWithCRS());
status = GeoParser::parseLegacyPoint(elem, _point.get(), true);
}
Status status = parseForS2Version(elem, skipValidation, indexVersion);
if (!status.isOK())
return status;

View File

@ -35,6 +35,7 @@
#include "mongo/bson/bsonobj.h"
#include "mongo/bson/dotted_path/dotted_path_support.h"
#include "mongo/db/geo/shapes.h"
#include "mongo/db/index/s2_common.h"
#include "mongo/util/modules.h"
#include <memory>
@ -49,6 +50,8 @@
#include <s2region.h>
#include <s2regionunion.h>
#include <boost/optional.hpp>
namespace mongo {
@ -70,7 +73,9 @@ public:
/**
* Loads an empty GeometryContainer from stored geometry.
*/
Status parseFromStorage(const BSONElement& elem, bool skipValidation = false);
Status parseFromStorage(const BSONElement& elem,
bool skipValidation = false,
boost::optional<S2IndexVersion> indexVersion = boost::none);
/**
* Is the geometry any of {Point, Line, Polygon}?
@ -153,6 +158,9 @@ private:
class R2BoxRegion;
Status parseFromGeoJSON(bool skipValidation = false);
Status parseForS2Version(const BSONElement& elem,
bool skipValidation,
boost::optional<S2IndexVersion> indexVersion);
// Does 'this' intersect with the provided type?
bool intersects(const S2Cell& otherPoint) const;

View File

@ -22,6 +22,7 @@ mongo_cc_library(
"//src/mongo/db:server_base",
"//src/mongo/db/geo:geometry",
"//src/mongo/db/geo:geoparser",
"//src/mongo/db/query:query_knobs",
"//src/mongo/db/query/collation:collator_interface",
"//src/mongo/db/storage/key_string",
"//src/third_party/s2",

View File

@ -548,6 +548,36 @@ public:
const KeyStringSet& multikeyMetadataKeys,
const MultikeyPaths& multikeyPaths) const;
/**
* Checks if this access method implements alternative explanations for a missing index entry.
* This allows access methods to return false early before seeking the record from the record
* store, avoiding expensive operations.
*
* Implementations should override this if checkMissingIndexEntryAlternative is overriden.
*/
virtual bool shouldCheckMissingIndexEntryAlternative(OperationContext* opCtx,
const IndexCatalogEntry& entry) const {
return false;
}
/**
* Checks if a missing index entry has an alternative explanation (e.g., version mismatch).
* This allows access methods to handle validation-specific logic without exposing it to
* generic validation code.
*
* Returns boost::none if there's no alternative explanation (normal missing entry error).
* Otherwise returns a pair of (error message, warning message) to be added to validation
* results.
*/
virtual boost::optional<std::pair<std::string, std::string>> checkMissingIndexEntryAlternative(
OperationContext* opCtx,
const IndexCatalogEntry& entry,
const key_string::Value& missingKey,
const RecordId& recordId,
const BSONObj& document) const {
return boost::none;
}
/**
* Provides direct access to the SortedDataInterface. This should not be used to insert
* documents into an index, except for testing purposes.

View File

@ -38,8 +38,12 @@
#include "mongo/db/index/expression_params.h"
#include "mongo/db/index/s2_key_generator.h"
#include "mongo/db/index_names.h"
#include "mongo/db/record_id.h"
#include "mongo/db/shard_role/shard_catalog/index_catalog_entry.h"
#include "mongo/db/shard_role/shard_catalog/index_descriptor.h"
#include "mongo/db/shard_role/transaction_resources.h"
#include "mongo/db/storage/key_string/key_string.h"
#include "mongo/db/version_context.h"
#include "mongo/logv2/log.h"
#include "mongo/util/assert_util.h"
#include "mongo/util/str.h"
@ -97,11 +101,25 @@ S2AccessMethod::S2AccessMethod(IndexCatalogEntry* btreeState,
}
}
std::string formatAllowedVersions(const std::set<long long>& allowedVersions) {
std::string result;
bool first = true;
for (long long version : allowedVersions) {
if (!first) {
result += ",";
}
result += std::to_string(version);
first = false;
}
return result;
}
StatusWith<BSONObj> cannotCreateIndexStatus(BSONElement indexVersionElt,
const std::string& message,
const std::string& expectedVersions = str::stream()
<< S2_INDEX_VERSION_1 << "," << S2_INDEX_VERSION_2
<< "," << S2_INDEX_VERSION_3,
<< "," << S2_INDEX_VERSION_3 << ","
<< S2_INDEX_VERSION_4,
const std::string& extraMessage = "") {
return {ErrorCodes::CannotCreateIndex,
str::stream() << message << " { " << kIndexVersionFieldName << " : " << indexVersionElt
@ -109,15 +127,23 @@ StatusWith<BSONObj> cannotCreateIndexStatus(BSONElement indexVersionElt,
<< extraMessage};
}
StatusWith<BSONObj> S2AccessMethod::_fixSpecHelper(const BSONObj& specObj,
boost::optional<long long> expectedVersion) {
// If the spec object doesn't have field "2dsphereIndexVersion", add {2dsphereIndexVersion: 3},
// which is the default for newly-built indexes.
StatusWith<BSONObj> S2AccessMethod::_fixSpecHelper(
const BSONObj& specObj, boost::optional<std::set<long long>> allowedVersions) {
// If the spec object doesn't have field "2dsphereIndexVersion", add the default version
// based on the feature flag.
BSONElement indexVersionElt = specObj[kIndexVersionFieldName];
long long defaultVersion = static_cast<long long>(index2dsphere::getDefaultS2IndexVersion());
if (indexVersionElt.eoo()) {
// Validate the default version against allowed versions if provided.
if (allowedVersions && !allowedVersions->contains(defaultVersion)) {
return {ErrorCodes::CannotCreateIndex,
str::stream() << "Default geo index version " << defaultVersion
<< " is not in the allowed set: ["
<< formatAllowedVersions(*allowedVersions) << "]"};
}
BSONObjBuilder bob;
bob.appendElements(specObj);
bob.append(kIndexVersionFieldName, S2_INDEX_VERSION_3);
bob.append(kIndexVersionFieldName, defaultVersion);
return bob.obj();
}
@ -131,23 +157,30 @@ StatusWith<BSONObj> S2AccessMethod::_fixSpecHelper(const BSONObj& specObj,
return cannotCreateIndexStatus(indexVersionElt, "Invalid value for geo index version");
}
const auto indexVersion = indexVersionElt.safeNumberLong();
long long indexVersion = indexVersionElt.safeNumberLong();
if (expectedVersion) {
// If we have an expectedVersion, we must be in timeseries.
if (indexVersion != *expectedVersion) {
return cannotCreateIndexStatus(indexVersionElt,
"unsupported geo index version",
std::to_string(*expectedVersion),
" for timeseries");
}
if (allowedVersions && !allowedVersions->contains(indexVersion)) {
// If we have allowedVersions, validate that the index version is in the set.
return cannotCreateIndexStatus(indexVersionElt,
"unsupported geo index version",
formatAllowedVersions(*allowedVersions),
"");
} else {
// Index version must be either 1, 2 or 3.
// Index version must be either 1, 2, 3, or 4.
switch (indexVersion) {
case S2_INDEX_VERSION_1:
case S2_INDEX_VERSION_2:
case S2_INDEX_VERSION_3:
break;
case S2_INDEX_VERSION_4: {
// Gate version 4 behind feature flag.
if (index2dsphere::getDefaultS2IndexVersion() != S2_INDEX_VERSION_4) {
return Status(ErrorCodes::CannotCreateIndex,
"2dsphereIndexVersion 4 requires feature flag "
"'featureFlag2dsphereIndexVersion4' to be enabled");
}
break;
}
default:
return cannotCreateIndexStatus(indexVersionElt, "unsupported geo index version");
}
@ -160,6 +193,104 @@ StatusWith<BSONObj> S2AccessMethod::fixSpec(const BSONObj& specObj) {
return S2AccessMethod::_fixSpecHelper(specObj);
}
// static
KeyStringSet S2AccessMethod::generateKeysForValidation(const BSONObj& indexSpec,
const CollatorInterface* collator,
const BSONObj& document,
Ordering ordering,
const boost::optional<RecordId>& recordId,
key_string::Version keyStringVersion) {
S2IndexingParams params;
index2dsphere::initialize2dsphereParams(indexSpec, collator, &params);
// Force version 4 for validation comparison
params.indexVersion = S2_INDEX_VERSION_4;
SharedBufferFragmentBuilder pool(key_string::HeapBuilder::kHeapAllocatorDefaultBytes);
KeyStringSet keys;
MultikeyPaths multikeyPaths;
BSONObj keyPattern = indexSpec.getObjectField("key");
index2dsphere::getS2Keys(pool,
document,
keyPattern,
params,
&keys,
&multikeyPaths,
keyStringVersion,
SortedDataIndexAccessMethod::GetKeysContext::kAddingKeys,
ordering,
recordId);
return keys;
}
// static
bool S2AccessMethod::isVersion3(const BSONObj& indexSpec) {
BSONElement versionElt = indexSpec["2dsphereIndexVersion"];
return versionElt.isNumber() && versionElt.numberInt() == S2_INDEX_VERSION_3;
}
bool S2AccessMethod::shouldCheckMissingIndexEntryAlternative(OperationContext* opCtx,
const IndexCatalogEntry& entry) const {
// Only proceed with expensive record lookup for version 3 indexes, which may need
// to be checked for version 4 upgrade scenarios.
return isVersion3(entry.descriptor()->infoObj());
}
boost::optional<std::pair<std::string, std::string>>
S2AccessMethod::checkMissingIndexEntryAlternative(OperationContext* opCtx,
const IndexCatalogEntry& entry,
const key_string::Value& missingKey,
const RecordId& recordId,
const BSONObj& document) const {
// Only check for version 3 to version 4 upgrade scenarios.
if (!isVersion3(entry.descriptor()->infoObj())) {
return boost::none;
}
try {
// Generate version 4 keys for this document.
KeyStringSet keysV4 =
generateKeysForValidation(entry.descriptor()->infoObj(),
entry.getCollator(),
document,
getSortedDataInterface()->getOrdering(),
recordId,
getSortedDataInterface()->getKeyStringVersion());
// Check if version 4 keys exist in the index. If they do, this indicates the
// validation failure was caused by SERVER-84794 and the index should be
// upgraded to v4.
if (!keysV4.empty()) {
auto sortedDataInterface = getSortedDataInterface();
auto& ru = *shard_role_details::getRecoveryUnit(opCtx);
auto cursor = sortedDataInterface->newCursor(opCtx, ru);
bool foundMatchingKey =
std::any_of(keysV4.begin(), keysV4.end(), [&](const auto& keyV4) {
// seekForKeyString checks if the key exists in the index and
// returns the KeyStringEntry with the RecordId if found.
auto ksEntry = cursor->seekForKeyString(ru, keyV4.getView());
return ksEntry && ksEntry->loc == recordId;
});
if (foundMatchingKey) {
const std::string indexName = entry.descriptor()->indexName();
std::string errorMsg = "Index '" + indexName +
"' was created with 2dsphereIndexVersion 3, but validation indicates it should "
"be "
"rebuilt with version 4. Please drop and recreate this index with version 4.";
std::string warningMsg =
"Index '" + indexName + "' needs to be rebuilt with 2dsphereIndexVersion 4.";
return std::make_pair(errorMsg, warningMsg);
}
}
} catch (...) {
// If key generation fails with version 4, continue with normal error reporting.
}
return boost::none;
}
void S2AccessMethod::validateDocument(const CollectionPtr& collection,
const BSONObj& obj,
const BSONObj& keyPattern) const {

View File

@ -46,6 +46,7 @@
#include "mongo/util/shared_buffer_fragment.h"
#include <memory>
#include <set>
#include <boost/optional/optional.hpp>
@ -60,12 +61,13 @@ public:
/**
* Helper for 'fixSpec' which validates the index and returns a copy tweaked to conform to the
* expected format. If expectedVersion is specified, the index version must match.
* expected format. If allowedVersions is specified, the index version (or default version if
* not specified) must be in the allowed set.
*
* Returns a non-OK status if 'specObj' is invalid.
*/
static StatusWith<BSONObj> _fixSpecHelper(
const BSONObj& specObj, boost::optional<long long> expectedVersion = boost::none);
const BSONObj& specObj, boost::optional<std::set<long long>> allowedVersions = boost::none);
/**
* Takes an index spec object for this index and returns a copy tweaked to conform to the
@ -77,6 +79,39 @@ public:
*/
static StatusWith<BSONObj> fixSpec(const BSONObj& specObj);
/**
* Public API for generating S2 index keys for validation purposes.
*/
static KeyStringSet generateKeysForValidation(const BSONObj& indexSpec,
const CollatorInterface* collator,
const BSONObj& document,
Ordering ordering,
const boost::optional<RecordId>& recordId,
key_string::Version keyStringVersion);
/**
* Public API for checking if an S2 index is version 3.
*/
static bool isVersion3(const BSONObj& indexSpec);
/**
* For S2 indexes, this only returns true for version 3 indexes that may need to be checked for
* version 4 upgrade scenarios.
*/
bool shouldCheckMissingIndexEntryAlternative(OperationContext* opCtx,
const IndexCatalogEntry& entry) const override;
/**
* Checks if a missing index entry is actually present when using version 4 key generation,
* indicating the index needs to be upgraded from version 3 to version 4.
*/
boost::optional<std::pair<std::string, std::string>> checkMissingIndexEntryAlternative(
OperationContext* opCtx,
const IndexCatalogEntry& entry,
const key_string::Value& missingKey,
const RecordId& recordId,
const BSONObj& document) const override;
protected:
S2AccessMethod(IndexCatalogEntry* btreeState,
std::unique_ptr<SortedDataInterface> btree,

View File

@ -29,7 +29,6 @@
#pragma once
#include "mongo/base/status.h"
#include "mongo/base/status_with.h"
#include "mongo/bson/bsonobj.h"
#include "mongo/db/index/s2_access_method.h"
@ -38,6 +37,8 @@
#include "mongo/db/storage/sorted_data_interface.h"
#include "mongo/util/modules.h"
#include <set>
namespace mongo {
// Public: instantiated in index_access_method.cpp (index_builds module) and fixSpec() called from
@ -56,7 +57,8 @@ public:
* Returns a non-OK status if 'specObj' is invalid.
*/
static StatusWith<BSONObj> fixSpec(const BSONObj& specObj) {
return S2AccessMethod::_fixSpecHelper(specObj, /*expectedVersion*/ S2_INDEX_VERSION_3);
std::set<long long> allowedVersions = {S2_INDEX_VERSION_4, S2_INDEX_VERSION_3};
return S2AccessMethod::_fixSpecHelper(specObj, allowedVersions);
}
};

View File

@ -41,6 +41,8 @@
#include "mongo/db/geo/geoconstants.h"
#include "mongo/db/geo/geometry_container.h"
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/query/query_feature_flags_gen.h"
#include "mongo/db/server_options.h"
#include <memory>
#include <ostream>
@ -233,9 +235,17 @@ void initialize2dsphereParams(const BSONObj& infoObj,
str::stream() << "unsupported geo index version { " << kIndexVersionFieldName << " : "
<< out->indexVersion << " }, only support versions: ["
<< S2_INDEX_VERSION_1 << "," << S2_INDEX_VERSION_2 << ","
<< S2_INDEX_VERSION_3 << "]",
out->indexVersion == S2_INDEX_VERSION_3 || out->indexVersion == S2_INDEX_VERSION_2 ||
out->indexVersion == S2_INDEX_VERSION_1);
<< S2_INDEX_VERSION_3 << "," << S2_INDEX_VERSION_4 << "]",
out->indexVersion == S2_INDEX_VERSION_4 || out->indexVersion == S2_INDEX_VERSION_3 ||
out->indexVersion == S2_INDEX_VERSION_2 || out->indexVersion == S2_INDEX_VERSION_1);
}
S2IndexVersion getDefaultS2IndexVersion(const VersionContext& versionContext) {
const auto fcvSnapshot = serverGlobalParams.featureCompatibility.acquireFCVSnapshot();
return feature_flags::gFeatureFlag2dsphereIndexVersion4.isEnabledUseLatestFCVWhenUninitialized(
versionContext, fcvSnapshot)
? S2_INDEX_VERSION_4
: S2_INDEX_VERSION_3;
}
} // namespace index2dsphere
} // namespace mongo

View File

@ -33,6 +33,7 @@
#include "mongo/bson/ordering.h"
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/storage/key_string/key_string.h"
#include "mongo/db/version_context.h"
#include "mongo/util/modules.h"
#include <cstddef>
@ -59,7 +60,11 @@ enum S2IndexVersion {
// The third version of the S2 index, introduced in MongoDB 3.2.0. Introduced
// performance improvements and changed the key type from string to numeric
S2_INDEX_VERSION_3 = 3
S2_INDEX_VERSION_3 = 3,
// The fourth version of the S2 index. Changed parsing order for object-type
// geometry elements to try GeoJSON parsing before legacy point parsing.
S2_INDEX_VERSION_4 = 4
};
struct S2IndexingParams {
@ -100,5 +105,16 @@ void S2CellIdToIndexKeyStringAppend(const S2CellId& cellId,
void initialize2dsphereParams(const BSONObj& infoObj,
const CollatorInterface* collator,
S2IndexingParams* out);
/**
* Returns the default S2 index version based on the feature flag.
* If the feature flag for version 4 is enabled, returns S2_INDEX_VERSION_4,
* otherwise returns S2_INDEX_VERSION_3.
*
* @param versionContext The version context to use for feature flag checks.
* Defaults to kVersionContextIgnored_UNSAFE.
*/
S2IndexVersion getDefaultS2IndexVersion(
const VersionContext& versionContext = kVersionContextIgnored_UNSAFE);
} // namespace index2dsphere
} // namespace mongo

View File

@ -64,7 +64,7 @@ Status S2GetKeysForElement(const BSONElement& element,
const S2IndexingParams& params,
std::vector<S2CellId>* out) {
GeometryContainer geoContainer;
Status status = geoContainer.parseFromStorage(element);
Status status = geoContainer.parseFromStorage(element, false, params.indexVersion);
if (!status.isOK())
return status;
@ -224,7 +224,7 @@ bool getS2BucketGeoKeys(const BSONObj& document,
BSONArrayBuilder coordinates(shape.subarrayStart("coordinates"));
for (BSONElementSet::iterator i = elements.begin(); i != elements.end(); ++i) {
GeometryContainer container;
auto status = container.parseFromStorage(*i, false);
auto status = container.parseFromStorage(*i, false, params.indexVersion);
uassert(183934,
str::stream() << "Can't extract geo keys: " << status.reason(),
status.isOK());

View File

@ -382,6 +382,13 @@ feature_flags:
default: false
fcv_gated: false
featureFlag2dsphereIndexVersion4:
description: "Feature flag to enable v4 of 2dsphereIndexVersion."
cpp_varname: gFeatureFlag2dsphereIndexVersion4
default: true
fcv_gated: true
version: 8.3
featureFlagCostBasedRanker:
description: "Feature flag to enable the cost-based ranker for query plans."
cpp_varname: gFeatureFlagCostBasedRanker

View File

@ -245,14 +245,43 @@ void KeyStringIndexConsistency::addIndexEntryErrors(OperationContext* opCtx,
// Inform which indexes have inconsistencies and add the BSON objects of the inconsistent index
// entries to the results vector.
int numMissingIndexEntryErrors = _missingIndexEntries.size();
// Track which indexes should get custom error messages from their access methods.
std::map<std::string, std::pair<std::string, std::string>> indexesWithCustomErrors;
for (const auto& [missingIndexKey, missingRecordId] : _missingIndexEntries) {
const IndexInfo& info = *missingIndexKey.first;
const std::string& indexName = info.indexName;
// Check if the access method has an alternative explanation for this missing entry.
const auto entry =
_validateState->getCollection()->getIndexCatalog()->findIndexByName(opCtx, indexName);
if (entry) {
auto accessMethod = entry->accessMethod()->asSortedData();
if (accessMethod) {
if (accessMethod->shouldCheckMissingIndexEntryAlternative(opCtx, *entry)) {
auto record = _validateState->getSeekRecordStoreCursor()->seekExact(
opCtx, missingRecordId);
if (record) {
BSONObj document = record->data.toBson();
auto alternative = accessMethod->checkMissingIndexEntryAlternative(
opCtx, *entry, missingIndexKey.second, missingRecordId, document);
if (alternative) {
indexesWithCustomErrors[indexName] = *alternative;
continue; // Skip normal error reporting for this entry.
}
}
}
}
}
// Mark the error as an inconsistency.
_foundInconsistency(opCtx,
missingIndexKey,
missingRecordId,
*results,
/*isMissing=*/true);
const std::string& indexName = missingIndexKey.first->indexName;
if (!results->getIndexResultsMap().at(indexName).isValid()) {
continue;
}
@ -261,6 +290,12 @@ void KeyStringIndexConsistency::addIndexEntryErrors(OperationContext* opCtx,
results->getIndexResultsMap().at(indexName).addError(ss.str(), false);
}
// Add custom error messages for indexes that need upgrades.
for (const auto& [indexName, messages] : indexesWithCustomErrors) {
results->getIndexResultsMap().at(indexName).addError(messages.first, false);
results->addWarning(messages.second);
}
int numExtraIndexEntryErrors = 0;
for (const auto& item : _extraIndexEntries) {
numExtraIndexEntryErrors += item.second.size();

View File

@ -41,6 +41,7 @@
#include "mongo/db/dbhelpers.h"
#include "mongo/db/index/index_access_method.h"
#include "mongo/db/index/multikey_paths.h"
#include "mongo/db/index/s2_common.h"
#include "mongo/db/index_builds/index_build_interceptor.h"
#include "mongo/db/index_builds/index_build_test_helpers.h"
#include "mongo/db/index_builds/index_builds_common.h"
@ -4230,6 +4231,133 @@ public:
}
};
// Test that validates an S2 index created with v4 code but persisted as v3 gets reported as
// needing upgrade to v4.
class ValidateS2IndexVersion3NeedsUpgrade : public ValidateBase {
public:
ValidateS2IndexVersion3NeedsUpgrade() : ValidateBase(/*full=*/false, /*background=*/false) {}
void run() {
// Create a new collection and insert a document with a GeoJSON Point.
lockDb(MODE_X);
{
beginTransaction();
ASSERT_OK(_db->dropCollection(&_opCtx, _nss));
_db->createCollection(&_opCtx, _nss);
commitTransaction();
}
// Insert the document with location field.
BSONObj originalDoc = BSON(
"_id" << 1 << "location"
<< BSON("latitude" << 38.7348661 << "longitude" << -9.1447487 << "coordinates"
<< BSON_ARRAY(-9.1447487 << 38.7348661) << "type"
<< "Point"));
{
beginTransaction();
insertDocument(originalDoc);
commitTransaction();
}
// Create a 2dsphere index. This will default to v4 and generate v4 keys.
const auto indexName = "location_2dsphere";
auto status =
createIndexFromSpec(BSON("name" << indexName << "key" << BSON("location" << "2dsphere")
<< "v" << static_cast<int>(kIndexVersion)));
ASSERT_OK(status);
// Now modify the durable catalog to change the index version from v4 to v3.
// This simulates an index that was created with v4 code but persisted as v3.
CollectionWriter writer(&_opCtx, coll()->ns());
{
beginTransaction();
auto collMetadata = durable_catalog::getParsedCatalogEntry(
&_opCtx, coll()->getCatalogId(), MDBCatalog::get(&_opCtx))
->metadata;
int offset = collMetadata->findIndexOffset(indexName);
ASSERT_GTE(offset, 0);
auto& indexMetadata = collMetadata->indexes[offset];
// Modify the spec to change version from v4 to v3.
BSONObjBuilder specBuilder;
for (BSONObjIterator bi(indexMetadata.spec); bi.more();) {
BSONElement e = bi.next();
if (e.fieldNameStringData() == "2dsphereIndexVersion") {
specBuilder.append("2dsphereIndexVersion", S2_INDEX_VERSION_3);
} else {
specBuilder.append(e);
}
}
indexMetadata.spec = specBuilder.obj();
writer.getWritableCollection(&_opCtx)->replaceMetadata(&_opCtx,
std::move(collMetadata));
commitTransaction();
}
// Reload the index from the modified catalog.
{
beginTransaction();
auto writableCatalog = writer.getWritableCollection(&_opCtx)->getIndexCatalog();
auto entry = writableCatalog->findIndexByName(&_opCtx, indexName);
writableCatalog->refreshEntry(&_opCtx,
writer.getWritableCollection(&_opCtx),
entry,
CreateIndexEntryFlags::kIsReady);
commitTransaction();
}
releaseDb();
// Run validate and check that it reports the index needs to be upgraded to v4.
{
ValidateResults results;
ASSERT_OK(CollectionValidation::validate(
&_opCtx,
_nss,
ValidationOptions{CollectionValidation::ValidateMode::kForeground,
CollectionValidation::RepairMode::kNone,
kLogDiagnostics},
&results));
ScopeGuard dumpOnErrorGuard([&] {
StorageDebugUtil::printValidateResults(results);
StorageDebugUtil::printCollectionAndIndexTableEntries(&_opCtx, coll()->ns());
});
// Validation should fail because the index has v4 keys but is marked as v3.
ASSERT_FALSE(results.isValid());
// Check that the index results contain the upgrade message.
auto indexResultsIt = results.getIndexResultsMap().find(indexName);
ASSERT(indexResultsIt != results.getIndexResultsMap().end());
const auto& indexResults = indexResultsIt->second;
// Check that there's an error and warning message about needing to upgrade to v4.
bool foundError =
std::any_of(indexResults.getErrors().begin(),
indexResults.getErrors().end(),
[](const auto& error) {
return error.find("2dsphereIndexVersion 3") != std::string::npos &&
error.find("version 4") != std::string::npos;
});
ASSERT_TRUE(foundError) << "Expected error message about upgrading to v4";
bool foundWarning =
std::any_of(results.getWarnings().begin(),
results.getWarnings().end(),
[&indexName](const auto& warning) {
return warning.find(indexName) != std::string::npos &&
warning.find("2dsphereIndexVersion 4") != std::string::npos;
});
ASSERT_TRUE(foundWarning) << "Expected warning about rebuilding with v4";
dumpOnErrorGuard.dismiss();
}
}
};
class ValidateInvalidBSONOnClusteredCollection : public ValidateBase {
public:
explicit ValidateInvalidBSONOnClusteredCollection(bool background)
@ -4895,6 +5023,9 @@ public:
add<ValidateAddNewMultikeyPaths>();
// Test that validates S2 index version upgrade detection.
add<ValidateS2IndexVersion3NeedsUpgrade>();
// Tests that validation works on clustered collections.
add<ValidateInvalidBSONOnClusteredCollection>(false);
add<ValidateInvalidBSONOnClusteredCollection>(true);