diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2017-08-24 13:23:41 -0400 |
---|---|---|
committer | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2017-09-19 13:47:58 -0400 |
commit | 4574a7c8dd12650eceee1e197b35ba47dfbb7f8e (patch) | |
tree | 9709578bb7347d0395e900b7ccace7b4cb5c156d /src | |
parent | af7387affece778a2507666b8bc5f502778fe1b8 (diff) | |
download | mongo-4574a7c8dd12650eceee1e197b35ba47dfbb7f8e.tar.gz |
SERVER-30176: Extend the JSON Schema parser to handle logical restriction keywords (enum only)
Diffstat (limited to 'src')
32 files changed, 1332 insertions, 274 deletions
diff --git a/src/mongo/bson/bson_comparator_interface_base.cpp b/src/mongo/bson/bson_comparator_interface_base.cpp index 6dfb379fd67..4605aa0fe24 100644 --- a/src/mongo/bson/bson_comparator_interface_base.cpp +++ b/src/mongo/bson/bson_comparator_interface_base.cpp @@ -43,10 +43,18 @@ template <typename T> void BSONComparatorInterfaceBase<T>::hashCombineBSONObj( size_t& seed, const BSONObj& objToHash, - bool considerFieldName, + ComparisonRulesSet rules, const StringData::ComparatorInterface* stringComparator) { - for (auto elem : objToHash) { - hashCombineBSONElement(seed, elem, considerFieldName, stringComparator); + + if (rules & ComparisonRules::kIgnoreFieldOrder) { + BSONObjIteratorSorted iter(objToHash); + while (iter.more()) { + hashCombineBSONElement(seed, iter.next(), rules, stringComparator); + } + } else { + for (auto elem : objToHash) { + hashCombineBSONElement(seed, elem, rules, stringComparator); + } } } @@ -54,18 +62,16 @@ template <typename T> void BSONComparatorInterfaceBase<T>::hashCombineBSONElement( size_t& hash, BSONElement elemToHash, - bool considerFieldName, + ComparisonRulesSet rules, const StringData::ComparatorInterface* stringComparator) { boost::hash_combine(hash, elemToHash.canonicalType()); const StringData fieldName = elemToHash.fieldNameStringData(); - if (considerFieldName && !fieldName.empty()) { + if ((rules & ComparisonRules::kConsiderFieldName) && !fieldName.empty()) { SimpleStringDataComparator::kInstance.hash_combine(hash, fieldName); } switch (elemToHash.type()) { - // Order of types is the same as in compareElementValues(). - case mongo::EOO: case mongo::Undefined: case mongo::jstNULL: @@ -145,7 +151,7 @@ void BSONComparatorInterfaceBase<T>::hashCombineBSONElement( case mongo::Array: hashCombineBSONObj(hash, elemToHash.embeddedObject(), - true, // considerFieldName + rules | ComparisonRules::kConsiderFieldName, stringComparator); break; @@ -166,7 +172,7 @@ void BSONComparatorInterfaceBase<T>::hashCombineBSONElement( hash, StringData(elemToHash.codeWScopeCode(), elemToHash.codeWScopeCodeLen())); hashCombineBSONObj(hash, elemToHash.codeWScopeObject(), - true, // considerFieldName + rules | ComparisonRules::kConsiderFieldName, &SimpleStringDataComparator::kInstance); break; } diff --git a/src/mongo/bson/bson_comparator_interface_base.h b/src/mongo/bson/bson_comparator_interface_base.h index 29d80916f72..341994aa67c 100644 --- a/src/mongo/bson/bson_comparator_interface_base.h +++ b/src/mongo/bson/bson_comparator_interface_base.h @@ -57,6 +57,39 @@ public: BSONComparatorInterfaceBase& operator=(BSONComparatorInterfaceBase&& other) = default; /** + * Set of rules used in the comparison of BSON Objects and Elements. + */ + enum ComparisonRules { + // Set this bit to consider the field name in element comparisons. + // if (kConsiderFieldName = 0) --> 'a: 1' == 'b: 1' + // if (kConsiderFieldName = 1) --> 'a: 1' != 'b: 1' + kConsiderFieldName = 1 << 0, + + // Set this bit to ignore the element order in BSON Object comparisons. This field will + // remain set/unset for nested objects. + // + // e.g. if kIgnoreFieldOrder == 1, then the following objects are considered equal: + // + // obj1: { + // a: { + // b: 1, + // c: 1 + // }, + // d: 1 + // } + // + // obj2: { + // d: 1, + // a: { + // c: 1, + // b: 1, + // }, + // } + kIgnoreFieldOrder = 1 << 1, + }; + using ComparisonRulesSet = uint32_t; + + /** * A deferred comparison between two objects of type T, which can be converted into a boolean * via the evaluate() method. */ @@ -240,7 +273,7 @@ protected: */ static void hashCombineBSONObj(size_t& seed, const BSONObj& objToHash, - bool considerFieldName, + ComparisonRulesSet rules, const StringData::ComparatorInterface* stringComparator); /** @@ -250,7 +283,7 @@ protected: */ static void hashCombineBSONElement(size_t& seed, BSONElement elemToHash, - bool considerFieldName, + ComparisonRulesSet rules, const StringData::ComparatorInterface* stringComparator); }; diff --git a/src/mongo/bson/bson_obj_test.cpp b/src/mongo/bson/bson_obj_test.cpp index 5a9360605d5..82f95be072a 100644 --- a/src/mongo/bson/bson_obj_test.cpp +++ b/src/mongo/bson/bson_obj_test.cpp @@ -29,6 +29,7 @@ #include "mongo/bson/bsonobj_comparator.h" #include "mongo/bson/simple_bsonelement_comparator.h" #include "mongo/bson/simple_bsonobj_comparator.h" +#include "mongo/bson/unordered_fields_bsonobj_comparator.h" #include "mongo/db/jsobj.h" #include "mongo/db/json.h" #include "mongo/platform/decimal128.h" @@ -525,6 +526,8 @@ TEST(BSONObjCompare, BSONObjHashingIgnoresTopLevelFieldNamesWhenRequested) { ASSERT_NE(bsonCmpConsiderFieldNames.hash(obj1), bsonCmpConsiderFieldNames.hash(obj2)); ASSERT_EQ(bsonCmpIgnoreFieldNames.hash(obj1), bsonCmpIgnoreFieldNames.hash(obj2)); ASSERT_NE(bsonCmpIgnoreFieldNames.hash(obj1), bsonCmpIgnoreFieldNames.hash(obj3)); + ASSERT_NE(bsonCmpIgnoreFieldNames.hash(fromjson("{a: {b: 1, c: 1}, d: 1}")), + bsonCmpIgnoreFieldNames.hash(fromjson("{a: {b: 1, c: 2}, d: 1}"))); } TEST(BSONObjCompare, BSONElementHashingIgnoresEltFieldNameWhenRequested) { @@ -544,6 +547,50 @@ TEST(BSONObjCompare, BSONElementHashingIgnoresEltFieldNameWhenRequested) { bsonCmpIgnoreFieldNames.hash(obj3.firstElement())); } +TEST(BSONObjCompare, WoCompareWithIdxKey) { + BSONObj obj = fromjson("{a: 1, b: 1, c: 1}"); + BSONObj objEq = fromjson("{a: 1, b: 1, c: 1}"); + BSONObj objGt = fromjson("{a: 2, b: 2, c: 2}"); + BSONObj objLt = fromjson("{a: 0, b: 0, c: 0}"); + BSONObj idxKeyAsc = fromjson("{a: 1, b: 1}"); + BSONObj idxKeyDesc = fromjson("{a: -1, b: 1}"); + BSONObj idxKeyShort = fromjson("{a: 1}"); + + ASSERT_EQ(obj.woCompare(objEq, idxKeyAsc), 0); + ASSERT_EQ(obj.woCompare(objEq, idxKeyDesc), 0); + ASSERT_EQ(obj.woCompare(objEq, idxKeyShort), 0); + ASSERT_EQ(obj.woCompare(objGt, idxKeyAsc), -1); + ASSERT_EQ(obj.woCompare(objGt, idxKeyDesc), 1); + ASSERT_EQ(obj.woCompare(objGt, idxKeyShort), -1); + ASSERT_EQ(obj.woCompare(objLt, idxKeyAsc), 1); + ASSERT_EQ(obj.woCompare(objLt, idxKeyDesc), -1); + ASSERT_EQ(obj.woCompare(objLt, idxKeyShort), 1); +} + +TEST(BSONObjCompare, UnorderedFieldsBSONObjComparison) { + BSONObj obj = fromjson("{a: {b: 1}, c: 1}"); + + UnorderedFieldsBSONObjComparator bsonCmp; + + ASSERT_TRUE(bsonCmp.evaluate(obj == fromjson("{c: 1, a: {b: 1}}"))); + ASSERT_FALSE(bsonCmp.evaluate(obj == fromjson("{a: {b: 1}, c: 1, d: 1}"))); + ASSERT_FALSE(bsonCmp.evaluate(obj == fromjson("{a: {b: 1}}"))); + ASSERT_FALSE(bsonCmp.evaluate(obj == fromjson("{a: {b: 2}, c: 1}"))); +} + +TEST(BSONObjCompare, UnorderedFieldsBSONObjHashing) { + BSONObj obj = fromjson("{a: {b: 1, c: 1}, d: 1}"); + + UnorderedFieldsBSONObjComparator bsonCmp; + + ASSERT_EQ(bsonCmp.hash(obj), bsonCmp.hash(obj)); + ASSERT_EQ(bsonCmp.hash(obj), bsonCmp.hash(fromjson("{d: 1, a: {b: 1, c: 1}}"))); + ASSERT_EQ(bsonCmp.hash(obj), bsonCmp.hash(fromjson("{a: {c: 1, b: 1}, d: 1}"))); + ASSERT_NE(bsonCmp.hash(obj), bsonCmp.hash(fromjson("{a: {b: 1, c: 1}}"))); + ASSERT_NE(bsonCmp.hash(obj), bsonCmp.hash(fromjson("{a: {b: 1, c: 1}, d: 2}"))); + ASSERT_NE(bsonCmp.hash(obj), bsonCmp.hash(fromjson("{a: {b: 1}, d: 1}"))); +} + TEST(Looping, Cpp11Basic) { int count = 0; for (BSONElement e : BSON("a" << 1 << "a" << 2 << "a" << 3)) { diff --git a/src/mongo/bson/bsonelement.cpp b/src/mongo/bson/bsonelement.cpp index de5d1e6d303..38ca7772058 100644 --- a/src/mongo/bson/bsonelement.cpp +++ b/src/mongo/bson/bsonelement.cpp @@ -321,6 +321,160 @@ int compareElementStringValues(const BSONElement& leftStr, const BSONElement& ri } // namespace +int BSONElement::compareElements(const BSONElement& l, + const BSONElement& r, + ComparisonRulesSet rules, + const StringData::ComparatorInterface* comparator) { + switch (l.type()) { + case BSONType::EOO: + case BSONType::Undefined: // EOO and Undefined are same canonicalType + case BSONType::jstNULL: + case BSONType::MaxKey: + case BSONType::MinKey: { + auto f = l.canonicalType() - r.canonicalType(); + if (f < 0) + return -1; + return f == 0 ? 0 : 1; + } + case BSONType::Bool: + return *l.value() - *r.value(); + case BSONType::bsonTimestamp: + // unsigned compare for timestamps - note they are not really dates but (ordinal + + // time_t) + if (l.timestamp() < r.timestamp()) + return -1; + return l.timestamp() == r.timestamp() ? 0 : 1; + case BSONType::Date: + // Signed comparisons for Dates. + { + const Date_t a = l.Date(); + const Date_t b = r.Date(); + if (a < b) + return -1; + return a == b ? 0 : 1; + } + + case BSONType::NumberInt: { + // All types can precisely represent all NumberInts, so it is safe to simply convert to + // whatever rhs's type is. + switch (r.type()) { + case NumberInt: + return compareInts(l._numberInt(), r._numberInt()); + case NumberLong: + return compareLongs(l._numberInt(), r._numberLong()); + case NumberDouble: + return compareDoubles(l._numberInt(), r._numberDouble()); + case NumberDecimal: + return compareIntToDecimal(l._numberInt(), r._numberDecimal()); + default: + invariant(false); + } + } + + case BSONType::NumberLong: { + switch (r.type()) { + case NumberLong: + return compareLongs(l._numberLong(), r._numberLong()); + case NumberInt: + return compareLongs(l._numberLong(), r._numberInt()); + case NumberDouble: + return compareLongToDouble(l._numberLong(), r._numberDouble()); + case NumberDecimal: + return compareLongToDecimal(l._numberLong(), r._numberDecimal()); + default: + invariant(false); + } + } + + case BSONType::NumberDouble: { + switch (r.type()) { + case NumberDouble: + return compareDoubles(l._numberDouble(), r._numberDouble()); + case NumberInt: + return compareDoubles(l._numberDouble(), r._numberInt()); + case NumberLong: + return compareDoubleToLong(l._numberDouble(), r._numberLong()); + case NumberDecimal: + return compareDoubleToDecimal(l._numberDouble(), r._numberDecimal()); + default: + invariant(false); + } + } + + case BSONType::NumberDecimal: { + switch (r.type()) { + case NumberDecimal: + return compareDecimals(l._numberDecimal(), r._numberDecimal()); + case NumberInt: + return compareDecimalToInt(l._numberDecimal(), r._numberInt()); + case NumberLong: + return compareDecimalToLong(l._numberDecimal(), r._numberLong()); + case NumberDouble: + return compareDecimalToDouble(l._numberDecimal(), r._numberDouble()); + default: + invariant(false); + } + } + + case BSONType::jstOID: + return memcmp(l.value(), r.value(), OID::kOIDSize); + case BSONType::Code: + return compareElementStringValues(l, r); + case BSONType::Symbol: + case BSONType::String: { + if (comparator) { + return comparator->compare(l.valueStringData(), r.valueStringData()); + } else { + return compareElementStringValues(l, r); + } + } + case BSONType::Object: + case BSONType::Array: { + return l.embeddedObject().woCompare( + r.embeddedObject(), + BSONObj(), + rules | BSONElement::ComparisonRules::kConsiderFieldName, + comparator); + } + case BSONType::DBRef: { + int lsz = l.valuesize(); + int rsz = r.valuesize(); + if (lsz - rsz != 0) + return lsz - rsz; + return memcmp(l.value(), r.value(), lsz); + } + case BSONType::BinData: { + int lsz = l.objsize(); // our bin data size in bytes, not including the subtype byte + int rsz = r.objsize(); + if (lsz - rsz != 0) + return lsz - rsz; + return memcmp(l.value() + 4, r.value() + 4, lsz + 1 /*+1 for subtype byte*/); + } + case BSONType::RegEx: { + int c = strcmp(l.regex(), r.regex()); + if (c) + return c; + return strcmp(l.regexFlags(), r.regexFlags()); + } + case BSONType::CodeWScope: { + int cmp = StringData(l.codeWScopeCode(), l.codeWScopeCodeLen() - 1) + .compare(StringData(r.codeWScopeCode(), r.codeWScopeCodeLen() - 1)); + if (cmp) + return cmp; + + // When comparing the scope object, we should consider field names. Special string + // comparison semantics do not apply to strings nested inside the CodeWScope scope + // object, so we do not pass through the string comparator. + return l.codeWScopeObject().woCompare( + r.codeWScopeObject(), + BSONObj(), + rules | BSONElement::ComparisonRules::kConsiderFieldName); + } + } + + MONGO_UNREACHABLE; +} + /** transform a BSON array into a vector of BSONElements. we match array # positions with their vector position, and ignore any fields with non-numeric field names. @@ -347,23 +501,20 @@ std::vector<BSONElement> BSONElement::Array() const { return v; } -/* wo = "well ordered" - note: (mongodb related) : this can only change in behavior when index version # changes -*/ -int BSONElement::woCompare(const BSONElement& e, - bool considerFieldName, +int BSONElement::woCompare(const BSONElement& elem, + ComparisonRulesSet rules, const StringData::ComparatorInterface* comparator) const { - if (type() != e.type()) { + if (type() != elem.type()) { int lt = (int)canonicalType(); - int rt = (int)e.canonicalType(); + int rt = (int)elem.canonicalType(); if (int diff = lt - rt) return diff; } - if (considerFieldName) { - if (int diff = strcmp(fieldName(), e.fieldName())) + if (rules & ComparisonRules::kConsiderFieldName) { + if (int diff = fieldNameStringData().compare(elem.fieldNameStringData())) return diff; } - return compareElementValues(*this, e, comparator); + return compareElements(*this, elem, rules, comparator); } bool BSONElement::binaryEqual(const BSONElement& rhs) const { @@ -810,159 +961,4 @@ bool BSONObj::coerceVector(std::vector<T>* out) const { return true; } -/** - * l and r must be same canonicalType when called. - */ -int compareElementValues(const BSONElement& l, - const BSONElement& r, - const StringData::ComparatorInterface* comparator) { - int f; - - switch (l.type()) { - case EOO: - case Undefined: // EOO and Undefined are same canonicalType - case jstNULL: - case MaxKey: - case MinKey: - f = l.canonicalType() - r.canonicalType(); - if (f < 0) - return -1; - return f == 0 ? 0 : 1; - case Bool: - return *l.value() - *r.value(); - case bsonTimestamp: - // unsigned compare for timestamps - note they are not really dates but (ordinal + - // time_t) - if (l.timestamp() < r.timestamp()) - return -1; - return l.timestamp() == r.timestamp() ? 0 : 1; - case Date: - // Signed comparisons for Dates. - { - const Date_t a = l.Date(); - const Date_t b = r.Date(); - if (a < b) - return -1; - return a == b ? 0 : 1; - } - - case NumberInt: { - // All types can precisely represent all NumberInts, so it is safe to simply convert to - // whatever rhs's type is. - switch (r.type()) { - case NumberInt: - return compareInts(l._numberInt(), r._numberInt()); - case NumberLong: - return compareLongs(l._numberInt(), r._numberLong()); - case NumberDouble: - return compareDoubles(l._numberInt(), r._numberDouble()); - case NumberDecimal: - return compareIntToDecimal(l._numberInt(), r._numberDecimal()); - default: - invariant(false); - } - } - - case NumberLong: { - switch (r.type()) { - case NumberLong: - return compareLongs(l._numberLong(), r._numberLong()); - case NumberInt: - return compareLongs(l._numberLong(), r._numberInt()); - case NumberDouble: - return compareLongToDouble(l._numberLong(), r._numberDouble()); - case NumberDecimal: - return compareLongToDecimal(l._numberLong(), r._numberDecimal()); - default: - invariant(false); - } - } - - case NumberDouble: { - switch (r.type()) { - case NumberDouble: - return compareDoubles(l._numberDouble(), r._numberDouble()); - case NumberInt: - return compareDoubles(l._numberDouble(), r._numberInt()); - case NumberLong: - return compareDoubleToLong(l._numberDouble(), r._numberLong()); - case NumberDecimal: - return compareDoubleToDecimal(l._numberDouble(), r._numberDecimal()); - default: - invariant(false); - } - } - - case NumberDecimal: { - switch (r.type()) { - case NumberDecimal: - return compareDecimals(l._numberDecimal(), r._numberDecimal()); - case NumberInt: - return compareDecimalToInt(l._numberDecimal(), r._numberInt()); - case NumberLong: - return compareDecimalToLong(l._numberDecimal(), r._numberLong()); - case NumberDouble: - return compareDecimalToDouble(l._numberDecimal(), r._numberDouble()); - default: - invariant(false); - } - } - - case jstOID: - return memcmp(l.value(), r.value(), OID::kOIDSize); - case Code: - return compareElementStringValues(l, r); - case Symbol: - case String: { - if (comparator) { - return comparator->compare(l.valueStringData(), r.valueStringData()); - } else { - return compareElementStringValues(l, r); - } - } - case Object: - case Array: - // woCompare parameters: r, ordering, considerFieldName, comparator. - // r: the BSONObj to compare with. - // ordering: the sort directions for each key. - // considerFieldName: whether field names should be considered in comparison. - // comparator: used for all string comparisons, if non-null. - return l.embeddedObject().woCompare(r.embeddedObject(), BSONObj(), true, comparator); - case DBRef: { - int lsz = l.valuesize(); - int rsz = r.valuesize(); - if (lsz - rsz != 0) - return lsz - rsz; - return memcmp(l.value(), r.value(), lsz); - } - case BinData: { - int lsz = l.objsize(); // our bin data size in bytes, not including the subtype byte - int rsz = r.objsize(); - if (lsz - rsz != 0) - return lsz - rsz; - return memcmp(l.value() + 4, r.value() + 4, lsz + 1 /*+1 for subtype byte*/); - } - case RegEx: { - int c = strcmp(l.regex(), r.regex()); - if (c) - return c; - return strcmp(l.regexFlags(), r.regexFlags()); - } - case CodeWScope: { - int cmp = StringData(l.codeWScopeCode(), l.codeWScopeCodeLen() - 1) - .compare(StringData(r.codeWScopeCode(), r.codeWScopeCodeLen() - 1)); - if (cmp) - return cmp; - - // When comparing the scope object, we should consider field names. Special string - // comparison semantics do not apply to strings nested inside the CodeWScope scope - // object, so we do not pass through the string comparator. - return l.codeWScopeObject().woCompare(r.codeWScopeObject(), BSONObj(), true); - } - default: - verify(false); - } - return -1; -} - } // namespace mongo diff --git a/src/mongo/bson/bsonelement.h b/src/mongo/bson/bsonelement.h index 7d58f4653aa..c86a50a0154 100644 --- a/src/mongo/bson/bsonelement.h +++ b/src/mongo/bson/bsonelement.h @@ -57,13 +57,6 @@ typedef BSONElement be; typedef BSONObj bo; typedef BSONObjBuilder bob; -/** l and r MUST have same type when called: check that first. - If comparator is non-null, it is used for all comparisons between two strings. -*/ -int compareElementValues(const BSONElement& l, - const BSONElement& r, - const StringData::ComparatorInterface* comparator = nullptr); - /** BSONElement represents an "element" in a BSONObj. So for the object { a : 3, b : "abc" }, 'a : 3' is the first element (key+value). @@ -88,6 +81,25 @@ public: */ using DeferredComparison = BSONComparatorInterfaceBase<BSONElement>::DeferredComparison; + /** + * Set of rules that dictate the behavior of the comparison APIs. + */ + using ComparisonRules = BSONComparatorInterfaceBase<BSONElement>::ComparisonRules; + using ComparisonRulesSet = BSONComparatorInterfaceBase<BSONElement>::ComparisonRulesSet; + + /** + * Compares two BSON elements of the same canonical type. + * + * Returns <0 if 'l' is less than the element 'r'. + * >0 if 'l' is greater than the element 'r'. + * 0 if 'l' is equal to the element 'r'. + */ + static int compareElements(const BSONElement& l, + const BSONElement& r, + ComparisonRulesSet rules, + const StringData::ComparatorInterface* comparator); + + /** These functions, which start with a capital letter, throw if the element is not of the required type. Example: @@ -518,14 +530,16 @@ public: */ bool binaryEqualValues(const BSONElement& rhs) const; - /** Well ordered comparison. - @return <0: l<r. 0:l==r. >0:l>r - order by type, field name, and field value. - If considerFieldName is true, pay attention to the field name. - If comparator is non-null, it is used for all comparisons between two strings. - */ - int woCompare(const BSONElement& e, - bool considerFieldName = true, + /** + * Compares two BSON Elements using the rules specified by 'rules' and the 'comparator' for + * string comparisons. + * + * Returns <0 if 'this' is less than 'elem'. + * >0 if 'this' is greater than 'elem'. + * 0 if 'this' is equal to 'elem'. + */ + int woCompare(const BSONElement& elem, + ComparisonRulesSet rules = ComparisonRules::kConsiderFieldName, const StringData::ComparatorInterface* comparator = nullptr) const; DeferredComparison operator<(const BSONElement& other) const { diff --git a/src/mongo/bson/bsonelement_comparator.h b/src/mongo/bson/bsonelement_comparator.h index 609bbb4e5bd..9125f2c37f4 100644 --- a/src/mongo/bson/bsonelement_comparator.h +++ b/src/mongo/bson/bsonelement_comparator.h @@ -55,21 +55,21 @@ public: */ BSONElementComparator(FieldNamesMode fieldNamesMode, const StringData::ComparatorInterface* stringComparator) - : _fieldNamesMode(fieldNamesMode), _stringComparator(stringComparator) {} + : _stringComparator(stringComparator), + _rules((fieldNamesMode == FieldNamesMode::kConsider) ? ComparisonRules::kConsiderFieldName + : 0) {} int compare(const BSONElement& lhs, const BSONElement& rhs) const final { - const bool considerFieldName = (_fieldNamesMode == FieldNamesMode::kConsider); - return lhs.woCompare(rhs, considerFieldName, _stringComparator); + return lhs.woCompare(rhs, _rules, _stringComparator); } void hash_combine(size_t& seed, const BSONElement& toHash) const final { - const bool considerFieldName = (_fieldNamesMode == FieldNamesMode::kConsider); - hashCombineBSONElement(seed, toHash, considerFieldName, _stringComparator); + hashCombineBSONElement(seed, toHash, _rules, _stringComparator); } private: - FieldNamesMode _fieldNamesMode; const StringData::ComparatorInterface* _stringComparator; + ComparisonRulesSet _rules; }; } // namespace mongo diff --git a/src/mongo/bson/bsonobj.cpp b/src/mongo/bson/bsonobj.cpp index 12bcb5f6747..5ec83659f63 100644 --- a/src/mongo/bson/bsonobj.cpp +++ b/src/mongo/bson/bsonobj.cpp @@ -42,7 +42,49 @@ #include "mongo/util/stringutils.h" namespace mongo { + +namespace { + +template <class ObjectIterator> +int compareObjects(const BSONObj& firstObj, + const BSONObj& secondObj, + const BSONObj& idxKey, + BSONObj::ComparisonRulesSet rules, + const StringData::ComparatorInterface* comparator) { + if (firstObj.isEmpty()) + return secondObj.isEmpty() ? 0 : -1; + if (secondObj.isEmpty()) + return 1; + + ObjectIterator firstIter(firstObj); + ObjectIterator secondIter(secondObj); + ObjectIterator idxKeyIter(idxKey); + + while (true) { + BSONElement l = firstIter.next(); + BSONElement r = secondIter.next(); + + if (l.eoo()) + return r.eoo() ? 0 : -1; + if (r.eoo()) + return 1; + + auto x = l.woCompare(r, rules, comparator); + + if (idxKeyIter.more() && idxKeyIter.next().number() < 0) + x = -x; + + if (x != 0) + return x; + } + + MONGO_UNREACHABLE; +} + +} // namespace + using namespace std; + /* BSONObj ------------------------------------------------------------*/ void BSONObj::_assertInvalid() const { @@ -104,7 +146,7 @@ bool BSONObj::valid(BSONVersion version) const { int BSONObj::woCompare(const BSONObj& r, const Ordering& o, - bool considerFieldName, + ComparisonRulesSet rules, const StringData::ComparatorInterface* comparator) const { if (isEmpty()) return r.isEmpty() ? 0 : -1; @@ -126,7 +168,7 @@ int BSONObj::woCompare(const BSONObj& r, int x; { - x = l.woCompare(r, considerFieldName, comparator); + x = l.woCompare(r, rules, comparator); if (o.descending(mask)) x = -x; } @@ -140,47 +182,11 @@ int BSONObj::woCompare(const BSONObj& r, /* well ordered compare */ int BSONObj::woCompare(const BSONObj& r, const BSONObj& idxKey, - bool considerFieldName, + ComparisonRulesSet rules, const StringData::ComparatorInterface* comparator) const { - if (isEmpty()) - return r.isEmpty() ? 0 : -1; - if (r.isEmpty()) - return 1; - - bool ordered = !idxKey.isEmpty(); - - BSONObjIterator i(*this); - BSONObjIterator j(r); - BSONObjIterator k(idxKey); - while (1) { - // so far, equal... - - BSONElement l = i.next(); - BSONElement r = j.next(); - BSONElement o; - if (ordered) - o = k.next(); - if (l.eoo()) - return r.eoo() ? 0 : -1; - if (r.eoo()) - return 1; - - int x; - /* - if( ordered && o.type() == String && strcmp(o.valuestr(), "ascii-proto") == 0 && - l.type() == String && r.type() == String ) { - // note: no negative support yet, as this is just sort of a POC - x = _stricmp(l.valuestr(), r.valuestr()); - } - else*/ { - x = l.woCompare(r, considerFieldName, comparator); - if (ordered && o.number() < 0) - x = -x; - } - if (x != 0) - return x; - } - return -1; + return (rules & ComparisonRules::kIgnoreFieldOrder) + ? compareObjects<BSONObjIteratorSorted>(*this, r, idxKey, rules, comparator) + : compareObjects<BSONObjIterator>(*this, r, idxKey, rules, comparator); } bool BSONObj::isPrefixOf(const BSONObj& otherObj, diff --git a/src/mongo/bson/bsonobj.h b/src/mongo/bson/bsonobj.h index a6d7abea953..6898c63a840 100644 --- a/src/mongo/bson/bsonobj.h +++ b/src/mongo/bson/bsonobj.h @@ -102,6 +102,12 @@ public: */ using DeferredComparison = BSONComparatorInterfaceBase<BSONObj>::DeferredComparison; + /** + * Set of rules that dictate the behavior of the comparison APIs. + */ + using ComparisonRules = BSONComparatorInterfaceBase<BSONObj>::ComparisonRules; + using ComparisonRulesSet = BSONComparatorInterfaceBase<BSONObj>::ComparisonRulesSet; + static const char kMinBSONLength = 5; /** Construct an empty BSONObj -- that is, {}. */ @@ -393,26 +399,22 @@ public: // See bsonobj_comparator_interface.h for details. // - /**wo='well ordered'. fields must be in same order in each object. - Ordering is with respect to the signs of the elements - and allows ascending / descending key mixing. - If comparator is non-null, it is used for all comparisons between two strings. - @return <0 if l<r. 0 if l==r. >0 if l>r - */ + /** + * Compares two BSON Objects using the rules specified by 'rules', 'comparator' for + * string comparisons, and 'o' for ascending vs. descending ordering. + * + * Returns <0 if 'this' is less than 'obj'. + * >0 if 'this' is greater than 'obj'. + * 0 if 'this' is equal to 'obj'. + */ int woCompare(const BSONObj& r, const Ordering& o, - bool considerFieldName = true, + ComparisonRulesSet rules = ComparisonRules::kConsiderFieldName, const StringData::ComparatorInterface* comparator = nullptr) const; - /**wo='well ordered'. fields must be in same order in each object. - Ordering is with respect to the signs of the elements - and allows ascending / descending key mixing. - If comparator is non-null, it is used for all comparisons between two strings. - @return <0 if l<r. 0 if l==r. >0 if l>r - */ int woCompare(const BSONObj& r, const BSONObj& ordering = BSONObj(), - bool considerFieldName = true, + ComparisonRulesSet rules = ComparisonRules::kConsiderFieldName, const StringData::ComparatorInterface* comparator = nullptr) const; DeferredComparison operator<(const BSONObj& other) const { diff --git a/src/mongo/bson/bsonobj_comparator.h b/src/mongo/bson/bsonobj_comparator.h index fac94464e94..a50c5cd3b36 100644 --- a/src/mongo/bson/bsonobj_comparator.h +++ b/src/mongo/bson/bsonobj_comparator.h @@ -58,23 +58,22 @@ public: FieldNamesMode fieldNamesMode, const StringData::ComparatorInterface* stringComparator) : _ordering(std::move(ordering)), - _fieldNamesMode(fieldNamesMode), - _stringComparator(stringComparator) {} + _stringComparator(stringComparator), + _rules((fieldNamesMode == FieldNamesMode::kConsider) ? ComparisonRules::kConsiderFieldName + : 0) {} int compare(const BSONObj& lhs, const BSONObj& rhs) const final { - const bool considerFieldName = (_fieldNamesMode == FieldNamesMode::kConsider); - return lhs.woCompare(rhs, _ordering, considerFieldName, _stringComparator); + return lhs.woCompare(rhs, _ordering, _rules, _stringComparator); } void hash_combine(size_t& seed, const BSONObj& toHash) const final { - const bool considerFieldName = (_fieldNamesMode == FieldNamesMode::kConsider); - hashCombineBSONObj(seed, toHash, considerFieldName, _stringComparator); + hashCombineBSONObj(seed, toHash, _rules, _stringComparator); } private: BSONObj _ordering; - FieldNamesMode _fieldNamesMode; const StringData::ComparatorInterface* _stringComparator; + ComparisonRulesSet _rules; }; } // namespace mongo diff --git a/src/mongo/bson/simple_bsonelement_comparator.h b/src/mongo/bson/simple_bsonelement_comparator.h index b05204c3f50..8fafbc3659f 100644 --- a/src/mongo/bson/simple_bsonelement_comparator.h +++ b/src/mongo/bson/simple_bsonelement_comparator.h @@ -43,12 +43,11 @@ public: static const SimpleBSONElementComparator kInstance; int compare(const BSONElement& lhs, const BSONElement& rhs) const final { - return lhs.woCompare(rhs, true, nullptr); + return lhs.woCompare(rhs, ComparisonRules::kConsiderFieldName, nullptr); } void hash_combine(size_t& seed, const BSONElement& toHash) const final { - const bool considerFieldName = true; - hashCombineBSONElement(seed, toHash, considerFieldName, nullptr); + hashCombineBSONElement(seed, toHash, ComparisonRules::kConsiderFieldName, nullptr); } }; diff --git a/src/mongo/bson/simple_bsonobj_comparator.h b/src/mongo/bson/simple_bsonobj_comparator.h index 2e88586746f..2202e221696 100644 --- a/src/mongo/bson/simple_bsonobj_comparator.h +++ b/src/mongo/bson/simple_bsonobj_comparator.h @@ -42,12 +42,11 @@ public: static const SimpleBSONObjComparator kInstance; int compare(const BSONObj& lhs, const BSONObj& rhs) const final { - return lhs.woCompare(rhs, BSONObj(), true, nullptr); + return lhs.woCompare(rhs, BSONObj(), ComparisonRules::kConsiderFieldName, nullptr); } void hash_combine(size_t& seed, const BSONObj& toHash) const final { - const bool considerFieldName = true; - hashCombineBSONObj(seed, toHash, considerFieldName, nullptr); + hashCombineBSONObj(seed, toHash, ComparisonRules::kConsiderFieldName, nullptr); } }; diff --git a/src/mongo/bson/unordered_fields_bsonelement_comparator.h b/src/mongo/bson/unordered_fields_bsonelement_comparator.h new file mode 100644 index 00000000000..23bbe8ed4f8 --- /dev/null +++ b/src/mongo/bson/unordered_fields_bsonelement_comparator.h @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include "mongo/base/string_data_comparator_interface.h" +#include "mongo/bson/bsonelement_comparator_interface.h" + +namespace mongo { + +/** + * A BSONElement comparator that supports unordered element comparisons for objects. Does not + * support using a non-simple string comparator. + */ +class UnorderedFieldsBSONElementComparator final : public BSONElement::ComparatorInterface { +public: + static constexpr StringData::ComparatorInterface* kStringComparator = nullptr; + + UnorderedFieldsBSONElementComparator() = default; + + int compare(const BSONElement& lhs, const BSONElement& rhs) const final { + return lhs.woCompare(rhs, ComparisonRules::kIgnoreFieldOrder, kStringComparator); + } + + void hash_combine(size_t& seed, const BSONElement& toHash) const final { + hashCombineBSONElement(seed, toHash, ComparisonRules::kIgnoreFieldOrder, kStringComparator); + } +}; + +} // namespace mongo diff --git a/src/mongo/bson/unordered_fields_bsonobj_comparator.h b/src/mongo/bson/unordered_fields_bsonobj_comparator.h new file mode 100644 index 00000000000..248331407f0 --- /dev/null +++ b/src/mongo/bson/unordered_fields_bsonobj_comparator.h @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include "mongo/base/string_data_comparator_interface.h" +#include "mongo/bson/bsonobj_comparator_interface.h" + +namespace mongo { + +/** + * A BSONObj comparator that supports unordered element comparison. Does not support using a + * non-simple string comparator. + */ +class UnorderedFieldsBSONObjComparator final : public BSONObj::ComparatorInterface { +public: + static constexpr StringData::ComparatorInterface* kStringComparator = nullptr; + + UnorderedFieldsBSONObjComparator() = default; + + int compare(const BSONObj& lhs, const BSONObj& rhs) const final { + return lhs.woCompare(rhs, + BSONObj(), + ComparisonRules::kIgnoreFieldOrder | + ComparisonRules::kConsiderFieldName, + kStringComparator); + } + + void hash_combine(size_t& seed, const BSONObj& toHash) const final { + hashCombineBSONObj(seed, + toHash, + ComparisonRules::kIgnoreFieldOrder | ComparisonRules::kConsiderFieldName, + kStringComparator); + } +}; +} // namespace mongo diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index e1b4008f208..f3096cfc1c1 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -52,11 +52,13 @@ env.Library( 'schema/expression_internal_schema_all_elem_match_from_index.cpp', 'schema/expression_internal_schema_allowed_properties.cpp', 'schema/expression_internal_schema_cond.cpp', + 'schema/expression_internal_schema_eq.cpp', 'schema/expression_internal_schema_fmod.cpp', 'schema/expression_internal_schema_match_array_index.cpp', 'schema/expression_internal_schema_num_array_items.cpp', 'schema/expression_internal_schema_num_properties.cpp', 'schema/expression_internal_schema_object_match.cpp', + 'schema/expression_internal_schema_root_doc_eq.cpp', 'schema/expression_internal_schema_str_length.cpp', 'schema/expression_internal_schema_unique_items.cpp', 'schema/expression_internal_schema_xor.cpp', @@ -94,6 +96,7 @@ env.CppUnitTest( 'schema/expression_internal_schema_all_elem_match_from_index_test.cpp', 'schema/expression_internal_schema_allowed_properties_test.cpp', 'schema/expression_internal_schema_cond_test.cpp', + 'schema/expression_internal_schema_eq_test.cpp', 'schema/expression_internal_schema_fmod_test.cpp', 'schema/expression_internal_schema_match_array_index_test.cpp', 'schema/expression_internal_schema_max_items_test.cpp', @@ -103,6 +106,7 @@ env.CppUnitTest( 'schema/expression_internal_schema_min_length_test.cpp', 'schema/expression_internal_schema_min_properties_test.cpp', 'schema/expression_internal_schema_object_match_test.cpp', + 'schema/expression_internal_schema_root_doc_eq_test.cpp', 'schema/expression_internal_schema_unique_items_test.cpp', 'schema/expression_internal_schema_xor_test.cpp', ], diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h index a7e89a59c9e..768c265d3ea 100644 --- a/src/mongo/db/matcher/expression.h +++ b/src/mongo/db/matcher/expression.h @@ -104,6 +104,7 @@ public: INTERNAL_SCHEMA_ALLOWED_PROPERTIES, INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX, INTERNAL_SCHEMA_COND, + INTERNAL_SCHEMA_EQ, INTERNAL_SCHEMA_FMOD, INTERNAL_SCHEMA_MATCH_ARRAY_INDEX, INTERNAL_SCHEMA_MAX_ITEMS, @@ -113,6 +114,7 @@ public: INTERNAL_SCHEMA_MIN_LENGTH, INTERNAL_SCHEMA_MIN_PROPERTIES, INTERNAL_SCHEMA_OBJECT_MATCH, + INTERNAL_SCHEMA_ROOT_DOC_EQ, INTERNAL_SCHEMA_TYPE, INTERNAL_SCHEMA_UNIQUE_ITEMS, INTERNAL_SCHEMA_XOR, diff --git a/src/mongo/db/matcher/expression_algo.cpp b/src/mongo/db/matcher/expression_algo.cpp index 609eef099e6..3bd30006f00 100644 --- a/src/mongo/db/matcher/expression_algo.cpp +++ b/src/mongo/db/matcher/expression_algo.cpp @@ -89,9 +89,10 @@ bool _isSubsetOf(const ComparisonMatchExpression* lhs, const ComparisonMatchExpr return false; } - // Either collator may be used by compareElementValues() here, since either the collators are + // Either collator may be used by compareElements() here, since either the collators are // the same or lhsData does not contain string comparison. - int cmp = compareElementValues(lhsData, rhsData, rhs->getCollator()); + int cmp = BSONElement::compareElements( + lhsData, rhsData, BSONElement::ComparisonRules::kConsiderFieldName, rhs->getCollator()); // Check whether the two expressions are equivalent. if (lhs->matchType() == rhs->matchType() && cmp == 0) { diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp index 56238d995b4..6a3502566e8 100644 --- a/src/mongo/db/matcher/expression_leaf.cpp +++ b/src/mongo/db/matcher/expression_leaf.cpp @@ -122,7 +122,8 @@ bool ComparisonMatchExpression::matchesSingleElement(const BSONElement& e, } } - int x = compareElementValues(e, _rhs, _collator); + int x = BSONElement::compareElements( + e, _rhs, BSONElement::ComparisonRules::kConsiderFieldName, _collator); switch (matchType()) { case LT: diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index 50747bebfbd..b1dec5b6a33 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -50,6 +50,7 @@ #include "mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h" #include "mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h" #include "mongo/db/matcher/schema/expression_internal_schema_cond.h" +#include "mongo/db/matcher/schema/expression_internal_schema_eq.h" #include "mongo/db/matcher/schema/expression_internal_schema_fmod.h" #include "mongo/db/matcher/schema/expression_internal_schema_match_array_index.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" @@ -59,6 +60,7 @@ #include "mongo/db/matcher/schema/expression_internal_schema_min_length.h" #include "mongo/db/matcher/schema/expression_internal_schema_min_properties.h" #include "mongo/db/matcher/schema/expression_internal_schema_object_match.h" +#include "mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h" #include "mongo/db/matcher/schema/expression_internal_schema_unique_items.h" #include "mongo/db/matcher/schema/expression_internal_schema_xor.h" #include "mongo/db/matcher/schema/json_schema_parser.h" @@ -458,6 +460,15 @@ StatusWithMatchExpression MatchExpressionParser::_parseSubField( case PathAcceptingKeyword::INTERNAL_SCHEMA_TYPE: { return _parseType<InternalSchemaTypeExpression>(name, e); } + + case PathAcceptingKeyword::INTERNAL_SCHEMA_EQ: { + auto eqExpr = stdx::make_unique<InternalSchemaEqMatchExpression>(); + auto status = eqExpr->init(name, e); + if (!status.isOK()) { + return status; + } + return {std::move(eqExpr)}; + } } return {Status(ErrorCodes::BadValue, @@ -639,6 +650,22 @@ StatusWithMatchExpression MatchExpressionParser::_parse( auto alwaysTrueExpr = stdx::make_unique<AlwaysTrueMatchExpression>(); root->add(alwaysTrueExpr.release()); + } else if (mongoutils::str::equals("_internalSchemaRootDocEq", rest)) { + if (!topLevel) { + return {Status(ErrorCodes::FailedToParse, + str::stream() << InternalSchemaRootDocEqMatchExpression::kName + << " must be at the top level")}; + } + + if (e.type() != BSONType::Object) { + return {Status(ErrorCodes::TypeMismatch, + str::stream() << InternalSchemaRootDocEqMatchExpression::kName + << " must be an object, found type " + << e.type())}; + } + auto rootDocEq = stdx::make_unique<InternalSchemaRootDocEqMatchExpression>(); + rootDocEq->init(e.embeddedObject()); + root->add(rootDocEq.release()); } else { return {Status(ErrorCodes::BadValue, mongoutils::str::stream() << "unknown top level operator: " @@ -1704,12 +1731,12 @@ MONGO_INITIALIZER(MatchExpressionParser)(InitializerContext* context) { // TODO: SERVER-19565 Add $eq after auditing callers. {"_internalSchemaAllElemMatchFromIndex", PathAcceptingKeyword::INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX}, + {"_internalSchemaEq", PathAcceptingKeyword::INTERNAL_SCHEMA_EQ}, {"_internalSchemaFmod", PathAcceptingKeyword::INTERNAL_SCHEMA_FMOD}, {"_internalSchemaMatchArrayIndex", PathAcceptingKeyword::INTERNAL_SCHEMA_MATCH_ARRAY_INDEX}, {"_internalSchemaMaxItems", PathAcceptingKeyword::INTERNAL_SCHEMA_MAX_ITEMS}, {"_internalSchemaMaxLength", PathAcceptingKeyword::INTERNAL_SCHEMA_MAX_LENGTH}, - {"_internalSchemaMaxLength", PathAcceptingKeyword::INTERNAL_SCHEMA_MAX_LENGTH}, {"_internalSchemaMinItems", PathAcceptingKeyword::INTERNAL_SCHEMA_MIN_ITEMS}, {"_internalSchemaMinItems", PathAcceptingKeyword::INTERNAL_SCHEMA_MIN_ITEMS}, {"_internalSchemaMinLength", PathAcceptingKeyword::INTERNAL_SCHEMA_MIN_LENGTH}, diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h index f69eafbff12..095bd79212d 100644 --- a/src/mongo/db/matcher/expression_parser.h +++ b/src/mongo/db/matcher/expression_parser.h @@ -61,6 +61,7 @@ enum class PathAcceptingKeyword { GREATER_THAN, GREATER_THAN_OR_EQUAL, INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX, + INTERNAL_SCHEMA_EQ, INTERNAL_SCHEMA_FMOD, INTERNAL_SCHEMA_MATCH_ARRAY_INDEX, INTERNAL_SCHEMA_MAX_ITEMS, diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp index 4ae5a56848d..1971087ed30 100644 --- a/src/mongo/db/matcher/expression_serialization_test.cpp +++ b/src/mongo/db/matcher/expression_serialization_test.cpp @@ -1747,5 +1747,21 @@ TEST(SerializeInternalSchema, }})")); ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); } + +TEST(SerializeInternalSchema, ExpressionInternalSchemaEqSerializesCorrectly) { + Matcher original(fromjson("{x: {$_internalSchemaEq: {y: 1}}}"), kSimpleCollator); + Matcher reserialized(serialize(original.getMatchExpression()), kSimpleCollator); + + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), fromjson("{x: {$_internalSchemaEq: {y: 1}}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); +} + +TEST(SerializeInternalSchema, ExpressionInternalSchemaRootDocEqSerializesCorrectly) { + Matcher original(fromjson("{$_internalSchemaRootDocEq: {y: 1}}"), kSimpleCollator); + Matcher reserialized(serialize(original.getMatchExpression()), kSimpleCollator); + + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), fromjson("{$_internalSchemaRootDocEq: {y: 1}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); +} } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp new file mode 100644 index 00000000000..20de5d876e9 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/matcher/schema/expression_internal_schema_eq.h" + +#include "mongo/bson/bsonmisc.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" + +namespace mongo { + +constexpr StringData InternalSchemaEqMatchExpression::kName; + +Status InternalSchemaEqMatchExpression::init(StringData path, BSONElement rhs) { + invariant(rhs); + _rhsElem = rhs; + return setPath(path); +} + +bool InternalSchemaEqMatchExpression::matchesSingleElement(const BSONElement& elem, + MatchDetails* details) const { + return _eltCmp.evaluate(_rhsElem == elem); +} + +void InternalSchemaEqMatchExpression::debugString(StringBuilder& debug, int level) const { + _debugAddSpace(debug, level); + debug << path() << " " << kName << " " << _rhsElem.toString(false); + + auto td = getTag(); + if (td) { + debug << " "; + td->debugString(&debug); + } + + debug << "\n"; +} + +void InternalSchemaEqMatchExpression::serialize(BSONObjBuilder* out) const { + BSONObjBuilder eqObj(out->subobjStart(path())); + eqObj.appendAs(_rhsElem, kName); + eqObj.doneFast(); +} + +bool InternalSchemaEqMatchExpression::equivalent(const MatchExpression* other) const { + if (other->matchType() != matchType()) { + return false; + } + + auto realOther = static_cast<const InternalSchemaEqMatchExpression*>(other); + return path() == realOther->path() && _eltCmp.evaluate(_rhsElem == realOther->_rhsElem); +} + +std::unique_ptr<MatchExpression> InternalSchemaEqMatchExpression::shallowClone() const { + auto clone = stdx::make_unique<InternalSchemaEqMatchExpression>(); + invariantOK(clone->init(path(), _rhsElem)); + if (getTag()) { + clone->setTag(getTag()->clone()); + } + return std::move(clone); +} + +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_eq.h new file mode 100644 index 00000000000..7905671c7a8 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq.h @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include "mongo/bson/unordered_fields_bsonelement_comparator.h" +#include "mongo/db/matcher/expression_leaf.h" + +namespace mongo { + +/** + * MatchExpression for $_internalSchemaEq, which behaves similar to $eq except: + * + * - leaf arrays are not traversed. + * - comparisons between objects do not consider field order. + * - null element values only match the literal null, and not missing or undefined values. + * - always uses simple string comparison semantics, even if the query has a non-simple collation. + */ +class InternalSchemaEqMatchExpression final : public LeafMatchExpression { +public: + static constexpr StringData kName = "$_internalSchemaEq"_sd; + + InternalSchemaEqMatchExpression() : LeafMatchExpression(MatchType::INTERNAL_SCHEMA_EQ) {} + + Status init(StringData path, BSONElement rhs); + + std::unique_ptr<MatchExpression> shallowClone() const final; + + bool matchesSingleElement(const BSONElement&, MatchDetails*) const final; + + void debugString(StringBuilder& debug, int level) const final; + + void serialize(BSONObjBuilder* out) const final; + + bool equivalent(const MatchExpression* other) const final; + + size_t numChildren() const final { + return 0; + } + + MatchExpression* getChild(size_t i) const final { + MONGO_UNREACHABLE; + } + + std::vector<MatchExpression*>* getChildVector() final { + return nullptr; + } + + bool shouldExpandLeafArray() const final { + return false; + } + +private: + UnorderedFieldsBSONElementComparator _eltCmp; + BSONElement _rhsElem; +}; +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_eq_test.cpp new file mode 100644 index 00000000000..f82f0b34bf0 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq_test.cpp @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/json.h" +#include "mongo/db/matcher/matcher.h" +#include "mongo/db/matcher/schema/expression_internal_schema_eq.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +TEST(InternalSchemaEqMatchExpression, CorrectlyMatchesScalarElements) { + BSONObj numberOperand = BSON("a" << 5); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", numberOperand["a"])); + ASSERT_TRUE(eq.matchesBSON(BSON("a" << 5.0))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" << 6))); + + BSONObj stringOperand = BSON("a" + << "str"); + + ASSERT_OK(eq.init("a", stringOperand["a"])); + ASSERT_TRUE(eq.matchesBSON(BSON("a" + << "str"))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" + << "string"))); +} + +TEST(InternalSchemaEqMatchExpression, CorrectlyMatchesArrayElement) { + BSONObj operand = BSON("a" << BSON_ARRAY("b" << 5)); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", operand["a"])); + ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON_ARRAY("b" << 5)))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" << BSON_ARRAY(5 << "b")))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" << BSON_ARRAY("b" << 5 << 5)))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" << BSON_ARRAY("b" << 6)))); +} + +TEST(InternalSchemaEqMatchExpression, CorrectlyMatchesNullElement) { + BSONObj operand = BSON("a" << BSONNULL); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", operand["a"])); + ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSONNULL))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" << 4))); +} + +TEST(InternalSchemaEqMatchExpression, NullElementDoesNotMatchMissing) { + BSONObj operand = BSON("a" << BSONNULL); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", operand["a"])); + ASSERT_FALSE(eq.matchesBSON(BSONObj())); + ASSERT_FALSE(eq.matchesBSON(BSON("b" << 4))); +} + +TEST(InternalSchemaEqMatchExpression, NullElementDoesNotMatchUndefinedOrMissing) { + BSONObj operand = BSON("a" << BSONNULL); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", operand["a"])); + ASSERT_FALSE(eq.matchesBSON(BSONObj())); + ASSERT_FALSE(eq.matchesBSON(fromjson("{a: undefined}"))); +} + +TEST(InternalSchemaEqMatchExpression, DoesNotTraverseLeafArrays) { + BSONObj operand = BSON("a" << 5); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", operand["a"])); + ASSERT_TRUE(eq.matchesBSON(BSON("a" << 5.0))); + ASSERT_FALSE(eq.matchesBSON(BSON("a" << BSON_ARRAY(5)))); +} + +TEST(InternalSchemaEqMatchExpression, MatchesObjectsIndependentOfFieldOrder) { + BSONObj operand = fromjson("{a: {b: 1, c: {d: 2, e: 3}}}"); + + InternalSchemaEqMatchExpression eq; + ASSERT_OK(eq.init("a", operand["a"])); + ASSERT_TRUE(eq.matchesBSON(fromjson("{a: {b: 1, c: {d: 2, e: 3}}}"))); + ASSERT_TRUE(eq.matchesBSON(fromjson("{a: {c: {e: 3, d: 2}, b: 1}}"))); + ASSERT_FALSE(eq.matchesBSON(fromjson("{a: {b: 1, c: {d: 2}, e: 3}}"))); + ASSERT_FALSE(eq.matchesBSON(fromjson("{a: {b: 2, c: {d: 2}}}"))); + ASSERT_FALSE(eq.matchesBSON(fromjson("{a: {b: 1}}"))); +} + +TEST(InternalSchemaEqMatchExpression, EquivalentReturnsCorrectResults) { + auto query = fromjson(R"( + {a: {$_internalSchemaEq: { + b: {c: 1, d: 1} + }}})"); + Matcher eqExpr(query, nullptr); + + query = fromjson(R"( + {a: {$_internalSchemaEq: { + b: {d: 1, c: 1} + }}})"); + Matcher eqExprEq(query, nullptr); + ASSERT_TRUE(eqExpr.getMatchExpression()->equivalent(eqExprEq.getMatchExpression())); + + query = fromjson(R"( + {a: {$_internalSchemaEq: { + b: {d: 1} + }}})"); + Matcher eqExprNotEq(query, nullptr); + ASSERT_FALSE(eqExpr.getMatchExpression()->equivalent(eqExprNotEq.getMatchExpression())); +} + +TEST(InternalSchemaEqMatchExpression, EquivalentToClone) { + auto query = fromjson("{a: {$_internalSchemaEq: {a:1, b: {c: 1, d: [1]}}}}"); + Matcher rootDocEq(query, nullptr); + auto clone = rootDocEq.getMatchExpression()->shallowClone(); + ASSERT_TRUE(rootDocEq.getMatchExpression()->equivalent(clone.get())); +} +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp new file mode 100644 index 00000000000..c0a5bbf8922 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h" + +namespace mongo { + +constexpr StringData InternalSchemaRootDocEqMatchExpression::kName; + +bool InternalSchemaRootDocEqMatchExpression::matches(const MatchableDocument* doc, + MatchDetails* details) const { + return _objCmp.evaluate(doc->toBSON() == _rhsObj); +} + +void InternalSchemaRootDocEqMatchExpression::debugString(StringBuilder& debug, int level) const { + _debugAddSpace(debug, level); + debug << kName << " " << _rhsObj.toString(); + + auto td = getTag(); + if (td) { + debug << " "; + td->debugString(&debug); + } + + debug << "\n"; +} + +void InternalSchemaRootDocEqMatchExpression::serialize(BSONObjBuilder* out) const { + BSONObjBuilder subObj(out->subobjStart(kName)); + subObj.appendElements(_rhsObj); + subObj.doneFast(); +} + +bool InternalSchemaRootDocEqMatchExpression::equivalent(const MatchExpression* other) const { + if (matchType() != other->matchType()) { + return false; + } + + auto realOther = static_cast<const InternalSchemaRootDocEqMatchExpression*>(other); + return _objCmp.evaluate(_rhsObj == realOther->_rhsObj); +} + +std::unique_ptr<MatchExpression> InternalSchemaRootDocEqMatchExpression::shallowClone() const { + auto clone = stdx::make_unique<InternalSchemaRootDocEqMatchExpression>(); + clone->init(_rhsObj); + if (getTag()) { + clone->setTag(getTag()->clone()); + } + return std::move(clone); +} + +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h new file mode 100644 index 00000000000..0255f39236e --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include "mongo/bson/unordered_fields_bsonobj_comparator.h" +#include "mongo/db/matcher/expression.h" + +namespace mongo { + +/** + * MatchExpression for $_internalSchemaRootDocEq, which matches the root document with the + * following equality semantics: + * + * - comparisons between objects do not consider field order. + * - null element values only match the literal null, and not missing or undefined values. + * - always uses simple string comparison semantics, even if the query has a non-simple collation. + */ +class InternalSchemaRootDocEqMatchExpression final : public MatchExpression { +public: + static constexpr StringData kName = "$_internalSchemaRootDocEq"_sd; + + InternalSchemaRootDocEqMatchExpression() + : MatchExpression(MatchExpression::INTERNAL_SCHEMA_ROOT_DOC_EQ) {} + + void init(BSONObj obj) { + _rhsObj = std::move(obj); + } + + bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final; + + /** + * This expression should only be used to match full documents, not objects within an array + * in the case of $elemMatch. + */ + bool matchesSingleElement(const BSONElement& elem, + MatchDetails* details = nullptr) const final { + MONGO_UNREACHABLE; + } + + std::unique_ptr<MatchExpression> shallowClone() const final; + + void debugString(StringBuilder& debug, int level = 0) const final; + + void serialize(BSONObjBuilder* out) const final; + + bool equivalent(const MatchExpression* other) const final; + + size_t numChildren() const final { + return 0; + } + + MatchExpression* getChild(size_t i) const final { + MONGO_UNREACHABLE; + } + + std::vector<MatchExpression*>* getChildVector() final { + return nullptr; + } + + MatchCategory getCategory() const final { + return MatchCategory::kOther; + } + +private: + UnorderedFieldsBSONObjComparator _objCmp; + BSONObj _rhsObj; +}; +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq_test.cpp new file mode 100644 index 00000000000..4b46779b173 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq_test.cpp @@ -0,0 +1,119 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/json.h" +#include "mongo/db/matcher/matcher.h" +#include "mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesObject) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(BSON("a" << 1 << "b" + << "string")); + ASSERT_TRUE(rootDocEq.matchesBSON(BSON("a" << 1 << "b" + << "string"))); + ASSERT_FALSE(rootDocEq.matchesBSON(BSON("a" << 2 << "b" + << "string"))); +} + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesNestedObject) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(BSON("a" << 1 << "b" << BSON("c" << 1))); + ASSERT_TRUE(rootDocEq.matchesBSON(BSON("a" << 1 << "b" << BSON("c" << 1)))); + ASSERT_FALSE(rootDocEq.matchesBSON(BSON("a" << 1 << "b" << BSON("c" << 2)))); +} + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesObjectIgnoresElementOrder) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(BSON("a" << 1 << "b" << BSON("c" << 1))); + ASSERT_TRUE(rootDocEq.matchesBSON(BSON("b" << BSON("c" << 1) << "a" << 1))); +} + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesNestedObjectIgnoresElementOrder) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(BSON("a" << BSON("b" << 1 << "c" << 1))); + ASSERT_TRUE(rootDocEq.matchesBSON(BSON("a" << BSON("c" << 1 << "b" << 1)))); +} + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesEmptyObject) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(BSONObj()); + ASSERT_TRUE(rootDocEq.matchesBSON(BSONObj())); +} + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesNestedArray) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(BSON("a" << BSON_ARRAY(1 << 2 << 3))); + ASSERT_TRUE(rootDocEq.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 3)))); + ASSERT_FALSE(rootDocEq.matchesBSON(BSON("a" << BSON_ARRAY(1 << 3 << 2)))); +} + +TEST(InternalSchemaRootDocEqMatchExpression, MatchesObjectWithNullElement) { + InternalSchemaRootDocEqMatchExpression rootDocEq; + rootDocEq.init(fromjson("{a: null}")); + ASSERT_TRUE(rootDocEq.matchesBSON(fromjson("{a: null}"))); + ASSERT_FALSE(rootDocEq.matchesBSON(fromjson("{a: 1}"))); + ASSERT_FALSE(rootDocEq.matchesBSON(fromjson("{}"))); + ASSERT_FALSE(rootDocEq.matchesBSON(fromjson("{a: undefined}"))); +} + +TEST(InternalSchemaRootDocEqMatchExpression, EquivalentReturnsCorrectResults) { + auto query = fromjson(R"( + {$_internalSchemaRootDocEq: { + b: 1, c: 1 + }})"); + Matcher rootDocEq(query, nullptr); + + query = fromjson(R"( + {$_internalSchemaRootDocEq: { + c: 1, b: 1 + }})"); + Matcher exprEq(query, nullptr); + ASSERT_TRUE(rootDocEq.getMatchExpression()->equivalent(exprEq.getMatchExpression())); + + query = fromjson(R"( + {$_internalSchemaRootDocEq: { + c: 1 + }})"); + Matcher exprNotEq(query, nullptr); + ASSERT_FALSE(rootDocEq.getMatchExpression()->equivalent(exprNotEq.getMatchExpression())); +} + +TEST(InternalSchemaRootDocEqMatchExpression, EquivalentToClone) { + auto query = fromjson("{$_internalSchemaRootDocEq: {a:1, b: {c: 1, d: [1]}}}"); + Matcher rootDocEq(query, nullptr); + auto clone = rootDocEq.getMatchExpression()->shallowClone(); + ASSERT_TRUE(rootDocEq.getMatchExpression()->equivalent(clone.get())); +} +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp index 4b6c8009df6..154e395a125 100644 --- a/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp +++ b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp @@ -808,5 +808,47 @@ TEST(MatchExpressionParserSchemaTest, AllowedPropertiesAcceptsEmptyOtherwiseExpr ASSERT_TRUE(allowedProperties.getValue()->matchesBSON(BSONObj())); } +TEST(MatchExpressionParserSchemaTest, EqParsesSuccessfully) { + auto query = fromjson("{foo: {$_internalSchemaEq: 1}}"); + auto eqExpr = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_OK(eqExpr.getStatus()); + + ASSERT_TRUE(eqExpr.getValue()->matchesBSON(fromjson("{foo: 1}"))); + ASSERT_FALSE(eqExpr.getValue()->matchesBSON(fromjson("{foo: [1]}"))); + ASSERT_FALSE(eqExpr.getValue()->matchesBSON(fromjson("{not_foo: 1}"))); +} + +TEST(MatchExpressionParserSchemaTest, RootDocEqFailsToParseNonObjects) { + auto query = fromjson("{$_internalSchemaRootDocEq: 1}"); + auto rootDocEq = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_EQ(rootDocEq.getStatus(), ErrorCodes::TypeMismatch); + + query = fromjson("{$_internalSchemaRootDocEq: [{}]}"); + rootDocEq = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_EQ(rootDocEq.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(MatchExpressionParserSchemaTest, RootDocEqMustBeTopLevel) { + auto query = fromjson("{a: {$_internalSchemaRootDocEq: 1}}"); + auto rootDocEq = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_EQ(rootDocEq.getStatus(), ErrorCodes::BadValue); + + query = fromjson("{$or: [{a: 1}, {$_internalSchemaRootDocEq: 1}]}"); + rootDocEq = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_EQ(rootDocEq.getStatus(), ErrorCodes::FailedToParse); + + query = fromjson("{a: {$elemMatch: {$_internalSchemaRootDocEq: 1}}}"); + rootDocEq = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_EQ(rootDocEq.getStatus(), ErrorCodes::BadValue); +} + +TEST(MatchExpressionParserSchemaTest, RootDocEqParsesSuccessfully) { + auto query = fromjson("{$_internalSchemaRootDocEq: {}}"); + auto eqExpr = MatchExpressionParser::parse(query, kSimpleCollator); + ASSERT_OK(eqExpr.getStatus()); + + ASSERT_TRUE(eqExpr.getValue()->matchesBSON(fromjson("{}"))); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index e0b4d78a168..8c107f0b759 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -35,12 +35,14 @@ #include <boost/container/flat_set.hpp> #include "mongo/bson/bsontypes.h" +#include "mongo/bson/unordered_fields_bsonelement_comparator.h" #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/matcher/matcher_type_set.h" #include "mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h" #include "mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h" #include "mongo/db/matcher/schema/expression_internal_schema_cond.h" +#include "mongo/db/matcher/schema/expression_internal_schema_eq.h" #include "mongo/db/matcher/schema/expression_internal_schema_fmod.h" #include "mongo/db/matcher/schema/expression_internal_schema_match_array_index.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" @@ -50,6 +52,7 @@ #include "mongo/db/matcher/schema/expression_internal_schema_min_length.h" #include "mongo/db/matcher/schema/expression_internal_schema_min_properties.h" #include "mongo/db/matcher/schema/expression_internal_schema_object_match.h" +#include "mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h" #include "mongo/db/matcher/schema/expression_internal_schema_unique_items.h" #include "mongo/db/matcher/schema/expression_internal_schema_xor.h" #include "mongo/logger/log_component_settings.h" @@ -70,6 +73,7 @@ constexpr StringData kSchemaAllOfKeyword = "allOf"_sd; constexpr StringData kSchemaAnyOfKeyword = "anyOf"_sd; constexpr StringData kSchemaDependenciesKeyword = "dependencies"_sd; constexpr StringData kSchemaDescriptionKeyword = "description"_sd; +constexpr StringData kSchemaEnumKeyword = "enum"_sd; constexpr StringData kSchemaExclusiveMaximumKeyword = "exclusiveMaximum"_sd; constexpr StringData kSchemaExclusiveMinimumKeyword = "exclusiveMinimum"_sd; constexpr StringData kSchemaItemsKeyword = "items"_sd; @@ -410,6 +414,60 @@ StatusWithMatchExpression parseLogicalKeyword(StringData path, return {std::move(listOfExpr)}; } +StatusWithMatchExpression parseEnum(StringData path, BSONElement enumElement) { + if (enumElement.type() != BSONType::Array) { + return {ErrorCodes::TypeMismatch, + str::stream() << "$jsonSchema keyword '" << kSchemaEnumKeyword + << "' must be an array, but found an element of type " + << enumElement.type()}; + } + + auto enumArray = enumElement.embeddedObject(); + if (enumArray.isEmpty()) { + return {ErrorCodes::FailedToParse, + str::stream() << "$jsonSchema keyword '" << kSchemaEnumKeyword + << "' cannot be an empty array"}; + } + + auto orExpr = stdx::make_unique<OrMatchExpression>(); + UnorderedFieldsBSONElementComparator eltComp; + BSONEltSet eqSet = eltComp.makeBSONEltSet(); + for (auto&& arrayElem : enumArray) { + auto insertStatus = eqSet.insert(arrayElem); + if (!insertStatus.second) { + return {ErrorCodes::FailedToParse, + str::stream() << "$jsonSchema keyword '" << kSchemaEnumKeyword + << "' array cannot contain duplicate values."}; + } + + // 'enum' at the top-level implies a literal object match on the root document. + if (path.empty()) { + // Top-level non-object enum values can be safely ignored, since MongoDB only stores + // objects, not scalars or arrays. + if (arrayElem.type() == BSONType::Object) { + auto rootDocEq = stdx::make_unique<InternalSchemaRootDocEqMatchExpression>(); + rootDocEq->init(arrayElem.embeddedObject()); + orExpr->add(rootDocEq.release()); + } + } else { + auto eqExpr = stdx::make_unique<InternalSchemaEqMatchExpression>(); + auto initStatus = eqExpr->init(path, arrayElem); + if (!initStatus.isOK()) { + return initStatus; + } + + orExpr->add(eqExpr.release()); + } + } + + // Make sure that the OR expression has at least 1 child. + if (orExpr->numChildren() == 0) { + return {stdx::make_unique<AlwaysFalseMatchExpression>()}; + } + + return {std::move(orExpr)}; +} + /** * Given a BSON element corresponding to the $jsonSchema "required" keyword, returns the set of * required property names. If the contents of the "required" keyword are invalid, returns a non-OK @@ -1079,6 +1137,14 @@ Status translateLogicalKeywords(StringMap<BSONElement>* keywordMap, andExpr->add(notMatchExpr.release()); } + if (auto enumElt = keywordMap->get(kSchemaEnumKeyword)) { + auto enumExpr = parseEnum(path, enumElt); + if (!enumExpr.isOK()) { + return enumExpr.getStatus(); + } + andExpr->add(enumExpr.getValue().release()); + } + return Status::OK(); } @@ -1361,6 +1427,7 @@ StatusWithMatchExpression _parse(StringData path, BSONObj schema, bool ignoreUnk {kSchemaBsonTypeKeyword, {}}, {kSchemaDependenciesKeyword, {}}, {kSchemaDescriptionKeyword, {}}, + {kSchemaEnumKeyword, {}}, {kSchemaExclusiveMaximumKeyword, {}}, {kSchemaExclusiveMinimumKeyword, {}}, {kSchemaItemsKeyword, {}}, diff --git a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp index c43f9c5c1b4..87ee0483c34 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp @@ -2292,5 +2292,67 @@ TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionIfItemsAnObjec }] }]})")); } + +TEST(JSONSchemaParserTest, FailsToParseIfEnumIsNotAnArray) { + BSONObj schema = fromjson("{properties: {foo: {enum: 'foo'}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserTest, FailsToParseEnumIfArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {foo: {enum: []}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserTest, FailsToParseEnumIfArrayContainsDuplicateValue) { + BSONObj schema = fromjson("{properties: {foo: {enum: [1, 2, 1]}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); + + schema = fromjson("{properties: {foo: {enum: [{a: 1, b: 1}, {b: 1, a: 1}]}}}"); + result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserTest, EnumTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {enum: [1, '2', [3]]}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_OK(result.getStatus()); + ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({ + $and: [{ + $and: [{ + $or: [ + {$nor: [{foo: {$exists: true}}]}, + { + $and: [{ + $or: [ + {foo: {$_internalSchemaEq: 1}}, + {foo: {$_internalSchemaEq: "2"}}, + {foo: {$_internalSchemaEq: [3]}} + ] + }] + } + ] + }] + }] + })")); +} + +TEST(JSONSchemaParserTest, TopLevelEnumTranslatesCorrectly) { + BSONObj schema = fromjson("{enum: [1, {foo: 1}]}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_OK(result.getStatus()); + ASSERT_SERIALIZES_TO(result.getValue().get(), + fromjson("{$and: [{$or: [{$_internalSchemaRootDocEq: {foo: 1}}]}]}")); +} + +TEST(JSONSchemaParserTest, TopLevelEnumWithZeroObjectsTranslatesCorrectly) { + BSONObj schema = fromjson("{enum: [1, 'impossible', true]}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_OK(result.getStatus()); + ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson("{$and: [{$alwaysFalse: 1}]}")); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index 7bc49ef0ac3..84e35a01c82 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -258,6 +258,7 @@ Document redactSafePortionDollarOps(BSONObj expr) { case PathAcceptingKeyword::GEO_INTERSECTS: case PathAcceptingKeyword::GEO_NEAR: case PathAcceptingKeyword::INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX: + case PathAcceptingKeyword::INTERNAL_SCHEMA_EQ: case PathAcceptingKeyword::INTERNAL_SCHEMA_FMOD: case PathAcceptingKeyword::INTERNAL_SCHEMA_MATCH_ARRAY_INDEX: case PathAcceptingKeyword::INTERNAL_SCHEMA_MAX_ITEMS: diff --git a/src/mongo/db/pipeline/value.cpp b/src/mongo/db/pipeline/value.cpp index a7bce3fc777..2695da381d0 100644 --- a/src/mongo/db/pipeline/value.cpp +++ b/src/mongo/db/pipeline/value.cpp @@ -648,7 +648,7 @@ inline static int cmp(const T& left, const T& right) { int Value::compare(const Value& rL, const Value& rR, const StringData::ComparatorInterface* stringComparator) { - // Note, this function needs to behave identically to BSON's compareElementValues(). + // Note, this function needs to behave identically to BSONElement::compareElements(). // Additionally, any changes here must be replicated in hash_combine(). BSONType lType = rL.getType(); BSONType rType = rR.getType(); @@ -660,7 +660,8 @@ int Value::compare(const Value& rL, return ret; switch (lType) { - // Order of types is the same as in compareElementValues() to make it easier to verify + // Order of types is the same as in BSONElement::compareElements() to make it easier to + // verify. // These are valueless types case EOO: @@ -802,7 +803,9 @@ int Value::compare(const Value& rL, return rL.getStringData().compare(rR.getStringData()); } - case RegEx: // same as String in this impl but keeping order same as compareElementValues + case RegEx: + // same as String in this impl but keeping order same as + // BSONElement::compareElements(). return rL.getStringData().compare(rR.getStringData()); case CodeWScope: { @@ -826,7 +829,7 @@ void Value::hash_combine(size_t& seed, boost::hash_combine(seed, canonicalizeBSONType(type)); switch (type) { - // Order of types is the same as in Value::compare() and compareElementValues(). + // Order of types is the same as in Value::compare() and BSONElement::compareElements(). // These are valueless types case EOO: diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp index 092a3bf6512..43502d96c0b 100644 --- a/src/mongo/db/query/plan_cache.cpp +++ b/src/mongo/db/query/plan_cache.cpp @@ -186,6 +186,9 @@ const char* encodeMatchType(MatchExpression::MatchType mt) { case MatchExpression::INTERNAL_SCHEMA_COND: return "internalSchemaCond"; + case MatchExpression::INTERNAL_SCHEMA_EQ: + return "internalSchemaEq"; + case MatchExpression::INTERNAL_SCHEMA_FMOD: return "internalSchemaFmod"; @@ -204,6 +207,9 @@ const char* encodeMatchType(MatchExpression::MatchType mt) { case MatchExpression::INTERNAL_SCHEMA_OBJECT_MATCH: return "internalSchemaObjectMatch"; + case MatchExpression::INTERNAL_SCHEMA_ROOT_DOC_EQ: + return "internalSchemaRootDocEq"; + case MatchExpression::INTERNAL_SCHEMA_MIN_LENGTH: return "internalSchemaMinLength"; |