diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2017-06-27 15:26:26 -0400 |
---|---|---|
committer | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2017-06-27 17:57:36 -0400 |
commit | 8aa235f5b5eb82223cee67433df4cb78e19d10c5 (patch) | |
tree | 6974625936875735fd0153d0739631cd64512814 | |
parent | 9c65741b29f592379be4114f651849e8dde856b6 (diff) | |
download | mongo-8aa235f5b5eb82223cee67433df4cb78e19d10c5.tar.gz |
SERVER-29587: Create $_internalSchemaMinItems and $_internalSchemaMaxItems MatchExpressions
20 files changed, 834 insertions, 48 deletions
diff --git a/src/mongo/bson/bsonelement.cpp b/src/mongo/bson/bsonelement.cpp index 679367f1c18..454a938a17f 100644 --- a/src/mongo/bson/bsonelement.cpp +++ b/src/mongo/bson/bsonelement.cpp @@ -334,6 +334,8 @@ const StringMap<BSONObj::MatchType> queryOperatorMap{ {"bitsAllClear", BSONObj::opBITS_ALL_CLEAR}, {"bitsAnySet", BSONObj::opBITS_ANY_SET}, {"bitsAnyClear", BSONObj::opBITS_ANY_CLEAR}, + {"_internalSchemaMinItems", BSONObj::opINTERNAL_SCHEMA_MIN_ITEMS}, + {"_internalSchemaMaxItems", BSONObj::opINTERNAL_SCHEMA_MAX_ITEMS}, }; // Compares two string elements using a simple binary compare. diff --git a/src/mongo/bson/bsonobj.h b/src/mongo/bson/bsonobj.h index 15e2326c0db..49d0edf06bc 100644 --- a/src/mongo/bson/bsonobj.h +++ b/src/mongo/bson/bsonobj.h @@ -528,6 +528,8 @@ public: opBITS_ALL_CLEAR = 0x18, opBITS_ANY_SET = 0x19, opBITS_ANY_CLEAR = 0x1A, + opINTERNAL_SCHEMA_MIN_ITEMS = 0x1B, + opINTERNAL_SCHEMA_MAX_ITEMS = 0x1C, }; /** add all elements of the object to the specified vector */ diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index 0c53e9afda7..192b8ebe96a 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -45,6 +45,7 @@ env.Library( 'match_details.cpp', 'matchable.cpp', 'matcher.cpp', + 'schema/expression_internal_schema_num_array_items.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/base', @@ -64,6 +65,8 @@ env.CppUnitTest( 'expression_leaf_test.cpp', 'expression_test.cpp', 'expression_tree_test.cpp', + 'schema/expression_internal_schema_max_items_test.cpp', + 'schema/expression_internal_schema_min_items_test.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/db/query/collation/collator_interface_mock', @@ -78,6 +81,7 @@ env.CppUnitTest( 'expression_parser_leaf_test.cpp', 'expression_parser_test.cpp', 'expression_parser_tree_test.cpp', + 'schema/expression_parser_schema_test.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/db/query/collation/collator_interface_mock', diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h index 968b64843bb..292d82107f4 100644 --- a/src/mongo/db/matcher/expression.h +++ b/src/mongo/db/matcher/expression.h @@ -95,7 +95,11 @@ public: // Expressions that are only created internally INTERNAL_2DSPHERE_KEY_IN_REGION, INTERNAL_2D_KEY_IN_REGION, - INTERNAL_2D_POINT_IN_ANNULUS + INTERNAL_2D_POINT_IN_ANNULUS, + + // JSON Schema expressions. + INTERNAL_SCHEMA_MIN_ITEMS, + INTERNAL_SCHEMA_MAX_ITEMS, }; MatchExpression(MatchType type); diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp index 4980986f9d4..d1d9ad08567 100644 --- a/src/mongo/db/matcher/expression_leaf.cpp +++ b/src/mongo/db/matcher/expression_leaf.cpp @@ -39,6 +39,7 @@ #include "mongo/config.h" #include "mongo/db/field_ref.h" #include "mongo/db/jsobj.h" +#include "mongo/db/matcher/expression_parser.h" #include "mongo/db/matcher/path.h" #include "mongo/db/query/collation/collator_interface.h" #include "mongo/stdx/memory.h" @@ -655,9 +656,6 @@ Status InMatchExpression::addRegex(std::unique_ptr<RegexMatchExpression> expr) { // ----------- -const double BitTestMatchExpression::kLongLongMaxPlusOneAsDouble = - scalbn(1, std::numeric_limits<long long>::digits); - Status BitTestMatchExpression::init(StringData path, std::vector<uint32_t> bitPositions) { _bitPositions = std::move(bitPositions); @@ -796,7 +794,7 @@ bool BitTestMatchExpression::matchesSingleElement(const BSONElement& e) const { // integer are treated as 0. We use 'kLongLongMaxAsDouble' because if we just did // eDouble > 2^63-1, it would be compared against 2^63. eDouble=2^63 would not get caught // that way. - if (eDouble >= kLongLongMaxPlusOneAsDouble || + if (eDouble >= MatchExpressionParser::kLongLongMaxPlusOneAsDouble || eDouble < std::numeric_limits<long long>::min()) { return false; } diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h index 093ebe7edad..346ba831492 100644 --- a/src/mongo/db/matcher/expression_leaf.h +++ b/src/mongo/db/matcher/expression_leaf.h @@ -461,10 +461,6 @@ private: */ class BitTestMatchExpression : public LeafMatchExpression { public: - // Constant used in matchesSingleElement() and MatchExpressionParser::_parseBitTest. Is a - // double representation of 2^63. - static const double kLongLongMaxPlusOneAsDouble; - BitTestMatchExpression(MatchType type) : LeafMatchExpression(type) {} virtual ~BitTestMatchExpression() {} diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index 2d1399edcf9..6542d7ac68c 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -36,6 +36,8 @@ #include "mongo/db/matcher/expression_array.h" #include "mongo/db/matcher/expression_leaf.h" #include "mongo/db/matcher/expression_tree.h" +#include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" +#include "mongo/db/matcher/schema/expression_internal_schema_min_items.h" #include "mongo/db/namespace_string.h" #include "mongo/stdx/memory.h" #include "mongo/util/mongoutils/str.h" @@ -67,6 +69,9 @@ namespace mongo { using std::string; using stdx::make_unique; +const double MatchExpressionParser::kLongLongMaxPlusOneAsDouble = + scalbn(1, std::numeric_limits<long long>::digits); + StatusWithMatchExpression MatchExpressionParser::_parseComparison( const char* name, ComparisonMatchExpression* cmp, @@ -277,6 +282,16 @@ StatusWithMatchExpression MatchExpressionParser::_parseSubField(const BSONObj& c case BSONObj::opBITS_ANY_CLEAR: { return _parseBitTest<BitsAnyClearMatchExpression>(name, e); } + + case BSONObj::opINTERNAL_SCHEMA_MIN_ITEMS: { + return _parseInternalSchemaSingleIntegerArgument<InternalSchemaMinItemsMatchExpression>( + name, e); + } + + case BSONObj::opINTERNAL_SCHEMA_MAX_ITEMS: { + return _parseInternalSchemaSingleIntegerArgument<InternalSchemaMaxItemsMatchExpression>( + name, e); + } } return {Status(ErrorCodes::BadValue, @@ -830,45 +845,12 @@ StatusWithMatchExpression MatchExpressionParser::_parseBitTest(const char* name, } } else if (e.isNumber()) { // Integer bitmask provided as value. - - if (e.type() == BSONType::NumberDouble) { - double eDouble = e.numberDouble(); - - // NaN doubles are rejected. - if (std::isnan(eDouble)) { - mongoutils::str::stream ss; - ss << name << " cannot take a NaN"; - return Status(ErrorCodes::BadValue, ss); - } - - // No integral doubles that are too large to be represented as a 64 bit signed integer. - // We use 'kLongLongMaxAsDouble' because if we just did eDouble > 2^63-1, it would be - // compared against 2^63. eDouble=2^63 would not get caught that way. - if (eDouble >= BitTestMatchExpression::kLongLongMaxPlusOneAsDouble || - eDouble < std::numeric_limits<long long>::min()) { - mongoutils::str::stream ss; - ss << name << " cannot be represented as a 64-bit integer: " << e; - return Status(ErrorCodes::BadValue, ss); - } - - // This checks if e is an integral double. - if (eDouble != static_cast<double>(static_cast<long long>(eDouble))) { - mongoutils::str::stream ss; - ss << name << " cannot have a fractional part but received: " << e; - return Status(ErrorCodes::BadValue, ss); - } - } - - long long bitMask = e.numberLong(); - - // No negatives. - if (bitMask < 0) { - mongoutils::str::stream ss; - ss << name << " cannot take a negative number: " << e; - return Status(ErrorCodes::BadValue, ss); + auto bitMask = parseIntegerElementToPositiveLong(name, e); + if (!bitMask.isOK()) { + return bitMask.getStatus(); } - Status s = bitTestMatchExpression->init(name, bitMask); + Status s = bitTestMatchExpression->init(name, bitMask.getValue()); if (!s.isOK()) { return s; } @@ -964,4 +946,75 @@ StatusWithMatchExpression expressionParserGeoCallbackDefault(const char* name, } MatchExpressionParserGeoCallback expressionParserGeoCallback = expressionParserGeoCallbackDefault; + +StatusWith<long long> MatchExpressionParser::parseIntegerElementToPositiveLong( + const char* name, const BSONElement& elem) { + if (!elem.isNumber()) { + return Status(ErrorCodes::FailedToParse, str::stream() << name << " must be a number"); + } + + long long number = 0; + if (elem.type() == BSONType::NumberDouble) { + double eDouble = elem.numberDouble(); + + // NaN doubles are rejected. + if (std::isnan(eDouble)) { + return Status(ErrorCodes::FailedToParse, str::stream() << name << " cannot take a NaN"); + } + + // No integral doubles that are too large to be represented as a 64 bit signed integer. + // We use 'kLongLongMaxAsDouble' because if we just did eDouble > 2^63-1, it would be + // compared against 2^63. eDouble=2^63 would not get caught that way. + if (eDouble >= MatchExpressionParser::kLongLongMaxPlusOneAsDouble || + eDouble < std::numeric_limits<long long>::min()) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << name << " cannot be represented as a 64-bit integer: " << elem); + } + + // This checks if elem is an integral double. + if (eDouble != static_cast<double>(static_cast<long long>(eDouble))) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << name << " cannot have a fractional part but received: " << elem); + } + + number = elem.numberLong(); + } else if (elem.type() == BSONType::NumberDecimal) { + uint32_t signalingFlags = 0; + number = elem.numberDecimal().toLongExact(&signalingFlags); + if (Decimal128::hasFlag(signalingFlags, Decimal128::kInexact)) { + return Status( + ErrorCodes::FailedToParse, + str::stream() << name << " cannot be represented as a 64-bit integer: " << elem); + } + } else { + number = elem.numberLong(); + } + + if (number < 0) { + return Status(ErrorCodes::FailedToParse, + str::stream() << name << " cannot take a negative number"); + } + + return number; +} + +template <class T> +StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaSingleIntegerArgument( + const char* name, const BSONElement& elem) const { + + auto parsedInt = parseIntegerElementToPositiveLong(name, elem); + if (!parsedInt.isOK()) { + return parsedInt.getStatus(); + } + + auto matchExpression = stdx::make_unique<T>(); + auto status = matchExpression->init(name, parsedInt.getValue()); + if (!status.isOK()) { + return status; + } + + return {std::move(matchExpression)}; } +} // namespace mongo diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h index 8141726251b..c39df98c77e 100644 --- a/src/mongo/db/matcher/expression_parser.h +++ b/src/mongo/db/matcher/expression_parser.h @@ -46,6 +46,11 @@ class OperationContext; class MatchExpressionParser { public: /** + * Constant double representation of 2^63. + */ + static const double kLongLongMaxPlusOneAsDouble; + + /** * caller has to maintain ownership obj * the tree has views (BSONElement) into obj */ @@ -56,6 +61,17 @@ public: return MatchExpressionParser(&extensionsCallback)._parse(obj, collator, topLevelCall); } + /** + * Parses a BSONElement of any numeric type into a positive long long, failing if the value + * is any of the following: + * + * - NaN + * - Too big or small to fit within a 64-bit signed integer. + * - A floating point number which is not integral. + */ + static StatusWith<long long> parseIntegerElementToPositiveLong(const char* name, + const BSONElement& elem); + private: MatchExpressionParser(const ExtensionsCallback* extensionsCallback) : _extensionsCallback(extensionsCallback) {} @@ -169,6 +185,15 @@ private: */ StatusWith<std::vector<uint32_t>> _parseBitPositionsArray(const BSONObj& theArray); + /** + * Parses the given BSONElement into a single integer argument and creates a MatchExpression + * of type 'T' that gets initialized with the resulting integer. + */ + template <class T> + StatusWithMatchExpression _parseInternalSchemaSingleIntegerArgument( + const char* name, const BSONElement& elem) const; + + // The maximum allowed depth of a query tree. Just to guard against stack overflow. static const int kMaximumTreeDepth; diff --git a/src/mongo/db/matcher/expression_parser_leaf_test.cpp b/src/mongo/db/matcher/expression_parser_leaf_test.cpp index 67c21bdfc8a..aa6e054870f 100644 --- a/src/mongo/db/matcher/expression_parser_leaf_test.cpp +++ b/src/mongo/db/matcher/expression_parser_leaf_test.cpp @@ -1578,6 +1578,12 @@ TEST(MatchExpressionParserTest, BitTestMatchExpressionInvalidMaskValue) { collator) .getStatus()); + ASSERT_NOT_OK( + MatchExpressionParser::parse(BSON("a" << BSON("$bitsAllSet" << Decimal128("2.5"))), + ExtensionsCallbackDisallowExtensions(), + collator) + .getStatus()); + ASSERT_NOT_OK(MatchExpressionParser::parse(fromjson("{a: {$bitsAllClear: NaN}}"), ExtensionsCallbackDisallowExtensions(), collator) @@ -1601,6 +1607,12 @@ TEST(MatchExpressionParserTest, BitTestMatchExpressionInvalidMaskValue) { collator) .getStatus()); + ASSERT_NOT_OK( + MatchExpressionParser::parse(BSON("a" << BSON("$bitsAllClear" << Decimal128("2.5"))), + ExtensionsCallbackDisallowExtensions(), + collator) + .getStatus()); + ASSERT_NOT_OK(MatchExpressionParser::parse(fromjson("{a: {$bitsAnySet: NaN}}"), ExtensionsCallbackDisallowExtensions(), collator) @@ -1624,6 +1636,12 @@ TEST(MatchExpressionParserTest, BitTestMatchExpressionInvalidMaskValue) { collator) .getStatus()); + ASSERT_NOT_OK( + MatchExpressionParser::parse(BSON("a" << BSON("$bitsAnySet" << Decimal128("2.5"))), + ExtensionsCallbackDisallowExtensions(), + collator) + .getStatus()); + ASSERT_NOT_OK(MatchExpressionParser::parse(fromjson("{a: {$bitsAnyClear: NaN}}"), ExtensionsCallbackDisallowExtensions(), collator) @@ -1646,6 +1664,12 @@ TEST(MatchExpressionParserTest, BitTestMatchExpressionInvalidMaskValue) { ExtensionsCallbackDisallowExtensions(), collator) .getStatus()); + + ASSERT_NOT_OK( + MatchExpressionParser::parse(BSON("a" << BSON("$bitsAnyClear" << Decimal128("2.5"))), + ExtensionsCallbackDisallowExtensions(), + collator) + .getStatus()); } TEST(MatchExpressionParserTest, BitTestMatchExpressionInvalidArray) { diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp index 13a2aaa172d..b05cbe71bfc 100644 --- a/src/mongo/db/matcher/expression_serialization_test.cpp +++ b/src/mongo/db/matcher/expression_serialization_test.cpp @@ -33,6 +33,7 @@ #include "mongo/db/json.h" #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/matcher/extensions_callback_disallow_extensions.h" #include "mongo/db/matcher/extensions_callback_noop.h" #include "mongo/db/matcher/matcher.h" #include "mongo/unittest/unittest.h" @@ -989,5 +990,27 @@ TEST(SerializeBasic, ExpressionTextWithDefaultLanguageSerializesCorrectly) { ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); } +TEST(SerializeInternalSchema, ExpressionInternalSchemaMinItemsSerializesCorrectly) { + const CollatorInterface* collator = nullptr; + Matcher original(fromjson("{x: {$_internalSchemaMinItems: 1}}"), + ExtensionsCallbackDisallowExtensions(), + collator); + Matcher reserialized( + serialize(original.getMatchExpression()), ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), fromjson("{x: {$_internalSchemaMinItems: 1}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); +} + +TEST(SerializeInternalSchema, ExpressionInternalSchemaMaxItemsSerializesCorrectly) { + const CollatorInterface* collator = nullptr; + Matcher original(fromjson("{x: {$_internalSchemaMaxItems: 1}}"), + ExtensionsCallbackDisallowExtensions(), + collator); + Matcher reserialized( + serialize(original.getMatchExpression()), ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), fromjson("{x: {$_internalSchemaMaxItems: 1}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h new file mode 100644 index 00000000000..3ba965e298a --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h @@ -0,0 +1,60 @@ +/** + * 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/db/matcher/schema/expression_internal_schema_num_array_items.h" + +namespace mongo { + +/** + * MatchExpression for $_internalSchemaMaxItems keyword. Takes an integer argument that indicates + * the maximum amount of elements in an array. + */ +class InternalSchemaMaxItemsMatchExpression final + : public InternalSchemaNumArrayItemsMatchExpression { +public: + InternalSchemaMaxItemsMatchExpression() + : InternalSchemaNumArrayItemsMatchExpression(INTERNAL_SCHEMA_MAX_ITEMS, + "$_internalSchemaMaxItems"_sd) {} + + bool matchesArray(const BSONObj& anArray, MatchDetails* details) const final { + return (anArray.nFields() <= numItems()); + } + + std::unique_ptr<MatchExpression> shallowClone() const final { + std::unique_ptr<InternalSchemaMaxItemsMatchExpression> maxItems = + stdx::make_unique<InternalSchemaMaxItemsMatchExpression>(); + invariantOK(maxItems->init(path(), numItems())); + if (getTag()) { + maxItems->setTag(getTag()->clone()); + } + return std::move(maxItems); + } +}; +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_max_items_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_max_items_test.cpp new file mode 100644 index 00000000000..27675889dee --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_max_items_test.cpp @@ -0,0 +1,96 @@ +/** + * 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/expression.h" +#include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { + +namespace { + +TEST(InternalSchemaMaxItemsMatchExpression, RejectsNonArrayElements) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a", 1)); + + ASSERT(!maxItems.matchesBSON(BSON("a" << BSONObj()))); + ASSERT(!maxItems.matchesBSON(BSON("a" << 1))); + ASSERT(!maxItems.matchesBSON(BSON("a" + << "string"))); +} + +TEST(InternalSchemaMaxItemsMatchExpression, RejectsArraysWithTooManyElements) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a", 0)); + + ASSERT(!maxItems.matchesBSON(BSON("a" << BSON_ARRAY(1)))); + ASSERT(!maxItems.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2)))); +} + +TEST(InternalSchemaMaxItemsMatchExpression, AcceptsArrayWithLessThanOrEqualToMaxElements) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a", 2)); + + ASSERT(maxItems.matchesBSON(BSON("a" << BSON_ARRAY(5 << 6)))); + ASSERT(maxItems.matchesBSON(BSON("a" << BSON_ARRAY(5)))); +} + +TEST(InternalSchemaMaxItemsMatchExpression, MaxItemsZeroAllowsEmptyArrays) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a", 0)); + + ASSERT(maxItems.matchesBSON(BSON("a" << BSONArray()))); +} + +TEST(InternalSchemaMaxItemsMatchExpression, NullArrayEntriesCountAsItems) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a", 2)); + + ASSERT(maxItems.matchesBSON(BSON("a" << BSON_ARRAY(BSONNULL << 1)))); + ASSERT(!maxItems.matchesBSON(BSON("a" << BSON_ARRAY(BSONNULL << 1 << 2)))); +} + +TEST(InternalSchemaMaxItemsMatchExpression, NestedArraysAreNotUnwound) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a", 2)); + + ASSERT(maxItems.matchesBSON(BSON("a" << BSON_ARRAY(BSON_ARRAY(1 << 2 << 3))))); +} + +TEST(InternalSchemaMaxItemsMatchExpression, NestedArraysWorkWithDottedPaths) { + InternalSchemaMaxItemsMatchExpression maxItems; + ASSERT_OK(maxItems.init("a.b", 2)); + + ASSERT(maxItems.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(1))))); + ASSERT(!maxItems.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(1 << 2 << 3))))); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h new file mode 100644 index 00000000000..82eb16b5511 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h @@ -0,0 +1,60 @@ +/** + * 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/db/matcher/schema/expression_internal_schema_num_array_items.h" + +namespace mongo { + +/** + * MatchExpression for $_internalSchemaMinItems keyword. Takes an integer argument that indicates + * the minimum amount of elements in an array. + */ +class InternalSchemaMinItemsMatchExpression final + : public InternalSchemaNumArrayItemsMatchExpression { +public: + InternalSchemaMinItemsMatchExpression() + : InternalSchemaNumArrayItemsMatchExpression(INTERNAL_SCHEMA_MIN_ITEMS, + "$_internalSchemaMinItems"_sd) {} + + bool matchesArray(const BSONObj& anArray, MatchDetails* details) const final { + return (anArray.nFields() >= numItems()); + } + + std::unique_ptr<MatchExpression> shallowClone() const final { + std::unique_ptr<InternalSchemaMinItemsMatchExpression> minItems = + stdx::make_unique<InternalSchemaMinItemsMatchExpression>(); + invariantOK(minItems->init(path(), numItems())); + if (getTag()) { + minItems->setTag(getTag()->clone()); + } + return std::move(minItems); + } +}; +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_min_items_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_min_items_test.cpp new file mode 100644 index 00000000000..2f43c269c7f --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_min_items_test.cpp @@ -0,0 +1,95 @@ +/** + * 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/expression.h" +#include "mongo/db/matcher/schema/expression_internal_schema_min_items.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { + +namespace { + +TEST(InternalSchemaMinItemsMatchExpression, RejectsNonArrayElements) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a", 1)); + + ASSERT(!minItems.matchesBSON(BSON("a" << BSONObj()))); + ASSERT(!minItems.matchesBSON(BSON("a" << 1))); + ASSERT(!minItems.matchesBSON(BSON("a" + << "string"))); +} + +TEST(InternalSchemaMinItemsMatchExpression, RejectsArraysWithTooFewElements) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a", 2)); + + ASSERT(!minItems.matchesBSON(BSON("a" << BSONArray()))); + ASSERT(!minItems.matchesBSON(BSON("a" << BSON_ARRAY(1)))); +} + +TEST(InternalSchemaMinItemsMatchExpression, AcceptsArrayWithAtLeastMinElements) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a", 2)); + + ASSERT(minItems.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2)))); + ASSERT(minItems.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 3)))); +} + +TEST(InternalSchemaMinItemsMatchExpression, MinItemsZeroAllowsEmptyArrays) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a", 0)); + + ASSERT(minItems.matchesBSON(BSON("a" << BSONArray()))); +} + +TEST(InternalSchemaMinItemsMatchExpression, NullArrayEntriesCountAsItems) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a", 1)); + + ASSERT(minItems.matchesBSON(BSON("a" << BSON_ARRAY(BSONNULL)))); + ASSERT(minItems.matchesBSON(BSON("a" << BSON_ARRAY(BSONNULL << 1)))); +} + +TEST(InternalSchemaMinItemsMatchExpression, NestedArraysAreNotUnwound) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a", 2)); + + ASSERT(!minItems.matchesBSON(BSON("a" << BSON_ARRAY(BSON_ARRAY(1 << 2))))); +} + +TEST(InternalSchemaMinItemsMatchExpression, NestedArraysWorkWithDottedPaths) { + InternalSchemaMinItemsMatchExpression minItems; + ASSERT_OK(minItems.init("a.b", 2)); + + ASSERT(minItems.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(1 << 2))))); + ASSERT(!minItems.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(1))))); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp new file mode 100644 index 00000000000..f6a31657847 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp @@ -0,0 +1,63 @@ +/** + * 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_num_array_items.h" + +namespace mongo { + +void InternalSchemaNumArrayItemsMatchExpression::debugString(StringBuilder& debug, + int level) const { + _debugAddSpace(debug, level); + debug << path() << " " << _name << " " << _numItems << "\n"; + + MatchExpression::TagData* td = getTag(); + if (nullptr != td) { + debug << " "; + td->debugString(&debug); + } + debug << "\n"; +} + +void InternalSchemaNumArrayItemsMatchExpression::serialize(BSONObjBuilder* out) const { + BSONObjBuilder subBob(out->subobjStart(path())); + subBob.append(_name, _numItems); + subBob.doneFast(); +} + +bool InternalSchemaNumArrayItemsMatchExpression::equivalent(const MatchExpression* other) const { + if (matchType() != other->matchType()) + return false; + + const InternalSchemaNumArrayItemsMatchExpression* realOther = + static_cast<const InternalSchemaNumArrayItemsMatchExpression*>(other); + + return path() == realOther->path() && _numItems == realOther->_numItems; +} +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h new file mode 100644 index 00000000000..0acedc4ee67 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h @@ -0,0 +1,66 @@ +/** + * 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.h" +#include "mongo/db/matcher/expression_array.h" + +namespace mongo { + +/** + * MatchExpression for internal JSON Schema keywords that validate the number of items in an array. + */ +class InternalSchemaNumArrayItemsMatchExpression : public ArrayMatchingMatchExpression { +public: + InternalSchemaNumArrayItemsMatchExpression(MatchType type, StringData name) + : ArrayMatchingMatchExpression(type), _name(name) {} + + virtual ~InternalSchemaNumArrayItemsMatchExpression() {} + + Status init(StringData path, long long numItems) { + _numItems = numItems; + return setPath(path); + } + + void debugString(StringBuilder& debug, int level) const final; + + void serialize(BSONObjBuilder* out) const final; + + bool equivalent(const MatchExpression* other) const final; + +protected: + long long numItems() const { + return _numItems; + } + +private: + StringData _name; + long long _numItems = 0; +}; +} // 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 new file mode 100644 index 00000000000..304922d62f1 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp @@ -0,0 +1,203 @@ +/** + * 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/expression.h" +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/matcher/extensions_callback_disallow_extensions.h" +#include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" +#include "mongo/db/matcher/schema/expression_internal_schema_min_items.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { + +namespace { + +// +// Tests for parsing an integer BSONElement into a positive long long. +// +TEST(MatchExpressionParserIntegerConversionTest, FailsToParseNonIntegerTypes) { + BSONObj query = BSON("$_internalSchemaMinItems" << -2LL); + StatusWith<long long> result = MatchExpressionParser::parseIntegerElementToPositiveLong( + query.firstElementFieldName(), query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); + + query = BSON("$_internalSchemaMinItems" << MatchExpressionParser::kLongLongMaxPlusOneAsDouble); + result = MatchExpressionParser::parseIntegerElementToPositiveLong(query.firstElementFieldName(), + query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); + + query = BSON("$_internalSchemaMinItems" + << "1"); + result = MatchExpressionParser::parseIntegerElementToPositiveLong(query.firstElementFieldName(), + query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); + + query = BSON("$_internalSchemaMinItems" << -2.0); + result = MatchExpressionParser::parseIntegerElementToPositiveLong(query.firstElementFieldName(), + query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); + + query = BSON("$_internalSchemaMinItems" << 2.5); + result = MatchExpressionParser::parseIntegerElementToPositiveLong(query.firstElementFieldName(), + query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); + + query = BSON("$_internalSchemaMinItems" << Decimal128("2.5")); + result = MatchExpressionParser::parseIntegerElementToPositiveLong(query.firstElementFieldName(), + query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); + + query = BSON("$_internalSchemaMinItems" << Decimal128(Decimal128::kLargestPositive)); + result = MatchExpressionParser::parseIntegerElementToPositiveLong(query.firstElementFieldName(), + query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(MatchExpressionParserIntegerConversionTest, FailsToParseNegativeNumbers) { + BSONObj query = BSON("$_internalSchemaMinItems" << -2); + auto result = MatchExpressionParser::parseIntegerElementToPositiveLong( + query.firstElementFieldName(), query.firstElement()); + ASSERT_NOT_OK(result.getStatus()); +} + +// +// Tests for parsing the $_internalSchemaMinItems expression. +// +TEST(MatchExpressionParserInternalSchemaMinItemsTest, CorrectlyParsesIntegerArgument) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMinItems" << 2)); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + +TEST(MatchExpressionParserInternalSchemaMinItemsTest, CorrectlyParsesLongArgument) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMinItems" << 2LL)); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + + +TEST(MatchExpressionParserInternalSchemaMinItemsTest, CorrectlyParsesDoubleArgumentAsInteger) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMinItems" << 2.0)); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + +TEST(MatchExpressionParserInternalSchemaMinItemsTest, CorrectlyParsesDecimalArgumentAsInteger) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMinItems" << Decimal128("2"))); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + +// +// Tests for parsing the $_internalSchemaMaxItems expression. +// +TEST(MatchExpressionParserInternalSchemaMaxItemsTest, CorrectlyParsesIntegerArgument) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMaxItems" << 2)); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + +TEST(MatchExpressionParserInternalSchemaMaxItemsTest, CorrectlyParsesLongArgument) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMaxItems" << 2LL)); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + + +TEST(MatchExpressionParserInternalSchemaMaxItemsTest, CorrectlyParsesDoubleArgumentAsInteger) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMaxItems" << 2.0)); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + +TEST(MatchExpressionParserInternalSchemaMaxItemsTest, CorrectlyParsesDecimalArgumentAsInteger) { + BSONObj query = BSON("x" << BSON("$_internalSchemaMaxItems" << Decimal128("2"))); + const CollatorInterface* collator = nullptr; + StatusWithMatchExpression result = + MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), collator); + ASSERT_TRUE(result.isOK()); + + ASSERT(!result.getValue()->matchesBSON(BSON("x" << 1))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2)))); + ASSERT(result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1)))); + ASSERT(!result.getValue()->matchesBSON(BSON("x" << BSON_ARRAY(1 << 2 << 3)))); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index bf37a88955e..7606bdcf1b0 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -273,6 +273,8 @@ Document redactSafePortionDollarOps(BSONObj expr) { case BSONObj::opEXISTS: case BSONObj::opWITHIN: case BSONObj::opGEO_INTERSECTS: + case BSONObj::opINTERNAL_SCHEMA_MIN_ITEMS: + case BSONObj::opINTERNAL_SCHEMA_MAX_ITEMS: continue; } } diff --git a/src/mongo/db/pipeline/document_source_match_test.cpp b/src/mongo/db/pipeline/document_source_match_test.cpp index f471d6aad31..b84b472f41e 100644 --- a/src/mongo/db/pipeline/document_source_match_test.cpp +++ b/src/mongo/db/pipeline/document_source_match_test.cpp @@ -106,6 +106,10 @@ TEST_F(DocumentSourceMatchTest, RedactSafePortion) { assertExpectedRedactSafePortion("{$nor: [{a:1}]}", "{}"); + assertExpectedRedactSafePortion("{a: {$_internalSchemaMinItems: 1}}", "{}"); + + assertExpectedRedactSafePortion("{a: {$_internalSchemaMaxItems: 1}}", "{}"); + // Combinations assertExpectedRedactSafePortion("{a:1, b: 'asdf'}", "{a:1, b: 'asdf'}"); diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp index b0a80b537ee..f1f3fb6d6fd 100644 --- a/src/mongo/db/query/plan_cache.cpp +++ b/src/mongo/db/query/plan_cache.cpp @@ -89,7 +89,7 @@ void encodeUserString(StringData s, StringBuilder* keyBuilder) { } /** - * 2-character encoding of MatchExpression::MatchType. + * String encoding of MatchExpression::MatchType. */ const char* encodeMatchType(MatchExpression::MatchType mt) { switch (mt) { @@ -171,8 +171,14 @@ const char* encodeMatchType(MatchExpression::MatchType mt) { case MatchExpression::BITS_ANY_CLEAR: return "yc"; break; + case MatchExpression::INTERNAL_SCHEMA_MIN_ITEMS: + return "internalSchemaMinItems"; + break; + case MatchExpression::INTERNAL_SCHEMA_MAX_ITEMS: + return "internalSchemaMaxItems"; + break; default: - verify(0); + MONGO_UNREACHABLE; return ""; } } |