mongo/jstests/libs/override_methods/implicit_wildcard_indexes.js

178 lines
7.5 KiB
JavaScript

/**
* This file defines command overrides in order to implcitly create additional wildcard indexes for
* tests running in this suite.
*/
import {OverrideHelpers} from "jstests/libs/override_methods/override_helpers.js";
const prefix = "HIDDEN_WILDCARD_";
const isHiddenWildcardIndex = {
$regexMatch: {input: "$name", regex: prefix},
};
TestData.implicitWildcardIndexesEnabled = true;
TestData.hideImplicitlyCreatedIndexesFromListIndexes = true;
/**
* Error codes we ignore when trying to create implicit indexes because they depend on the test
* structure itself.
*/
const acceptableErrorCodes = [
// Wildcard prefix overlap.
7246204, 7246200,
// Non-wildcard field in compound index can't be multikey.
7246301,
// Index already exists with a different name.
85,
// Too many compound keys.
13103,
// Trying to create a wildcard index on a timeseries collection results in a duplicate wildcard
// key error or invalid wildcardProjection.
7246201, 9,
];
/**
* Substitute the field 'subFieldname' with a wildcard field & generate appropriate exclusion
* projections for the other fields in the CWI as needed.
*/
function transformToWildcard(key, subFieldname) {
const subFieldElems = subFieldname.split(".");
subFieldElems.pop();
subFieldElems.push("");
const wildcardPrefix = subFieldElems.join(".");
let outKey = {};
let exclusionProjection = {};
let hasWildcard = false;
for (const [field, dir] of Object.entries(key)) {
if (field === subFieldname && !hasWildcard) {
outKey[wildcardPrefix + "$**"] = dir;
hasWildcard = true;
} else {
outKey[field] = dir;
if (field.startsWith(wildcardPrefix)) {
exclusionProjection[field] = 0;
}
}
}
return [outKey, exclusionProjection];
}
/**
* If the keyPattern does not correspond to a wildcard index already, generate one
* wildcard index per entry in the keyPattern. For example, the following indexes would
* result in the following CWIs:
* - {a.b.c: 1} ->
* 1. [{a.b.$**: 1}, no exclusion]
* - {a: -1, b: 1, c.d: 1} ->
* 1. [{$**: -1, b: 1, c.d: 1}, {b: 0, c.d: 0}],
* 2. [{a: -1, $**: 1, c.d: 1}, {a: 0, c.d: 0}],
* 3. [{a: -1, b: 1, c.$**: 1}, no exclusion]
*/
function transformIntoWildcardIndexes(keyPattern) {
let out = [];
for (const [field, value] of Object.entries(keyPattern)) {
if (field.endsWith("$**") || typeof value === "string") {
// This is either already a wildcard index, or would be an invalid wildcard index, so
// there's no point in implicitly creating any new indexes from it.
return [];
}
out.push(transformToWildcard(keyPattern, field).concat([prefix + out.length + "_"]));
}
return out;
}
/**
* Returns a list of indexes implicitly created by this suite for the current test (up to now).
*/
function getHiddenIndexes(dbName, collName) {
return db
.getSiblingDB(dbName)
[collName].aggregate([
{$indexStats: {}},
{$addFields: {isHidden: isHiddenWildcardIndex}},
{$match: {isHidden: true}},
])
.toArray();
}
/**
* Execute a call to 'runCommand', then perform the following additional actions:
* 1. createIndexes: we will try to transform the indexes passed into the command into additional
* (hidden!) wildcard indexes and create them as well.
* 2. listIndexes: we will filter out any implicitly created wildcard indexes in order to avoid
* failing in tests that expect only the idnexes they explicitly created.
* 3. insert: we may have successfully created an implicit compound wildcard index on an earlier
* index creation call, only to fail to insert a document in the collection with an array value on
* one of the non-wildcard components in the index. In this case, we will drop all implicitly
* created indexes.
*/
function runCommandOverride(conn, dbName, cmdName, cmdObj, originalRunCommand, makeRunCommandArgs) {
const collName = cmdObj[cmdName];
// Actually run the provided command.
let res = originalRunCommand.apply(conn, makeRunCommandArgs(cmdObj));
if (cmdName == "createIndexes" && res.ok) {
// We successfully created one or more indexes, so now we can try to convert them into some
// number of wildcard indexes.
const indexes = cmdObj.indexes;
for (const {key, name} of indexes) {
// Attempt to convert the original index passed to 'createIndexes' to one
// or more wildcard indexes. note that we attempt to create one at a time, since some of
// these index creations may fail with an error code we can ignore.
const wildIndexes = transformIntoWildcardIndexes(key);
for (const wildIndex of wildIndexes) {
const [newKey, exclusionProjection, newName] = wildIndex;
const index = {key: newKey, name: newName + name};
if (Object.keys(exclusionProjection).length > 0) {
index["wildcardProjection"] = exclusionProjection;
}
// Note: if we try to run 'createIndexes' directly here, we will
// re-enter the override method.
const wildRes = originalRunCommand.apply(
conn,
makeRunCommandArgs({createIndexes: collName, indexes: [index]}),
);
if (wildRes.ok) {
jsTest.log.info("Created implicit wildcard index", {index});
} else if (!wildRes.ok && !new Set(acceptableErrorCodes).has(wildRes.code)) {
jsTest.log.info("Unexpected error code: ", wildRes.code, wildRes.errmsg);
}
assert.commandWorkedOrFailedWithCode(wildRes, acceptableErrorCodes);
}
}
} else if (cmdName == "listIndexes" && res.ok) {
// If we're listing indexes, we need to make sure we eliminate any of the
// implicitly created indexes from consideration.
if (TestData.hideImplicitlyCreatedIndexesFromListIndexes && res.cursor.firstBatch) {
res.cursor.firstBatch = res.cursor.firstBatch.filter((idx) => !idx.name.startsWith(prefix));
}
} else if (cmdName == "insert" && res.ok) {
const writeErrors = res.writeErrors ? res.writeErrors.filter((err) => err.code === 7246301) : [];
if (writeErrors.length > 0) {
// We are trying to make a non-wildcard component of the index multikey.
// Drop all the implicitly created indexes and try again, since we can't be
// sure which index was problematic.
jsTest.log.info("Trying to make a non-wildcard component of the index multikey!");
// We must successfully drop our hidden indexes, otherwise we cannot proceed with the
// test. We're not sure which index was problematic (or if multiple indexes might be
// problematic) so we just drop them all.
const hiddenIndexes = getHiddenIndexes(dbName, collName).map((idx) => idx.name);
assert.commandWorked(
originalRunCommand.apply(conn, makeRunCommandArgs({dropIndexes: collName, index: hiddenIndexes})),
);
// Now run the original command again.
res = originalRunCommand.apply(conn, makeRunCommandArgs(cmdObj));
}
}
return res;
}
OverrideHelpers.prependOverrideInParallelShell("jstests/libs/override_methods/implicit_wildcard_indexes.js");
OverrideHelpers.overrideRunCommand(runCommandOverride);