/** * Test that 'planCacheShapeHash' and 'planCacheKey' from explain() output have sensible values * across catalog changes. * @tags: [ * assumes_read_concern_local, * # This test expects query shapes and plans to stay the same at the beginning and * # at the end of test run. That's just wrong expectation when chunks are moving * # randomly across shards. * assumes_balancer_off, * requires_fcv_51, * # There are no guarantees about how 'planCacheShapeHash' is computed between different versions * # of binaries. Therefore, this test can fail if a stepdown occurs between the two explains, * # as it compares two different 'planCacheShapeHash' values. * multiversion_incompatible, * # The test expects the plan cache key on a given node to remain stable. However, the plan * # cache key is allowed to change between versions. Therefore, this test cannot run in * # passthroughs that do upgrade/downgrade. * cannot_run_during_upgrade_downgrade, * # This test expects stable query plans, creating unanticipated indexes can lead to variations * # in the plans. * assumes_no_implicit_index_creation, * ] */ import {assertDropAndRecreateCollection} from "jstests/libs/collection_drop_recreate.js"; import { getAllNodeExplains, getPlanCacheKeyFromExplain, getPlanCacheShapeHashFromExplain } from "jstests/libs/query/analyze_plan.js"; import {checkSbeFullFeatureFlagEnabled} from "jstests/libs/query/sbe_util.js"; function groupBy(arr, keyFn) { let dict = {}; for (const elem of arr) { const key = keyFn(elem); if (!dict.hasOwnProperty(key)) { dict[key] = []; } dict[key].push(elem); } return dict; } // SERVER-56980: When running in a sharded environment, we group the explains by shard. This is // because in a multi-version environment, we want to ensure that we are comparing the results // produced by the same shard in the event that the 'planCacheKey' format changed in between // versions. function runTest({explain0, explain1, assertionFn}) { const groupedExplains = groupBy([...getAllNodeExplains(explain0), ...getAllNodeExplains(explain1)], /* keyFn */ (explain) => explain.shardName); for (const group of Object.values(groupedExplains)) { assert.eq(group.length, 2); const [nodeExplain0, nodeExplain1] = group; assertionFn(nodeExplain0, nodeExplain1); } } const coll = assertDropAndRecreateCollection(db, "plan_cache_shape_hash_stability"); assert.commandWorked(coll.insert({x: 5})); const query = { x: 3 }; const initialExplain = coll.find(query).explain(); // Add a sparse index. assert.commandWorked(coll.createIndex({x: 1}, {sparse: true})); const withIndexExplain = coll.find(query).explain(); runTest({ explain0: initialExplain, explain1: withIndexExplain, assertionFn: (nodeExplain0, nodeExplain1) => { assert.eq(getPlanCacheShapeHashFromExplain(nodeExplain0), getPlanCacheShapeHashFromExplain(nodeExplain1), "'planCacheShapeHash' shouldn't change accross catalog changes"); // We added an index so the plan cache key changed. assert.neq(getPlanCacheKeyFromExplain(nodeExplain0), getPlanCacheKeyFromExplain(nodeExplain1), "'planCacheKey' should change accross catalog changes"); } }); // Drop the index. assert.commandWorked(coll.dropIndex({x: 1})); const postDropExplain = coll.find(query).explain(); const usesSbePlanCache = checkSbeFullFeatureFlagEnabled(db); runTest({ explain0: initialExplain, explain1: postDropExplain, assertionFn: (nodeExplain0, nodeExplain1) => { assert.eq(getPlanCacheShapeHashFromExplain(nodeExplain0), getPlanCacheShapeHashFromExplain(nodeExplain1), "'planCacheShapeHash' shouldn't change accross catalog changes"); if (usesSbePlanCache) { // SBE's 'planCacheKey' encoding encodes "collection version" which will be increased // after dropping an index. assert.neq(getPlanCacheKeyFromExplain(nodeExplain0), getPlanCacheKeyFromExplain(nodeExplain1), "'planCacheKey' should change accross catalog changes"); } else { // The 'planCacheKey' should be the same as what it was before we dropped the index. assert.eq(getPlanCacheKeyFromExplain(nodeExplain0), getPlanCacheKeyFromExplain(nodeExplain1)); } } });