From 523f08103d6317dff1700828d939b09d11c2fd2d Mon Sep 17 00:00:00 2001 From: James Wahlin Date: Tue, 24 Sep 2019 12:49:28 +0000 Subject: SERVER-39019 $elemMatch $ne serialization is incorrect, doesn't roundtrip (cherry picked from commit 3db787da9dc453192adcf7706991d2d09f0ff0c7) --- src/mongo/db/matcher/expression_array.cpp | 34 +++++++++++++++ .../db/matcher/expression_serialization_test.cpp | 50 ++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/mongo/db/matcher/expression_array.cpp b/src/mongo/db/matcher/expression_array.cpp index 86fbc384eae..b6a9734233d 100644 --- a/src/mongo/db/matcher/expression_array.cpp +++ b/src/mongo/db/matcher/expression_array.cpp @@ -201,6 +201,40 @@ void ElemMatchValueMatchExpression::debugString(StringBuilder& debug, int level) void ElemMatchValueMatchExpression::serialize(BSONObjBuilder* out) const { BSONObjBuilder emBob; + // NotMatchExpression will serialize to a $nor. This serialization is incorrect for + // ElemMatchValue however as $nor is a top-level expression and expects that contained + // expressions have path information. For this case we will serialize to $not. + if (_subs.size() == 1 && _subs[0]->matchType() == MatchType::NOT) { + std::vector childList{_subs[0]->getChild(0)}; + + MatchExpression* notChildExpr = _subs[0]->getChild(0); + if (notChildExpr->matchType() == MatchType::AND) { + childList = *notChildExpr->getChildVector(); + } + + const bool allChildrenAreLeafMatchExpression = + std::all_of(childList.begin(), childList.end(), [](MatchExpression* child) { + return dynamic_cast(child); + }); + + if (allChildrenAreLeafMatchExpression) { + BSONObjBuilder pathBuilder(out->subobjStart(path())); + BSONObjBuilder elemMatchBuilder(pathBuilder.subobjStart("$elemMatch")); + BSONObjBuilder notBuilder(elemMatchBuilder.subobjStart("$not")); + + for (auto&& child : childList) { + BSONObjBuilder predicate; + child->serialize(&predicate); + notBuilder.appendElements(predicate.obj().firstElement().embeddedObject()); + } + + notBuilder.doneFast(); + elemMatchBuilder.doneFast(); + pathBuilder.doneFast(); + return; + } + } + for (unsigned i = 0; i < _subs.size(); i++) { BSONObjBuilder predicate; _subs[i]->serialize(&predicate); diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp index 13a2aaa172d..cac2f5155ba 100644 --- a/src/mongo/db/matcher/expression_serialization_test.cpp +++ b/src/mongo/db/matcher/expression_serialization_test.cpp @@ -207,6 +207,56 @@ TEST(SerializeBasic, ExpressionElemMatchValueWithEmptyStringSerializesCorrectly) ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); } +TEST(SerializeBasic, ExpressionElemMatchValueWithNotEqualSerializesCorrectly) { + const CollatorInterface* collator = nullptr; + Matcher original(fromjson("{x: {$elemMatch: {$ne: 1}}}"), ExtensionsCallbackNoop(), collator); + Matcher reserialized( + serialize(original.getMatchExpression()), ExtensionsCallbackNoop(), collator); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), fromjson("{x: {$elemMatch: {$not: {$eq: 1}}}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); + + BSONObj obj = fromjson("{x: [{a: 1, b: -1}, {a: -1, b: 1}]}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); + + obj = fromjson("{x: [{a: 1, b: 1}, {a: 0, b: 0}]}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); + + obj = fromjson("{x: [1, 0]}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); +} + +TEST(SerializeBasic, ExpressionElemMatchValueWithNotLessThanGreaterThanSerializesCorrectly) { + const CollatorInterface* collator = nullptr; + Matcher original(fromjson("{x: {$elemMatch: {$not: {$lt: 10, $gt: 5}}}}"), + ExtensionsCallbackNoop(), + collator); + Matcher reserialized( + serialize(original.getMatchExpression()), ExtensionsCallbackNoop(), collator); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), + fromjson("{x: {$elemMatch: {$not: {$lt: 10, $gt: 5}}}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); + + auto obj = fromjson("{x: [5]}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); +} + +TEST(SerializeBasic, ExpressionElemMatchObjectWithNorSerializesCorrectly) { + const CollatorInterface* collator = nullptr; + Matcher original( + fromjson("{x: {$elemMatch: {$nor: [{y: 2}]}}}"), ExtensionsCallbackNoop(), collator); + Matcher reserialized( + serialize(original.getMatchExpression()), ExtensionsCallbackNoop(), collator); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), + fromjson("{x: {$elemMatch: {$nor: [{y: {$eq: 2}}]}}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); + + BSONObj obj = fromjson("{x: [{y: 100}]}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); + + obj = fromjson("{x: [{y: 2}]}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); +} + TEST(SerializeBasic, ExpressionSizeSerializesCorrectly) { const CollatorInterface* collator = nullptr; Matcher original(fromjson("{x: {$size: 2}}"), ExtensionsCallbackNoop(), collator); -- cgit v1.2.1