diff options
-rw-r--r-- | jstests/core/query/partial_index_logical.js | 180 | ||||
-rw-r--r-- | jstests/libs/analyze_plan.js | 15 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_algo_test.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/query/canonical_query_encoder.cpp | 26 | ||||
-rw-r--r-- | src/mongo/db/query/canonical_query_encoder.h | 32 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_indexability.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_indexability.h | 32 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_indexability_test.cpp | 148 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_key_factory.cpp | 33 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_key_info_test.cpp | 133 |
10 files changed, 494 insertions, 126 deletions
diff --git a/jstests/core/query/partial_index_logical.js b/jstests/core/query/partial_index_logical.js new file mode 100644 index 00000000000..145eb206dd7 --- /dev/null +++ b/jstests/core/query/partial_index_logical.js @@ -0,0 +1,180 @@ +/** + * Test the planners ability to distinguish parameterized queries in the presence of a partial index + * containing logical expressions ($and, $or). + * + * @tags: [ + * # TODO SERVER-67607: Test plan cache with CQF enabled. + * cqf_incompatible, + * # Since the plan cache is per-node state, this test assumes that all operations are happening + * # against the same mongod. + * assumes_read_preference_unchanged, + * assumes_read_concern_unchanged, + * does_not_support_stepdowns, + * # If all chunks are moved off of a shard, it can cause the plan cache to miss commands. + * assumes_balancer_off, + * assumes_unsharded_collection, + * requires_fcv_63, + * # Plan cache state is node-local and will not get migrated alongside tenant data. + * tenant_migration_incompatible, + * ] + */ +(function() { +"use strict"; + +load("jstests/libs/analyze_plan.js"); // For getPlanCacheKeyFromShape. + +(function partialIndexMixedFields() { + db.test.drop(); + + // Create enough competing indexes such that a query is eligible for caching (single plan + // queries are not cached). + assert.commandWorked( + db.test.createIndex({num: 1}, {partialFilterExpression: {num: 5, foo: 6}})); + assert.commandWorked(db.test.createIndex({num: -1})); + assert.commandWorked(db.test.createIndex({num: -1, not_num: 1})); + + assert.commandWorked(db.test.insert([ + {_id: 0, num: 5, foo: 6}, + {_id: 1, num: 5, foo: 7}, + ])); + + // Run a query which is eligible to use the {num: 1} index as it is covered by the partial + // filter expression. + assert.eq(db.test.find({num: 5, foo: 6}).itcount(), 1); + assert.eq(db.test.find({num: 5, foo: 6}).itcount(), 1); + const matchingKey = + getPlanCacheKeyFromShape({query: {num: 5, foo: 6}, collection: db.test, db: db}); + assert.eq(1, + db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: matchingKey}}]) + .itcount()); + + // This query should not be eligible for the {num: 1} index despite the path 'num' being + // compatible (per the plan cache key encoding). + assert.eq(1, db.test.find({num: 5, foo: 7}).itcount()); + const nonCoveredKey = + getPlanCacheKeyFromShape({query: {num: 5, foo: 7}, collection: db.test, db: db}); + assert.eq(1, + db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: nonCoveredKey}}]) + .itcount()); + + // Sanity check that the generated keys are different due to the index compatibility. + assert.neq(nonCoveredKey, matchingKey); +})(); + +(function partialIndexDisjunction() { + db.test.drop(); + + // Create enough competing indexes such that a query is eligible for caching (single plan + // queries are not cached). + assert.commandWorked(db.test.createIndex( + {num: 1}, + {partialFilterExpression: {$or: [{num: {$exists: true}}, {num: {$type: 'number'}}]}})); + assert.commandWorked(db.test.createIndex({num: -1})); + assert.commandWorked(db.test.createIndex({num: -1, not_num: 1})); + + assert.commandWorked(db.test.insert([ + {_id: 0}, + {_id: 1, num: null}, + {_id: 2, num: 5}, + ])); + + // Run a query which is eligible to use the {num: 1} index as it is covered by the partial + // filter expression. + assert.eq(db.test.find({num: 5}).itcount(), 1); + assert.eq(db.test.find({num: 5}).itcount(), 1); + const numericKey = getPlanCacheKeyFromShape({query: {num: 5}, collection: db.test, db: db}); + assert.eq( + 1, + db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: numericKey}}]).itcount()); + + // The plan for the query above should now be in the cache and active. Now execute a query with + // a very similar shape, however the predicate parameters are not satisfied by the partial + // filter expression. This is because {num: null} should match both explicit null as well as + // missing values (the latter are not indexed). + assert.eq(2, db.test.find({num: null}).itcount()); + const nullKey = getPlanCacheKeyFromShape({query: {num: null}, collection: db.test, db: db}); + assert.eq( + 1, db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: nullKey}}]).itcount()); + + // Sanity check that the generated keys are different due to the index compatibility. + assert.neq(nullKey, numericKey); +})(); + +(function partialIndexDisjunctionWithCollation() { + db.test.drop(); + + const caseInsensitive = {locale: "en_US", strength: 2}; + + // Create enough competing indexes such that a query is eligible for caching (single plan + // queries are not cached). + assert.commandWorked(db.test.createIndex({a: 1}, { + partialFilterExpression: {$or: [{a: {$gt: 0}}, {a: {$gt: ""}}]}, + collation: caseInsensitive, + })); + assert.commandWorked(db.test.createIndex({a: -1})); + assert.commandWorked(db.test.createIndex({a: -1, b: 1})); + + assert.commandWorked(db.test.insert([ + {_id: 0, a: "some"}, + {_id: 1, a: "string"}, + ])); + + // Populate the plan cache for a query which is eligible for the partial index. This is true + // without an explicit collation because the query text does not contain any string comparisons. + assert.eq(db.test.aggregate({$match: {a: {$in: [1, 3]}}}).itcount(), 0); + assert.eq(db.test.aggregate({$match: {a: {$in: [1, 3]}}}).itcount(), 0); + const simpleCollationKey = + getPlanCacheKeyFromShape({query: {a: {$in: [1, 3]}}, collection: db.test, db: db}); + assert.eq( + 1, + db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: simpleCollationKey}}]) + .itcount()); + + // A collation-sensitive query should _not_ use the cached plan since the default simple + // collation does not match the collation on the index. + assert.eq(db.test.aggregate({$match: {a: {$in: ["a", "Some"]}}}).itcount(), 0); + assert.eq(db.test.aggregate({$match: {a: {$in: ["a", "Some"]}}}).itcount(), 0); + const collationSensitiveKey = + getPlanCacheKeyFromShape({query: {a: {$in: ["a", "Some"]}}, collection: db.test, db: db}); + assert.eq( + 1, + db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: collationSensitiveKey}}]) + .itcount()); + + // Sanity check that the generated keys are different due to the collation and index + // compatibility. + assert.neq(collationSensitiveKey, simpleCollationKey); +})(); + +(function partialIndexConjunction() { + db.test.drop(); + + // Create enough competing indexes such that a query is eligible for caching (single plan + // queries are not cached). + assert.commandWorked( + db.test.createIndex({num: 1}, {partialFilterExpression: {num: {$gt: 0, $lt: 10}}})); + assert.commandWorked(db.test.createIndex({num: -1})); + assert.commandWorked(db.test.createIndex({num: -1, not_num: 1})); + + assert.commandWorked(db.test.insert([ + {_id: 0}, + {_id: 1, num: 1}, + {_id: 2, num: 11}, + ])); + + // Run a query which is eligible to use the {num: 1} index as it is covered by the partial + // filter expression. + assert.eq(db.test.find({num: {$gt: 0, $lt: 10}}).itcount(), 1); + assert.eq(db.test.find({num: {$gt: 0, $lt: 10}}).itcount(), 1); + const validKey = + getPlanCacheKeyFromShape({query: {num: {$gt: 0, $lt: 10}}, collection: db.test, db: db}); + assert.eq( + 1, + db.test.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: validKey}}]).itcount()); + + // The plan for the query above should now be in the cache and active. Now execute a query with + // a very similar shape, however the predicate parameters are not satisfied by the partial + // filter expression. + assert.eq(2, db.test.find({num: {$gt: 0, $lt: 12}}).itcount()); +})(); +})(); diff --git a/jstests/libs/analyze_plan.js b/jstests/libs/analyze_plan.js index dcfb3f11221..9ae5e62ba65 100644 --- a/jstests/libs/analyze_plan.js +++ b/jstests/libs/analyze_plan.js @@ -526,9 +526,18 @@ function getPlanCacheKeyFromExplain(explainRes, db) { * Helper to run a explain on the given query shape and get the "planCacheKey" from the explain * result. */ -function getPlanCacheKeyFromShape({query = {}, projection = {}, sort = {}, collection, db}) { - const explainRes = - assert.commandWorked(collection.explain().find(query, projection).sort(sort).finish()); +function getPlanCacheKeyFromShape({ + query = {}, + projection = {}, + sort = {}, + collation = { + locale: "simple" + }, + collection, + db +}) { + const explainRes = assert.commandWorked( + collection.explain().find(query, projection).collation(collation).sort(sort).finish()); return getPlanCacheKeyFromExplain(explainRes, db); } diff --git a/src/mongo/db/matcher/expression_algo_test.cpp b/src/mongo/db/matcher/expression_algo_test.cpp index f2ebefbb76d..3e0922bd9fc 100644 --- a/src/mongo/db/matcher/expression_algo_test.cpp +++ b/src/mongo/db/matcher/expression_algo_test.cpp @@ -181,6 +181,14 @@ TEST(ExpressionAlgoIsSubsetOf, CompareAnd_GT) { ASSERT_FALSE(expression::isSubsetOf(filter.get(), query.get())); } +TEST(ExpressionAlgoIsSubsetOf, CompareAnd_SingleField) { + ParsedMatchExpression filter("{a: {$gt: 5, $lt: 7}}"); + ParsedMatchExpression query("{a: {$gt: 5, $lt: 6}}"); + + ASSERT_TRUE(expression::isSubsetOf(query.get(), filter.get())); + ASSERT_FALSE(expression::isSubsetOf(filter.get(), query.get())); +} + TEST(ExpressionAlgoIsSubsetOf, CompareOr_LT) { ParsedMatchExpression lt5("{a: {$lt: 5}}"); ParsedMatchExpression eq2OrEq3("{$or: [{a: 2}, {a: 3}]}"); diff --git a/src/mongo/db/query/canonical_query_encoder.cpp b/src/mongo/db/query/canonical_query_encoder.cpp index 6a6938ef4b1..c8a8cd29d48 100644 --- a/src/mongo/db/query/canonical_query_encoder.cpp +++ b/src/mongo/db/query/canonical_query_encoder.cpp @@ -74,32 +74,6 @@ bool isQueryNegatingEqualToNull(const mongo::MatchExpression* tree) { namespace { -// Delimiters for cache key encoding. -const char kEncodeChildrenBegin = '['; -const char kEncodeChildrenEnd = ']'; -const char kEncodeChildrenSeparator = ','; -const char kEncodeCollationSection = '#'; -const char kEncodeProjectionSection = '|'; -const char kEncodeProjectionRequirementSeparator = '-'; -const char kEncodeRegexFlagsSeparator = '/'; -const char kEncodeSortSection = '~'; -const char kEncodeEngineSection = '@'; - -// These special bytes are used in the encoding of auto-parameterized match expressions in the SBE -// plan cache key. - -// Precedes the id number of a parameter marker. -const char kEncodeParamMarker = '?'; -// Precedes the encoding of a constant when that constant has not been auto-paramterized. The -// constant is typically encoded as a BSON type byte followed by a BSON value (without the -// BSONElement's field name). -const char kEncodeConstantLiteralMarker = ':'; -// Precedes a byte which encodes the bounds tightness associated with a predicate. The structure of -// the plan (i.e. presence of filters) is affected by bounds tightness. Therefore, if different -// parameter values can result in different tightnesses, this must be explicitly encoded into the -// plan cache key. -const char kEncodeBoundsTightnessDiscriminator = ':'; - /** * AppendChar provides the compiler with a type for a "appendChar(...)" member function. */ diff --git a/src/mongo/db/query/canonical_query_encoder.h b/src/mongo/db/query/canonical_query_encoder.h index 3164ddbec67..cf124655681 100644 --- a/src/mongo/db/query/canonical_query_encoder.h +++ b/src/mongo/db/query/canonical_query_encoder.h @@ -33,6 +33,38 @@ namespace mongo { +// Delimiters for canonical query portion of cache key encoding. +inline constexpr char kEncodeChildrenBegin = '['; +inline constexpr char kEncodeChildrenEnd = ']'; +inline constexpr char kEncodeChildrenSeparator = ','; +inline constexpr char kEncodeCollationSection = '#'; +inline constexpr char kEncodeProjectionSection = '|'; +inline constexpr char kEncodeProjectionRequirementSeparator = '-'; +inline constexpr char kEncodeRegexFlagsSeparator = '/'; +inline constexpr char kEncodeSortSection = '~'; +inline constexpr char kEncodeEngineSection = '@'; + +// These special bytes are used in the encoding of auto-parameterized match expressions in the SBE +// plan cache key. + +// Precedes the id number of a parameter marker. +inline constexpr char kEncodeParamMarker = '?'; +// Precedes the encoding of a constant when that constant has not been auto-paramterized. The +// constant is typically encoded as a BSON type byte followed by a BSON value (without the +// BSONElement's field name). +inline constexpr char kEncodeConstantLiteralMarker = ':'; +// Precedes a byte which encodes the bounds tightness associated with a predicate. The structure of +// the plan (i.e. presence of filters) is affected by bounds tightness. Therefore, if different +// parameter values can result in different tightnesses, this must be explicitly encoded into the +// plan cache key. +inline constexpr char kEncodeBoundsTightnessDiscriminator = ':'; + +// Delimiters for the discriminator portion of the cache key encoding. +inline constexpr char kEncodeDiscriminatorsBegin = '<'; +inline constexpr char kEncodeDiscriminatorsEnd = '>'; +inline constexpr char kEncodeGlobalDiscriminatorsBegin = '('; +inline constexpr char kEncodeGlobalDiscriminatorsEnd = ')'; + /** * Returns true if the query predicate involves a negation of an EQ, LTE, or GTE comparison to * 'null'. diff --git a/src/mongo/db/query/plan_cache_indexability.cpp b/src/mongo/db/query/plan_cache_indexability.cpp index 4f1b0c176f8..0498d5edc26 100644 --- a/src/mongo/db/query/plan_cache_indexability.cpp +++ b/src/mongo/db/query/plan_cache_indexability.cpp @@ -79,7 +79,6 @@ IndexabilityDiscriminator getCollatedIndexDiscriminator(const CollatorInterface* } return true; } - // The predicate never compares strings so it is not affected by collation. return true; }; @@ -104,14 +103,7 @@ void PlanCacheIndexabilityState::processSparseIndex(const std::string& indexName void PlanCacheIndexabilityState::processPartialIndex(const std::string& indexName, const MatchExpression* filterExpr) { - invariant(filterExpr); - for (size_t i = 0; i < filterExpr->numChildren(); ++i) { - processPartialIndex(indexName, filterExpr->getChild(i)); - } - if (filterExpr->getCategory() != MatchExpression::MatchCategory::kLogical) { - _pathDiscriminatorsMap[filterExpr->path()][indexName].addDiscriminator( - getPartialIndexDiscriminator(filterExpr)); - } + _globalDiscriminatorMap[indexName].addDiscriminator(getPartialIndexDiscriminator(filterExpr)); } void PlanCacheIndexabilityState::processWildcardIndex(const CoreIndexInfo& cii) { @@ -134,7 +126,7 @@ namespace { const IndexToDiscriminatorMap emptyDiscriminators{}; } // namespace -const IndexToDiscriminatorMap& PlanCacheIndexabilityState::getDiscriminators( +const IndexToDiscriminatorMap& PlanCacheIndexabilityState::getPathDiscriminators( StringData path) const { PathDiscriminatorsMap::const_iterator it = _pathDiscriminatorsMap.find(path); if (it == _pathDiscriminatorsMap.end()) { @@ -166,6 +158,7 @@ IndexToDiscriminatorMap PlanCacheIndexabilityState::buildWildcardDiscriminators( void PlanCacheIndexabilityState::updateDiscriminators( const std::vector<CoreIndexInfo>& indexCores) { _pathDiscriminatorsMap = PathDiscriminatorsMap(); + _globalDiscriminatorMap = IndexToDiscriminatorMap(); _wildcardIndexDiscriminators.clear(); for (const auto& idx : indexCores) { diff --git a/src/mongo/db/query/plan_cache_indexability.h b/src/mongo/db/query/plan_cache_indexability.h index 9bc03494865..0aa08359c27 100644 --- a/src/mongo/db/query/plan_cache_indexability.h +++ b/src/mongo/db/query/plan_cache_indexability.h @@ -47,6 +47,7 @@ class ProjectionExecutor; using IndexabilityDiscriminator = std::function<bool(const MatchExpression* me)>; using IndexabilityDiscriminators = std::vector<IndexabilityDiscriminator>; using IndexToDiscriminatorMap = StringMap<CompositeIndexabilityDiscriminator>; +using PathDiscriminatorsMap = StringMap<IndexToDiscriminatorMap>; /** * CompositeIndexabilityDiscriminator holds all indexability discriminators for a particular path, @@ -77,9 +78,14 @@ private: }; /** - * PlanCacheIndexabilityState holds a set of "indexability discriminators" for certain paths. - * An indexability discriminator is a binary predicate function, used to classify match - * expressions based on the data values in the expression. + * PlanCacheIndexabilityState holds a set of "indexability discriminators. An indexability + * discriminator is a binary predicate function, used to classify match expressions based on the + * data values in the expression. + * + * These discriminators are used to distinguish between queries of a similar shape but not the same + * candidate indexes. So each discriminator typically represents a decision like "is this index + * valid?" or "does this piece of the query disqualify it from using this index?". The output of + * these decisions is included in the plan cache key. */ class PlanCacheIndexabilityState { PlanCacheIndexabilityState(const PlanCacheIndexabilityState&) = delete; @@ -95,7 +101,15 @@ public: * The object returned by reference is valid until the next call to updateDiscriminators() or * until destruction of 'this', whichever is first. */ - const IndexToDiscriminatorMap& getDiscriminators(StringData path) const; + const IndexToDiscriminatorMap& getPathDiscriminators(StringData path) const; + + /** + * Returns a map of index name to discriminator set. These discriminators are not + * associated with a particular path of a query and apply to the entire MatchExpression. + */ + const IndexToDiscriminatorMap& getGlobalDiscriminators() const { + return _globalDiscriminatorMap; + } /** * Construct an IndexToDiscriminator map for the given path, only for the wildcard indexes @@ -109,8 +123,6 @@ public: void updateDiscriminators(const std::vector<CoreIndexInfo>& indexCores); private: - using PathDiscriminatorsMap = StringMap<IndexToDiscriminatorMap>; - /** * A $** index may index an infinite number of fields. We cannot just store a discriminator for * every possible field that it indexes, so we have to maintain some special context about the @@ -142,8 +154,8 @@ private: void processSparseIndex(const std::string& indexName, const BSONObj& keyPattern); /** - * Adds partial index discriminators for the partial index with the given filter expression - * to the discriminators for that index in '_pathDiscriminatorsMap'. + * Adds a global discriminator for the partial index with the given filter expression + * to the discriminators for that index in '_globalDiscriminatorMap'. * * A partial index discriminator distinguishes expressions that match a given partial index * predicate from expressions that don't match the partial index predicate. For example, @@ -174,6 +186,10 @@ private: // PathDiscriminatorsMap is a map from field path to index name to IndexabilityDiscriminator. PathDiscriminatorsMap _pathDiscriminatorsMap; + // Map from index name to global discriminators. These are discriminators which do not apply to + // a single path but the entire MatchExpression. + IndexToDiscriminatorMap _globalDiscriminatorMap; + std::vector<WildcardIndexDiscriminatorContext> _wildcardIndexDiscriminators; }; diff --git a/src/mongo/db/query/plan_cache_indexability_test.cpp b/src/mongo/db/query/plan_cache_indexability_test.cpp index af7677a8cd5..49f9fb79285 100644 --- a/src/mongo/db/query/plan_cache_indexability_test.cpp +++ b/src/mongo/db/query/plan_cache_indexability_test.cpp @@ -104,7 +104,7 @@ TEST(PlanCacheIndexabilityTest, SparseIndexSimple) { nullptr, nullptr)}); - auto discriminators = state.getDiscriminators("a"); + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1") != discriminators.end()); @@ -146,7 +146,7 @@ TEST(PlanCacheIndexabilityTest, SparseIndexCompound) { nullptr)}); { - auto discriminators = state.getDiscriminators("a"); + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1_b_1") != discriminators.end()); @@ -159,7 +159,7 @@ TEST(PlanCacheIndexabilityTest, SparseIndexCompound) { } { - auto discriminators = state.getDiscriminators("b"); + auto discriminators = state.getPathDiscriminators("b"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1_b_1") != discriminators.end()); @@ -193,12 +193,17 @@ TEST(PlanCacheIndexabilityTest, PartialIndexSimple) { nullptr, nullptr)}); + // The partial index is represented as a global discriminator that applies to the entire + // incoming MatchExpression. { - auto discriminators = state.getDiscriminators("f"); - ASSERT_EQ(1U, discriminators.size()); - ASSERT(discriminators.find("a_1") != discriminators.end()); + auto discriminators = state.getPathDiscriminators("f"); + ASSERT_EQ(0U, discriminators.size()); - auto disc = discriminators["a_1"]; + auto globalDiscriminators = state.getGlobalDiscriminators(); + ASSERT_EQ(1U, globalDiscriminators.size()); + ASSERT(globalDiscriminators.find("a_1") != globalDiscriminators.end()); + + auto disc = globalDiscriminators["a_1"]; ASSERT_EQ(false, disc.isMatchCompatibleWithIndex( parseMatchExpression(BSON("f" << BSON("$gt" << -5))).get())); @@ -208,7 +213,7 @@ TEST(PlanCacheIndexabilityTest, PartialIndexSimple) { } { - auto discriminators = state.getDiscriminators("a"); + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1") != discriminators.end()); @@ -243,32 +248,52 @@ TEST(PlanCacheIndexabilityTest, PartialIndexAnd) { nullptr, nullptr)}); + // partial index discriminators are global to the entire query, so an individual path should not + // have any discriminators. Also the entire query must be a subset of the partial filter + // expression, not just the leaves. + auto globalDiscriminators = state.getGlobalDiscriminators(); + ASSERT(globalDiscriminators.find("a_1") != globalDiscriminators.end()); + auto globalDisc = globalDiscriminators["a_1"]; + { - auto discriminators = state.getDiscriminators("f"); - ASSERT_EQ(1U, discriminators.size()); - ASSERT(discriminators.find("a_1") != discriminators.end()); + auto discriminators = state.getPathDiscriminators("f"); + ASSERT_EQ(0U, discriminators.size()); - auto disc = discriminators["a_1"]; - ASSERT_EQ(false, - disc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 0)).get())); - ASSERT_EQ(true, - disc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 1)).get())); + ASSERT_EQ( + false, + globalDisc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 0)).get())); + ASSERT_EQ( + false, + globalDisc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 1)).get())); } { - auto discriminators = state.getDiscriminators("g"); - ASSERT_EQ(1U, discriminators.size()); - ASSERT(discriminators.find("a_1") != discriminators.end()); + auto discriminators = state.getPathDiscriminators("g"); + ASSERT_EQ(0U, discriminators.size()); - auto disc = discriminators["a_1"]; + ASSERT_EQ( + false, + globalDisc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("g" << 0)).get())); + ASSERT_EQ( + false, + globalDisc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("g" << 1)).get())); + } + + { + // A match expression which is covered entirely by the partial filter should pass the global + // discriminator. ASSERT_EQ(false, - disc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("g" << 0)).get())); + globalDisc.isMatchCompatibleWithIndex( + parseMatchExpression(BSON("g" << 1 << "f" << 0)).get())); ASSERT_EQ(true, - disc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("g" << 1)).get())); + globalDisc.isMatchCompatibleWithIndex( + parseMatchExpression(BSON("g" << 1 << "f" << 1)).get())); } { - auto discriminators = state.getDiscriminators("a"); + // The path 'a' will still have a discriminator for the collation (even though it's + // defaulted). + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1") != discriminators.end()); @@ -319,33 +344,44 @@ TEST(PlanCacheIndexabilityTest, MultiplePartialIndexes) { nullptr, nullptr)}); - { - auto discriminators = state.getDiscriminators("f"); - ASSERT_EQ(2U, discriminators.size()); - ASSERT(discriminators.find("a_1") != discriminators.end()); - ASSERT(discriminators.find("b_1") != discriminators.end()); + // partial index discriminators are global to the entire query, so an individual path within the + // partial filter should not have any discriminators. Also the entire query must be a subset of + // the partial filter expression, not just the leaves. + auto globalDiscriminators = state.getGlobalDiscriminators(); + ASSERT(globalDiscriminators.find("a_1") != globalDiscriminators.end()); + ASSERT(globalDiscriminators.find("b_1") != globalDiscriminators.end()); + auto globalDiscA = globalDiscriminators["a_1"]; + auto globalDiscB = globalDiscriminators["b_1"]; - auto discA = discriminators["a_1"]; - auto discB = discriminators["b_1"]; + { + auto discriminators = state.getPathDiscriminators("f"); + ASSERT_EQ(0U, discriminators.size()); - ASSERT_EQ(false, - discA.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 0)).get())); - ASSERT_EQ(false, - discB.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 0)).get())); + ASSERT_EQ( + false, + globalDiscA.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 0)).get())); + ASSERT_EQ( + false, + globalDiscB.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 0)).get())); - ASSERT_EQ(true, - discA.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 1)).get())); - ASSERT_EQ(false, - discB.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 1)).get())); + ASSERT_EQ( + true, + globalDiscA.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 1)).get())); + ASSERT_EQ( + false, + globalDiscB.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 1)).get())); - ASSERT_EQ(false, - discA.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 2)).get())); - ASSERT_EQ(true, - discB.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 2)).get())); + ASSERT_EQ( + false, + globalDiscA.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 2)).get())); + ASSERT_EQ( + true, + globalDiscB.isMatchCompatibleWithIndex(parseMatchExpression(BSON("f" << 2)).get())); } + // The paths 'a' and 'b' will have one discriminator each to capture the collation of the index. { - auto discriminators = state.getDiscriminators("a"); + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1") != discriminators.end()); @@ -359,7 +395,7 @@ TEST(PlanCacheIndexabilityTest, MultiplePartialIndexes) { } { - auto discriminators = state.getDiscriminators("b"); + auto discriminators = state.getPathDiscriminators("b"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("b_1") != discriminators.end()); @@ -392,7 +428,7 @@ TEST(PlanCacheIndexabilityTest, IndexNeitherSparseNorPartial) { BSONObj(), nullptr, nullptr)}); - auto discriminators = state.getDiscriminators("a"); + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1") != discriminators.end()); } @@ -421,7 +457,7 @@ TEST(PlanCacheIndexabilityTest, DiscriminatorForCollationIndicatesWhenCollations boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); expCtx->setCollator(collator.clone()); - auto discriminators = state.getDiscriminators("a"); + auto discriminators = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminators.size()); ASSERT(discriminators.find("a_1") != discriminators.end()); @@ -506,11 +542,11 @@ TEST(PlanCacheIndexabilityTest, CompoundIndexCollationDiscriminator) { nullptr, nullptr)}); - auto discriminatorsA = state.getDiscriminators("a"); + auto discriminatorsA = state.getPathDiscriminators("a"); ASSERT_EQ(1U, discriminatorsA.size()); ASSERT(discriminatorsA.find("a_1_b_1") != discriminatorsA.end()); - auto discriminatorsB = state.getDiscriminators("b"); + auto discriminatorsB = state.getPathDiscriminators("b"); ASSERT_EQ(1U, discriminatorsB.size()); ASSERT(discriminatorsB.find("a_1_b_1") != discriminatorsB.end()); } @@ -619,13 +655,15 @@ TEST(PlanCacheIndexabilityTest, WildcardPartialIndexDiscriminator) { ASSERT_TRUE(wildcardDiscriminators.isMatchCompatibleWithIndex( parseMatchExpression(fromjson("{b: 6}")).get())); - // The regular (non-wildcard) set of discriminators for the path "a" should reflect whether a - // predicate on "a" is compatible with the partial filter expression. + // The global discriminator for the index "indexName" should reflect whether a MatchExpression + // is compatible with the partial filter expression. { - discriminatorsA = state.getDiscriminators("a"); - auto discriminatorsIt = discriminatorsA.find("indexName"); - ASSERT(discriminatorsIt != discriminatorsA.end()); - auto disc = discriminatorsIt->second; + discriminatorsA = state.getPathDiscriminators("a"); + ASSERT(discriminatorsA.find("indexName") == discriminatorsA.end()); + + auto globalDisc = state.getGlobalDiscriminators(); + ASSERT(globalDisc.find("indexName") != globalDisc.end()); + auto disc = globalDisc["indexName"]; ASSERT_FALSE( disc.isMatchCompatibleWithIndex(parseMatchExpression(fromjson("{a: 0}")).get())); @@ -640,7 +678,7 @@ TEST(PlanCacheIndexabilityTest, WildcardPartialIndexDiscriminator) { // There shouldn't be any regular discriminators associated with path "b". { - auto&& discriminatorsB = state.getDiscriminators("b"); + auto&& discriminatorsB = state.getPathDiscriminators("b"); ASSERT_FALSE(discriminatorsB.count("indexName")); } } diff --git a/src/mongo/db/query/plan_cache_key_factory.cpp b/src/mongo/db/query/plan_cache_key_factory.cpp index d47f1768858..c3573a8b4f4 100644 --- a/src/mongo/db/query/plan_cache_key_factory.cpp +++ b/src/mongo/db/query/plan_cache_key_factory.cpp @@ -29,30 +29,29 @@ #include "mongo/db/query/plan_cache_key_factory.h" +#include "mongo/db/query/canonical_query_encoder.h" #include "mongo/db/query/collection_query_info.h" #include "mongo/db/query/planner_ixselect.h" #include "mongo/db/s/operation_sharding_state.h" namespace mongo { namespace plan_cache_detail { -// Delimiters for cache key encoding. -const char kEncodeDiscriminatorsBegin = '<'; -const char kEncodeDiscriminatorsEnd = '>'; void encodeIndexabilityForDiscriminators(const MatchExpression* tree, const IndexToDiscriminatorMap& discriminators, StringBuilder* keyBuilder) { + for (auto&& indexAndDiscriminatorPair : discriminators) { *keyBuilder << indexAndDiscriminatorPair.second.isMatchCompatibleWithIndex(tree); } } -void encodeIndexability(const MatchExpression* tree, - const PlanCacheIndexabilityState& indexabilityState, - StringBuilder* keyBuilder) { +void encodeIndexabilityRecursive(const MatchExpression* tree, + const PlanCacheIndexabilityState& indexabilityState, + StringBuilder* keyBuilder) { if (!tree->path().empty()) { const IndexToDiscriminatorMap& discriminators = - indexabilityState.getDiscriminators(tree->path()); + indexabilityState.getPathDiscriminators(tree->path()); IndexToDiscriminatorMap wildcardDiscriminators = indexabilityState.buildWildcardDiscriminators(tree->path()); if (!discriminators.empty() || !wildcardDiscriminators.empty()) { @@ -72,8 +71,26 @@ void encodeIndexability(const MatchExpression* tree, } for (size_t i = 0; i < tree->numChildren(); ++i) { - encodeIndexability(tree->getChild(i), indexabilityState, keyBuilder); + encodeIndexabilityRecursive(tree->getChild(i), indexabilityState, keyBuilder); + } +} + +void encodeIndexability(const MatchExpression* tree, + const PlanCacheIndexabilityState& indexabilityState, + StringBuilder* keyBuilder) { + // Before encoding the indexability of the leaf MatchExpressions, apply the global + // discriminators to the expression as a whole. This is for cases such as partial indexes which + // must discriminate based on the entire query. + const auto& globalDiscriminators = indexabilityState.getGlobalDiscriminators(); + if (!globalDiscriminators.empty()) { + *keyBuilder << kEncodeGlobalDiscriminatorsBegin; + for (auto&& indexAndDiscriminatorPair : globalDiscriminators) { + *keyBuilder << indexAndDiscriminatorPair.second.isMatchCompatibleWithIndex(tree); + } + *keyBuilder << kEncodeGlobalDiscriminatorsEnd; } + + encodeIndexabilityRecursive(tree, indexabilityState, keyBuilder); } PlanCacheKeyInfo makePlanCacheKeyInfo(const CanonicalQuery& query, diff --git a/src/mongo/db/query/plan_cache_key_info_test.cpp b/src/mongo/db/query/plan_cache_key_info_test.cpp index 7235386e7f4..a13616e12c4 100644 --- a/src/mongo/db/query/plan_cache_key_info_test.cpp +++ b/src/mongo/db/query/plan_cache_key_info_test.cpp @@ -194,6 +194,107 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyPartialIndex) { makeKey(*cqGtZero, indexCores)); } +TEST(PlanCacheKeyInfoTest, ComputeKeyPartialIndexConjunction) { + BSONObj filterObj = fromjson("{f: {$gt: 0, $lt: 10}}"); + unique_ptr<MatchExpression> filterExpr(parseMatchExpression(filterObj)); + + const auto keyPattern = BSON("a" << 1); + const std::vector<CoreIndexInfo> indexCores = { + CoreIndexInfo(keyPattern, + IndexNames::nameToType(IndexNames::findPluginName(keyPattern)), + false, // sparse + IndexEntry::Identifier{""}, // name + filterExpr.get())}; // filterExpr + + unique_ptr<CanonicalQuery> satisfySinglePredicate(canonicalize("{f: {$gt: 0}}")); + ASSERT_EQ(makeKey(*satisfySinglePredicate, indexCores).getIndexabilityDiscriminators(), "(0)"); + + unique_ptr<CanonicalQuery> satisfyBothPredicates(canonicalize("{f: {$eq: 5}}")); + ASSERT_EQ(makeKey(*satisfyBothPredicates, indexCores).getIndexabilityDiscriminators(), "(1)"); + + unique_ptr<CanonicalQuery> conjSingleField(canonicalize("{f: {$gt: 2, $lt: 9}}")); + ASSERT_EQ(makeKey(*conjSingleField, indexCores).getIndexabilityDiscriminators(), "(1)"); + + unique_ptr<CanonicalQuery> conjSingleFieldNoMatch(canonicalize("{f: {$gt: 2, $lt: 11}}")); + ASSERT_EQ(makeKey(*conjSingleFieldNoMatch, indexCores).getIndexabilityDiscriminators(), "(0)"); + + // Note that these queries get optimized to a single $in over 'f'. + unique_ptr<CanonicalQuery> disjSingleFieldBothSatisfy( + canonicalize("{$or: [{f: {$eq: 2}}, {f: {$eq: 3}}]}")); + ASSERT_EQ(makeKey(*disjSingleFieldBothSatisfy, indexCores).getIndexabilityDiscriminators(), + "(1)"); + + unique_ptr<CanonicalQuery> disjSingleFieldNotSubset( + canonicalize("{$or: [{f: {$eq: 2}}, {f: {$eq: 11}}]}")); + ASSERT_EQ(makeKey(*disjSingleFieldNotSubset, indexCores).getIndexabilityDiscriminators(), + "(0)"); +} + +TEST(PlanCacheKeyInfoTest, ComputeKeyPartialIndexDisjunction) { + BSONObj filterObj = fromjson("{$or: [{f: {$gt: 10}}, {f: {$lt: 0}}]}"); + unique_ptr<MatchExpression> filterExpr(parseMatchExpression(filterObj)); + + const auto keyPattern = BSON("a" << 1); + const std::vector<CoreIndexInfo> indexCores = { + CoreIndexInfo(keyPattern, + IndexNames::nameToType(IndexNames::findPluginName(keyPattern)), + false, // sparse + IndexEntry::Identifier{""}, // name + filterExpr.get())}; // filterExpr + + unique_ptr<CanonicalQuery> satisfySinglePredicate(canonicalize("{f: {$eq: 11}}")); + ASSERT_EQ(makeKey(*satisfySinglePredicate, indexCores).getIndexabilityDiscriminators(), "(1)"); + + unique_ptr<CanonicalQuery> satisfyNeither(canonicalize("{f: {$eq: 5}}")); + ASSERT_EQ(makeKey(*satisfyNeither, indexCores).getIndexabilityDiscriminators(), "(0)"); + + unique_ptr<CanonicalQuery> conjSingleFieldMatch(canonicalize("{f: {$lt: 20, $gt: 10}}")); + ASSERT_EQ(makeKey(*conjSingleFieldMatch, indexCores).getIndexabilityDiscriminators(), "(1)"); + + unique_ptr<CanonicalQuery> conjSingleFieldNoMatch(canonicalize("{f: {$gt: 2, $lt: 10}}")); + ASSERT_EQ(makeKey(*conjSingleFieldNoMatch, indexCores).getIndexabilityDiscriminators(), "(0)"); + + unique_ptr<CanonicalQuery> conjSingleFieldOverlap(canonicalize("{f: {$gt: 2, $lt: 12}}")); + ASSERT_EQ(makeKey(*conjSingleFieldOverlap, indexCores).getIndexabilityDiscriminators(), "(0)"); + + // Although this query is technically a subset of the partial filter, the logic to determine + // such ('isSubsetOf' in the code) is conservative in how it compares certain shapes of + // expression trees. + unique_ptr<CanonicalQuery> disjSingleFieldBothSatisfy( + canonicalize("{$or: [{f: {$eq: -1}}, {f: {$gt: 10}}]}")); + ASSERT_EQ(makeKey(*disjSingleFieldBothSatisfy, indexCores).getIndexabilityDiscriminators(), + "(0)"); + + unique_ptr<CanonicalQuery> disjSingleFieldNotSubset( + canonicalize("{$or: [{f: {$eq: 2}}, {f: {$eq: 11}}]}")); + ASSERT_EQ(makeKey(*disjSingleFieldNotSubset, indexCores).getIndexabilityDiscriminators(), + "(0)"); +} + +TEST(PlanCacheKeyInfoTest, ComputeKeyPartialIndexNestedDisjunction) { + BSONObj filterObj = fromjson(R"( + {$and: [ + {$or: [{f: {$gt: 10}}, {f: {$lt: 0}}]}, + {$or: [{f: {$gt: 11}}, {f: {$lt: 1}}]} + ]})"); + unique_ptr<MatchExpression> filterExpr(parseMatchExpression(filterObj)); + + const auto keyPattern = BSON("a" << 1); + const std::vector<CoreIndexInfo> indexCores = { + CoreIndexInfo(keyPattern, + IndexNames::nameToType(IndexNames::findPluginName(keyPattern)), + false, // sparse + IndexEntry::Identifier{""}, // name + filterExpr.get())}; // filterExpr + + + unique_ptr<CanonicalQuery> satisfySinglePredicate(canonicalize("{f: {$eq: 11}}")); + ASSERT_EQ(makeKey(*satisfySinglePredicate, indexCores).getIndexabilityDiscriminators(), "(0)"); + + unique_ptr<CanonicalQuery> notCompat(canonicalize("{f: {$eq: 12}}")); + ASSERT_EQ(makeKey(*notCompat, indexCores).getIndexabilityDiscriminators(), "(1)"); +} + // Query shapes should get the same plan cache key if they have the same collation indexability. TEST(PlanCacheKeyInfoTest, ComputeKeyCollationIndex) { CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); @@ -366,8 +467,8 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyBasedOnPartia // The discriminator strings have the format "<xx>". That is, there are two discriminator // bits for the "x" predicate, the first pertaining to the partialFilterExpression and the // second around applicability to the wildcard index. - ASSERT_EQ(compatibleKey.getIndexabilityDiscriminators(), "<11>"); - ASSERT_EQ(incompatibleKey.getIndexabilityDiscriminators(), "<01>"); + ASSERT_EQ(compatibleKey.getIndexabilityDiscriminators(), "(1)<1>"); + ASSERT_EQ(incompatibleKey.getIndexabilityDiscriminators(), "(0)<1>"); } // The partialFilterExpression should lead to a discriminator over field 'x', but not over 'y'. @@ -382,8 +483,8 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyBasedOnPartia // The discriminator strings have the format "<xx><y>". That is, there are two discriminator // bits for the "x" predicate (the first pertaining to the partialFilterExpression, the // second around applicability to the wildcard index) and one discriminator bit for "y". - ASSERT_EQ(compatibleKey.getIndexabilityDiscriminators(), "<11><1>"); - ASSERT_EQ(incompatibleKey.getIndexabilityDiscriminators(), "<01><1>"); + ASSERT_EQ(compatibleKey.getIndexabilityDiscriminators(), "(1)<1><1>"); + ASSERT_EQ(incompatibleKey.getIndexabilityDiscriminators(), "(0)<1><1>"); } // $eq:null predicates cannot be assigned to a wildcard index. Make sure that this is @@ -398,8 +499,8 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyBasedOnPartia // The discriminator strings have the format "<xx><y>". That is, there are two discriminator // bits for the "x" predicate (the first pertaining to the partialFilterExpression, the // second around applicability to the wildcard index) and one discriminator bit for "y". - ASSERT_EQ(compatibleKey.getIndexabilityDiscriminators(), "<11><1>"); - ASSERT_EQ(incompatibleKey.getIndexabilityDiscriminators(), "<11><0>"); + ASSERT_EQ(compatibleKey.getIndexabilityDiscriminators(), "(1)<1><1>"); + ASSERT_EQ(incompatibleKey.getIndexabilityDiscriminators(), "(1)<1><0>"); } // Test that the discriminators are correct for an $eq:null predicate on 'x'. This predicate is @@ -408,7 +509,7 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyBasedOnPartia // result in two "0" bits inside the discriminator string. { auto key = makeKey(*canonicalize("{x: {$eq: null}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<00>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(0)<0>"); } } @@ -449,11 +550,11 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyWithPartialFi const std::vector<CoreIndexInfo> indexCores = {indexInfo}; { - // The discriminators should have the format <xx><yy><z>. The 'z' predicate has just one - // discriminator because it is not referenced in the partial filter expression. All + // TODO update The discriminators should have the format <xx><yy><z>. The 'z' predicate has + // just one discriminator because it is not referenced in the partial filter expression. All // predicates are compatible. auto key = makeKey(*canonicalize("{x: {$eq: 1}, y: {$eq: 2}, z: {$eq: 3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<11><11><1>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(1)<1><1><1>"); } { @@ -461,7 +562,7 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyWithPartialFi // compatible with the partial filter expression, leading to one of the 'y' bits being set // to zero. auto key = makeKey(*canonicalize("{x: {$eq: 1}, y: {$eq: -2}, z: {$eq: 3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<11><01><1>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(0)<1><1><1>"); } } @@ -480,20 +581,20 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyDiscriminatesCorrectlyWithPartialFilterAndW // the predicate is compatible with the partial filter expression, whereas the disciminator // for 'y' is about compatibility with the wildcard index. auto key = makeKey(*canonicalize("{x: {$eq: 1}, y: {$eq: 2}, z: {$eq: 3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<1><1>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(1)<1>"); } { // Similar to the previous case, except with an 'x' predicate that is incompatible with the // partial filter expression. auto key = makeKey(*canonicalize("{x: {$eq: -1}, y: {$eq: 2}, z: {$eq: 3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<0><1>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(0)<1>"); } { // Case where the 'y' predicate is not compatible with the wildcard index. auto key = makeKey(*canonicalize("{x: {$eq: 1}, y: {$eq: null}, z: {$eq: 3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<1><0>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(1)<0>"); } } @@ -511,14 +612,14 @@ TEST(PlanCacheKeyInfoTest, ComputeKeyWildcardDiscriminatesCorrectlyWithPartialFi // The discriminators have the format <x><(x.y)(x.y)<y>. All predicates are compatible auto key = makeKey(*canonicalize("{x: {$eq: 1}, y: {$eq: 2}, 'x.y': {$eq: 3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<1><11><1>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(1)<1><1><1>"); } { // Here, the predicate on "x.y" is not compatible with the partial filter expression. auto key = makeKey(*canonicalize("{x: {$eq: 1}, y: {$eq: 2}, 'x.y': {$eq: -3}}"), indexCores); - ASSERT_EQ(key.getIndexabilityDiscriminators(), "<1><01><1>"); + ASSERT_EQ(key.getIndexabilityDiscriminators(), "(0)<1><1><1>"); } } |