summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-06-27 15:26:26 -0400
committerNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-06-27 17:57:36 -0400
commit8aa235f5b5eb82223cee67433df4cb78e19d10c5 (patch)
tree6974625936875735fd0153d0739631cd64512814
parent9c65741b29f592379be4114f651849e8dde856b6 (diff)
downloadmongo-8aa235f5b5eb82223cee67433df4cb78e19d10c5.tar.gz
SERVER-29587: Create $_internalSchemaMinItems and $_internalSchemaMaxItems MatchExpressions
-rw-r--r--src/mongo/bson/bsonelement.cpp2
-rw-r--r--src/mongo/bson/bsonobj.h2
-rw-r--r--src/mongo/db/matcher/SConscript4
-rw-r--r--src/mongo/db/matcher/expression.h6
-rw-r--r--src/mongo/db/matcher/expression_leaf.cpp6
-rw-r--r--src/mongo/db/matcher/expression_leaf.h4
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp127
-rw-r--r--src/mongo/db/matcher/expression_parser.h25
-rw-r--r--src/mongo/db/matcher/expression_parser_leaf_test.cpp24
-rw-r--r--src/mongo/db/matcher/expression_serialization_test.cpp23
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_max_items.h60
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_max_items_test.cpp96
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_min_items.h60
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_min_items_test.cpp95
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp63
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h66
-rw-r--r--src/mongo/db/matcher/schema/expression_parser_schema_test.cpp203
-rw-r--r--src/mongo/db/pipeline/document_source_match.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_match_test.cpp4
-rw-r--r--src/mongo/db/query/plan_cache.cpp10
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 "";
}
}