summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mongo/db/matcher/SConscript2
-rw-r--r--src/mongo/db/matcher/expression.h1
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp273
-rw-r--r--src/mongo/db/matcher/expression_parser.h3
-rw-r--r--src/mongo/db/matcher/expression_serialization_test.cpp21
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp170
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h161
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties_test.cpp116
-rw-r--r--src/mongo/db/matcher/schema/expression_parser_schema_test.cpp267
-rw-r--r--src/mongo/db/query/plan_cache.cpp3
10 files changed, 981 insertions, 36 deletions
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript
index 04c1cfa0b98..5b3fe0f24c8 100644
--- a/src/mongo/db/matcher/SConscript
+++ b/src/mongo/db/matcher/SConscript
@@ -50,6 +50,7 @@ env.Library(
'matcher.cpp',
'matcher_type_alias.cpp',
'schema/expression_internal_schema_all_elem_match_from_index.cpp',
+ 'schema/expression_internal_schema_allowed_properties.cpp',
'schema/expression_internal_schema_cond.cpp',
'schema/expression_internal_schema_fmod.cpp',
'schema/expression_internal_schema_match_array_index.cpp',
@@ -89,6 +90,7 @@ env.CppUnitTest(
'expression_with_placeholder_test.cpp',
'path_accepting_keyword_test.cpp',
'schema/expression_internal_schema_all_elem_match_from_index_test.cpp',
+ 'schema/expression_internal_schema_allowed_properties_test.cpp',
'schema/expression_internal_schema_cond_test.cpp',
'schema/expression_internal_schema_fmod_test.cpp',
'schema/expression_internal_schema_match_array_index_test.cpp',
diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h
index c428d01c7df..c9eeb38463b 100644
--- a/src/mongo/db/matcher/expression.h
+++ b/src/mongo/db/matcher/expression.h
@@ -99,6 +99,7 @@ public:
INTERNAL_2D_POINT_IN_ANNULUS,
// JSON Schema expressions.
+ INTERNAL_SCHEMA_ALLOWED_PROPERTIES,
INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX,
INTERNAL_SCHEMA_COND,
INTERNAL_SCHEMA_FMOD,
diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp
index 78659ca29c7..e19be150ab1 100644
--- a/src/mongo/db/matcher/expression_parser.cpp
+++ b/src/mongo/db/matcher/expression_parser.cpp
@@ -27,10 +27,14 @@
* 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_parser.h"
+#include <boost/container/flat_set.hpp>
+#include <pcrecpp.h>
+
#include "mongo/base/init.h"
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobj.h"
@@ -43,6 +47,7 @@
#include "mongo/db/matcher/expression_type.h"
#include "mongo/db/matcher/expression_with_placeholder.h"
#include "mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h"
+#include "mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h"
#include "mongo/db/matcher/schema/expression_internal_schema_cond.h"
#include "mongo/db/matcher/schema/expression_internal_schema_fmod.h"
#include "mongo/db/matcher/schema/expression_internal_schema_match_array_index.h"
@@ -457,7 +462,6 @@ StatusWithMatchExpression MatchExpressionParser::_parse(
if (e.fieldName()[0] == '$') {
const char* rest = e.fieldName() + 1;
- // TODO: optimize if block?
if (mongoutils::str::equals("or", rest)) {
if (e.type() != Array)
return {Status(ErrorCodes::BadValue, "$or must be an array")};
@@ -513,6 +517,12 @@ StatusWithMatchExpression MatchExpressionParser::_parse(
eq->setCollator(str::equals("id", rest) ? collator : nullptr);
root->add(eq.release());
+ } else if (mongoutils::str::equals("_internalSchemaAllowedProperties", rest)) {
+ auto allowedProperties = _parseInternalSchemaAllowedProperties(e, collator);
+ if (!allowedProperties.isOK()) {
+ return allowedProperties.getStatus();
+ }
+ root->add(allowedProperties.getValue().release());
} else if (mongoutils::str::equals("_internalSchemaCond", rest)) {
auto condExpr =
_parseInternalSchemaFixedArityArgument<InternalSchemaCondMatchExpression>(
@@ -901,7 +911,8 @@ StatusWithMatchExpression MatchExpressionParser::_parseElemMatch(
!mongoutils::str::equals("$or", elt.fieldName()) &&
!mongoutils::str::equals("$where", elt.fieldName()) &&
!mongoutils::str::equals("$_internalSchemaMinProperties", elt.fieldName()) &&
- !mongoutils::str::equals("$_internalSchemaMaxProperties", elt.fieldName());
+ !mongoutils::str::equals("$_internalSchemaMaxProperties", elt.fieldName()) &&
+ !mongoutils::str::equals("$_internalSchemaAllowedProperties", elt.fieldName());
}
if (isElemMatchValue) {
@@ -1312,6 +1323,170 @@ StatusWithMatchExpression MatchExpressionParser::_parseTopLevelInternalSchemaSin
return {std::move(matchExpression)};
}
+namespace {
+/**
+ * Looks at the field named 'namePlaceholderFieldName' within 'containingObject' and parses a name
+ * placeholder from that element. 'expressionName' is the name of the expression that requires the
+ * name placeholder and is used to generate helpful error messages.
+ */
+StatusWith<StringData> parseNamePlaceholder(const BSONObj& containingObject,
+ StringData namePlaceholderFieldName,
+ StringData expressionName) {
+ auto namePlaceholderElem = containingObject[namePlaceholderFieldName];
+ if (!namePlaceholderElem) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << expressionName << " requires a '" << namePlaceholderFieldName
+ << "'"};
+ } else if (namePlaceholderElem.type() != BSONType::String) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << expressionName << " requires '" << namePlaceholderFieldName
+ << "' to be a string, not "
+ << namePlaceholderElem.type()};
+ }
+ return {namePlaceholderElem.valueStringData()};
+}
+
+/**
+ * Looks at the field named 'exprWithPlaceholderFieldName' within 'containingObject' and parses an
+ * ExpressionWithPlaceholder from that element. Fails if an error occurs during parsing, or if the
+ * ExpressionWithPlaceholder has a different name placeholder than 'expectedPlaceholder'.
+ * 'expressionName' is the name of the expression that requires the ExpressionWithPlaceholder and is
+ * used to generate helpful error messages.
+ */
+StatusWith<std::unique_ptr<ExpressionWithPlaceholder>> parseExprWithPlaceholder(
+ const BSONObj& containingObject,
+ StringData exprWithPlaceholderFieldName,
+ StringData expressionName,
+ StringData expectedPlaceholder,
+ const CollatorInterface* collator) {
+ auto exprWithPlaceholderElem = containingObject[exprWithPlaceholderFieldName];
+ if (!exprWithPlaceholderElem) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << expressionName << " requires '" << exprWithPlaceholderFieldName
+ << "'"};
+ } else if (exprWithPlaceholderElem.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << expressionName << " found '" << exprWithPlaceholderFieldName
+ << "', which is an incompatible type: "
+ << exprWithPlaceholderElem.type()};
+ }
+
+ auto result =
+ ExpressionWithPlaceholder::parse(exprWithPlaceholderElem.embeddedObject(), collator);
+ if (!result.isOK()) {
+ return result.getStatus();
+ }
+
+ if (result.getValue()->getPlaceholder() != expectedPlaceholder) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << expressionName << " expected a name placeholder of "
+ << expectedPlaceholder
+ << ", but '"
+ << exprWithPlaceholderElem.fieldName()
+ << "' has a mismatching placeholder '"
+ << result.getValue()->getPlaceholder()
+ << "'"};
+ }
+ return result;
+}
+
+StatusWith<std::vector<InternalSchemaAllowedPropertiesMatchExpression::PatternSchema>>
+parsePatternProperties(BSONElement patternPropertiesElem,
+ StringData expectedPlaceholder,
+ const CollatorInterface* collator) {
+ if (!patternPropertiesElem) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'patternProperties'"};
+ } else if (patternPropertiesElem.type() != BSONType::Array) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'patternProperties' to be an array, not "
+ << patternPropertiesElem.type()};
+ }
+
+ std::vector<InternalSchemaAllowedPropertiesMatchExpression::PatternSchema> patternProperties;
+ for (auto&& constraintElem : patternPropertiesElem.embeddedObject()) {
+ if (constraintElem.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'patternProperties' to be an array of objects"};
+ }
+
+ auto constraint = constraintElem.embeddedObject();
+ if (constraint.nFields() != 2) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'patternProperties' to be an array of objects "
+ "containing exactly two fields, 'regex' and 'expression'"};
+ }
+
+ auto expressionWithPlaceholder =
+ parseExprWithPlaceholder(constraint,
+ "expression"_sd,
+ InternalSchemaAllowedPropertiesMatchExpression::kName,
+ expectedPlaceholder,
+ collator);
+ if (!expressionWithPlaceholder.isOK()) {
+ return expressionWithPlaceholder.getStatus();
+ }
+
+ auto regexElem = constraint["regex"];
+ if (!regexElem) {
+ return {
+ ErrorCodes::FailedToParse,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires each object in 'patternProperties' to have a 'regex'"};
+ }
+ if (regexElem.type() != BSONType::RegEx) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'patternProperties' to be an array of objects, "
+ "where 'regex' is a regular expression"};
+ } else if (*regexElem.regexFlags() != '\0') {
+ return {
+ ErrorCodes::BadValue,
+ str::stream()
+ << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " does not accept regex flags for pattern schemas in 'patternProperties'"};
+ }
+
+ patternProperties.emplace_back(
+ InternalSchemaAllowedPropertiesMatchExpression::Pattern(regexElem.regex()),
+ std::move(expressionWithPlaceholder.getValue()));
+ }
+
+ return std::move(patternProperties);
+}
+
+StatusWith<boost::container::flat_set<StringData>> parseProperties(BSONElement propertiesElem) {
+ if (!propertiesElem) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'properties' to be present"};
+ } else if (propertiesElem.type() != BSONType::Array) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'properties' to be an array, not "
+ << propertiesElem.type()};
+ }
+
+ std::vector<StringData> properties;
+ for (auto&& property : propertiesElem.embeddedObject()) {
+ if (property.type() != BSONType::String) {
+ return {
+ ErrorCodes::TypeMismatch,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires 'properties' to be an array of strings, but found a "
+ << property.type()};
+ }
+ properties.push_back(property.valueStringData());
+ }
+
+ return boost::container::flat_set<StringData>(properties.begin(), properties.end());
+}
+} // namespace
+
StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaMatchArrayIndex(
const char* path, const BSONElement& elem, const CollatorInterface* collator) {
if (elem.type() != BSONType::Object) {
@@ -1333,47 +1508,22 @@ StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaMatchArrayI
return index.getStatus();
}
- auto namePlaceholderElem = subobj["namePlaceholder"];
- if (!namePlaceholderElem) {
- return {ErrorCodes::FailedToParse,
- str::stream() << InternalSchemaMatchArrayIndexMatchExpression::kName
- << " requires a 'namePlaceholder'"};
- } else if (namePlaceholderElem.type() != BSONType::String) {
- return {ErrorCodes::TypeMismatch,
- str::stream() << InternalSchemaMatchArrayIndexMatchExpression::kName
- << " requires 'namePlaceholder' to be a string, not "
- << namePlaceholderElem.type()};
- }
-
- auto expressionElem = subobj["expression"];
- if (!expressionElem) {
- return {ErrorCodes::FailedToParse,
- str::stream() << InternalSchemaMatchArrayIndexMatchExpression::kName
- << " requires an 'expression'"};
- } else if (expressionElem.type() != BSONType::Object) {
- return {ErrorCodes::TypeMismatch,
- str::stream() << InternalSchemaMatchArrayIndexMatchExpression::kName
- << " requires 'expression' to be an object, not "
- << expressionElem.type()};
+ auto namePlaceholder = parseNamePlaceholder(
+ subobj, "namePlaceholder"_sd, InternalSchemaMatchArrayIndexMatchExpression::kName);
+ if (!namePlaceholder.isOK()) {
+ return namePlaceholder.getStatus();
}
auto expressionWithPlaceholder =
- ExpressionWithPlaceholder::parse(expressionElem.embeddedObject(), collator);
+ parseExprWithPlaceholder(subobj,
+ "expression"_sd,
+ InternalSchemaMatchArrayIndexMatchExpression::kName,
+ namePlaceholder.getValue(),
+ collator);
if (!expressionWithPlaceholder.isOK()) {
return expressionWithPlaceholder.getStatus();
}
- if (namePlaceholderElem.valueStringData() !=
- expressionWithPlaceholder.getValue()->getPlaceholder()) {
- return {ErrorCodes::FailedToParse,
- str::stream() << InternalSchemaMatchArrayIndexMatchExpression::kName
- << " has a 'namePlaceholder' of '"
- << namePlaceholderElem.valueStringData()
- << "', but 'expression' has a mismatching placeholder '"
- << expressionWithPlaceholder.getValue()->getPlaceholder()
- << "'"};
- }
-
auto matchArrayIndexExpr = stdx::make_unique<InternalSchemaMatchArrayIndexMatchExpression>();
auto initStatus = matchArrayIndexExpr->init(
path, index.getValue(), std::move(expressionWithPlaceholder.getValue()));
@@ -1383,6 +1533,57 @@ StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaMatchArrayI
return {std::move(matchArrayIndexExpr)};
}
+StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaAllowedProperties(
+ const BSONElement& elem, const CollatorInterface* collator) {
+ if (elem.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " must be an object"};
+ }
+
+ auto subobj = elem.embeddedObject();
+ if (subobj.nFields() != 4) {
+ return {ErrorCodes::FailedToParse,
+ str::stream() << InternalSchemaAllowedPropertiesMatchExpression::kName
+ << " requires exactly four fields: 'properties', 'namePlaceholder', "
+ "'patternProperties' and 'otherwise'"};
+ }
+
+ auto namePlaceholder = parseNamePlaceholder(
+ subobj, "namePlaceholder"_sd, InternalSchemaAllowedPropertiesMatchExpression::kName);
+ if (!namePlaceholder.isOK()) {
+ return namePlaceholder.getStatus();
+ }
+
+ auto patternProperties =
+ parsePatternProperties(subobj["patternProperties"], namePlaceholder.getValue(), collator);
+ if (!patternProperties.isOK()) {
+ return patternProperties.getStatus();
+ }
+
+ auto otherwise = parseExprWithPlaceholder(subobj,
+ "otherwise"_sd,
+ InternalSchemaAllowedPropertiesMatchExpression::kName,
+ namePlaceholder.getValue(),
+ collator);
+ if (!otherwise.isOK()) {
+ return otherwise.getStatus();
+ }
+
+ auto properties = parseProperties(subobj["properties"]);
+ if (!properties.isOK()) {
+ return properties.getStatus();
+ }
+
+ auto allowedPropertiesExpr =
+ stdx::make_unique<InternalSchemaAllowedPropertiesMatchExpression>();
+ allowedPropertiesExpr->init(std::move(properties.getValue()),
+ namePlaceholder.getValue(),
+ std::move(patternProperties.getValue()),
+ std::move(otherwise.getValue()));
+ return {std::move(allowedPropertiesExpr)};
+}
+
StatusWithMatchExpression MatchExpressionParser::_parseGeo(const char* name,
PathAcceptingKeyword type,
const BSONObj& section) {
diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h
index 0a58e15ae90..621a35e4451 100644
--- a/src/mongo/db/matcher/expression_parser.h
+++ b/src/mongo/db/matcher/expression_parser.h
@@ -313,6 +313,9 @@ private:
boost::intrusive_ptr<Expression> _parseAggExpression(
BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx);
+ StatusWithMatchExpression _parseInternalSchemaAllowedProperties(
+ const BSONElement& elem, const CollatorInterface* collator);
+
// Performs parsing for the match extensions. We do not own this pointer - it has to live
// as long as the parser is active.
const ExtensionsCallback* _extensionsCallback;
diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp
index 678ea10f9ba..0f08fedebb7 100644
--- a/src/mongo/db/matcher/expression_serialization_test.cpp
+++ b/src/mongo/db/matcher/expression_serialization_test.cpp
@@ -1146,5 +1146,26 @@ TEST(SerializeInternalSchema, ExpressionInternalSchemaMatchArrayIndexSerializesC
"{index: 2, namePlaceholder: 'i', expression: {i: {$lt: 3}}}}}"));
ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression()));
}
+
+TEST(SerializeInternalSchema, ExpressionInternalSchemaAllowedPropertiesSerializesCorrectly) {
+ Matcher original(fromjson(R"({$_internalSchemaAllowedProperties: {
+ properties: ['a'],
+ otherwise: {i: {$gt: 10}},
+ namePlaceholder: 'i',
+ patternProperties: [{regex: /b/, expression: {i: {$type: 'number'}}}]
+ }})"),
+ ExtensionsCallbackDisallowExtensions(),
+ kSimpleCollator);
+ Matcher reserialized(serialize(original.getMatchExpression()),
+ ExtensionsCallbackDisallowExtensions(),
+ kSimpleCollator);
+ ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), fromjson(R"({$_internalSchemaAllowedProperties: {
+ properties: ['a'],
+ namePlaceholder: 'i',
+ patternProperties: [{regex: /b/, expression: {i: {$type: 'number'}}}],
+ otherwise: {i: {$gt: 10}}
+ }})"));
+ ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression()));
+}
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp
new file mode 100644
index 00000000000..21ae9713a1d
--- /dev/null
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp
@@ -0,0 +1,170 @@
+/**
+ * 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_allowed_properties.h"
+
+namespace mongo {
+constexpr StringData InternalSchemaAllowedPropertiesMatchExpression::kName;
+
+void InternalSchemaAllowedPropertiesMatchExpression::init(
+ boost::container::flat_set<StringData> properties,
+ StringData namePlaceholder,
+ std::vector<PatternSchema> patternProperties,
+ std::unique_ptr<ExpressionWithPlaceholder> otherwise) {
+ _properties = std::move(properties);
+ _namePlaceholder = namePlaceholder;
+ _patternProperties = std::move(patternProperties);
+ _otherwise = std::move(otherwise);
+}
+
+void InternalSchemaAllowedPropertiesMatchExpression::debugString(StringBuilder& debug,
+ int level) const {
+ _debugAddSpace(debug, level);
+
+ BSONObjBuilder builder;
+ serialize(&builder);
+ debug << builder.obj().toString() << "\n";
+
+ const auto* tag = getTag();
+ if (tag) {
+ debug << " ";
+ tag->debugString(&debug);
+ }
+
+ debug << "\n";
+}
+
+bool InternalSchemaAllowedPropertiesMatchExpression::equivalent(const MatchExpression* expr) const {
+ if (matchType() != expr->matchType()) {
+ return false;
+ }
+
+ const auto* other = static_cast<const InternalSchemaAllowedPropertiesMatchExpression*>(expr);
+ return _properties == other->_properties && _namePlaceholder == other->_namePlaceholder &&
+ _otherwise->equivalent(other->_otherwise.get()) &&
+ std::is_permutation(_patternProperties.begin(),
+ _patternProperties.end(),
+ other->_patternProperties.begin(),
+ other->_patternProperties.end(),
+ [](const auto& expr1, const auto& expr2) {
+ return expr1.first.rawRegex == expr2.first.rawRegex &&
+ expr1.second->equivalent(expr2.second.get());
+ });
+}
+
+bool InternalSchemaAllowedPropertiesMatchExpression::matches(const MatchableDocument* doc,
+ MatchDetails* details) const {
+ return _matchesBSONObj(doc->toBSON());
+}
+
+bool InternalSchemaAllowedPropertiesMatchExpression::matchesSingleElement(const BSONElement& elem,
+ MatchDetails*) const {
+ if (elem.type() != BSONType::Object) {
+ return false;
+ }
+
+ return _matchesBSONObj(elem.embeddedObject());
+}
+
+bool InternalSchemaAllowedPropertiesMatchExpression::_matchesBSONObj(const BSONObj& obj) const {
+ for (auto&& property : obj) {
+ bool checkOtherwise = true;
+ for (auto&& constraint : _patternProperties) {
+ if (constraint.first.regex->PartialMatch(property.fieldName())) {
+ checkOtherwise = false;
+ if (!constraint.second->getFilter()->matchesSingleElement(property)) {
+ return false;
+ }
+ }
+ }
+
+ if (checkOtherwise &&
+ _properties.find(property.fieldNameStringData()) != _properties.end()) {
+ checkOtherwise = false;
+ }
+
+ if (checkOtherwise && !_otherwise->getFilter()->matchesSingleElement(property)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void InternalSchemaAllowedPropertiesMatchExpression::serialize(BSONObjBuilder* builder) const {
+ BSONObjBuilder expressionBuilder(
+ builder->subobjStart(InternalSchemaAllowedPropertiesMatchExpression::kName));
+
+ BSONArrayBuilder propertiesBuilder(expressionBuilder.subarrayStart("properties"));
+ for (auto&& property : _properties) {
+ propertiesBuilder.append(property);
+ }
+ propertiesBuilder.doneFast();
+
+ expressionBuilder.append("namePlaceholder", _namePlaceholder);
+
+ BSONArrayBuilder patternPropertiesBuilder(expressionBuilder.subarrayStart("patternProperties"));
+ for (auto&& item : _patternProperties) {
+ BSONObjBuilder itemBuilder(patternPropertiesBuilder.subobjStart());
+ itemBuilder.appendRegex("regex", item.first.rawRegex);
+
+ BSONObjBuilder subexpressionBuilder(itemBuilder.subobjStart("expression"));
+ item.second->getFilter()->serialize(&subexpressionBuilder);
+ subexpressionBuilder.doneFast();
+ }
+ patternPropertiesBuilder.doneFast();
+
+ BSONObjBuilder otherwiseBuilder(expressionBuilder.subobjStart("otherwise"));
+ _otherwise->getFilter()->serialize(&otherwiseBuilder);
+ otherwiseBuilder.doneFast();
+ expressionBuilder.doneFast();
+}
+
+std::unique_ptr<MatchExpression> InternalSchemaAllowedPropertiesMatchExpression::shallowClone()
+ const {
+ std::vector<PatternSchema> clonedPatternProperties;
+ clonedPatternProperties.reserve(_patternProperties.size());
+ for (auto&& constraint : _patternProperties) {
+ clonedPatternProperties.emplace_back(Pattern(constraint.first.rawRegex),
+ stdx::make_unique<ExpressionWithPlaceholder>(
+ constraint.second->getPlaceholder().toString(),
+ constraint.second->getFilter()->shallowClone()));
+ }
+
+ auto clonedOtherwise = stdx::make_unique<ExpressionWithPlaceholder>(
+ _otherwise->getPlaceholder().toString(), _otherwise->getFilter()->shallowClone());
+
+ auto clone = stdx::make_unique<InternalSchemaAllowedPropertiesMatchExpression>();
+ clone->init(_properties,
+ _namePlaceholder,
+ std::move(clonedPatternProperties),
+ std::move(clonedOtherwise));
+ return {std::move(clone)};
+}
+} // namespace mongo
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h
new file mode 100644
index 00000000000..ff22aa65836
--- /dev/null
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h
@@ -0,0 +1,161 @@
+/**
+ * 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 <boost/container/flat_set.hpp>
+#include <pcrecpp.h>
+#include <utility>
+#include <vector>
+
+#include "mongo/db/matcher/expression.h"
+#include "mongo/db/matcher/expression_with_placeholder.h"
+#include "mongo/stdx/memory.h"
+
+namespace mongo {
+
+/**
+ * Match expression that matches documents whose properties meet certain requirements based on field
+ * name. Specifically, a document matches if:
+ *
+ * - each field that matches a regular expression in '_patternProperties' also matches the
+ * corresponding match expression; and
+ * - any field not contained in '_properties' nor matching a pattern in '_patternProperties'
+ * matches the '_otherwise' match expression.
+ *
+ * For example, consider the match expression
+ *
+ * {$_internalSchemaAllowedProperties: {
+ * properties: ["address", "phone"],
+ * namePlaceholder: "i",
+ * patternProperties: [
+ * {regex: /[nN]ame/, expression: {i: {$_internalSchemaType: "string"}}},
+ * {regex: /[aA]ddress/, expression: {i: {$_internalSchemaMinLength: 30}}}
+ * ],
+ * otherwise: {i: {$_internalSchemaType: 'number'}}
+ *
+ * Then, given the object
+ *
+ * {
+ * firstName: "juan",
+ * lastName: "de la cruz",
+ * middleName: ["daniel", "marcos"],
+ * phone: 1234567890,
+ * address: "new york, ny",
+ * hobbies: ["programming", "c++"],
+ * socialSecurityNumber: 123456789
+ * }
+ *
+ * we have that
+ *
+ * - "firstName" and "lastName" are valid, because they satisfy the pattern schema corresponding to
+ * to the regular expression /[nN]ame/.
+ * - "middleName" is invalid, because it doesn't satisfy the schema associated with /[nN]ame/.
+ * - "phone" is valid, because it is listed in the "properties" section.
+ * - "address" is invalid even though it is listed in "properties", because it fails to validate
+ * against the pattern schema for /[aA]ddress/.
+ * - "hobbies" is invalid because it is not contained in "properties", not matched by a regex in
+ * "patternProperties", and does not validate against the "otherwise" schema.
+ * - "socialSecurityNumber" is valid because it matches the schema for "otherwise".
+ *
+ * Because there exists at least one field that is invalid, the entire document would fail to
+ * match.
+ */
+class InternalSchemaAllowedPropertiesMatchExpression final : public MatchExpression {
+public:
+ /**
+ * A container for regular expression data. Holds a pcrecpp::RE object, as well as the original
+ * string pattern, which is used for comparisons and serialization.
+ */
+ struct Pattern {
+ explicit Pattern(StringData pattern)
+ : rawRegex(pattern), regex(stdx::make_unique<pcrecpp::RE>(pattern.toString())) {}
+
+ StringData rawRegex;
+ std::unique_ptr<pcrecpp::RE> regex;
+ };
+
+ /**
+ * A PatternSchema is a regular expression paired with an associated match expression, and
+ * represents a constraint in JSON Schema's "patternProperties" keyword.
+ */
+ using PatternSchema = std::pair<Pattern, std::unique_ptr<ExpressionWithPlaceholder>>;
+
+ static constexpr StringData kName = "$_internalSchemaAllowedProperties"_sd;
+
+ explicit InternalSchemaAllowedPropertiesMatchExpression()
+ : MatchExpression(MatchExpression::INTERNAL_SCHEMA_ALLOWED_PROPERTIES) {}
+
+ void init(boost::container::flat_set<StringData> properties,
+ StringData namePlaceholder,
+ std::vector<PatternSchema> patternProperties,
+ std::unique_ptr<ExpressionWithPlaceholder> otherwise);
+
+ void debugString(StringBuilder& debug, int level) const final;
+
+ bool equivalent(const MatchExpression* expr) const final;
+
+ MatchCategory getCategory() const final {
+ return MatchCategory::kOther;
+ }
+
+ /**
+ * The input matches if:
+ *
+ * - it is a document;
+ * - each field that matches a regular expression in '_patternProperties' also matches the
+ * corresponding match expression; and
+ * - any field not contained in '_properties' nor matching a pattern in '_patternProperties'
+ * matches the '_otherwise' match expression.
+ */
+ bool matches(const MatchableDocument* doc, MatchDetails* details) const final;
+ bool matchesSingleElement(const BSONElement& element, MatchDetails* details) const final;
+
+ void serialize(BSONObjBuilder* builder) const final;
+
+ std::unique_ptr<MatchExpression> shallowClone() const final;
+
+private:
+ /**
+ * Helper function for matches() and matchesSingleElement().
+ */
+ bool _matchesBSONObj(const BSONObj& obj) const;
+
+ // The names of the properties are owned by the BSONObj used to create this match expression.
+ // Since that BSONObj must outlive this object, we can safely store StringData.
+ boost::container::flat_set<StringData> _properties;
+
+ // The placeholder used in both '_patternProperties' and '_otherwise'.
+ StringData _namePlaceholder;
+
+ std::vector<PatternSchema> _patternProperties;
+
+ std::unique_ptr<ExpressionWithPlaceholder> _otherwise;
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties_test.cpp
new file mode 100644
index 00000000000..b00d45d9172
--- /dev/null
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties_test.cpp
@@ -0,0 +1,116 @@
+/**
+ * Copyright (C) 2017 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/bson/json.h"
+#include "mongo/db/matcher/expression_parser.h"
+#include "mongo/db/matcher/extensions_callback_disallow_extensions.h"
+#include "mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo {
+constexpr auto kSimpleCollator = nullptr;
+
+TEST(InternalSchemaAllowedPropertiesMatchExpression, MatchesObjectsWithListedProperties) {
+ auto filter = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: ['a', 'b'],"
+ "namePlaceholder: 'i', patternProperties: [], otherwise: {i: 0}}}");
+ auto expr = MatchExpressionParser::parse(
+ filter, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(expr.getStatus());
+
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{a: 1, b: 1}")));
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{a: 1}")));
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{b: 1}")));
+}
+
+TEST(InternalSchemaAllowedPropertiesMatchExpression, MatchesObjectsWithMatchingPatternProperties) {
+ auto filter = fromjson(R"(
+ {$_internalSchemaAllowedProperties: {
+ properties: [],
+ namePlaceholder: 'i',
+ patternProperties: [
+ {regex: /s$/, expression: {i: {$gt: 0}}},
+ {regex: /[nN]um/, expression: {i: {$type: 'number'}}}
+ ],
+ otherwise: {i: {$type: 'string'}}
+ }})");
+ auto expr = MatchExpressionParser::parse(
+ filter, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(expr.getStatus());
+
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{puppies: 2, kittens: 3, phoneNum: 1234}")));
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{puppies: 2}")));
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{phoneNum: 1234}")));
+}
+
+TEST(InternalSchemaAllowedPropertiesMatchExpression,
+ PatternPropertiesStillEnforcedEvenIfFieldListedInProperties) {
+ auto filter = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: ['a'], namePlaceholder: 'a',"
+ "patternProperties: [{regex: /a/, expression: {a: {$gt: 5}}}], otherwise: {a: 0}}}");
+ auto expr = MatchExpressionParser::parse(
+ filter, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(expr.getStatus());
+
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{a: 6}")));
+ ASSERT_FALSE(expr.getValue()->matchesBSON(fromjson("{a: 5}")));
+ ASSERT_FALSE(expr.getValue()->matchesBSON(fromjson("{a: 4}")));
+}
+
+TEST(InternalSchemaAllowedPropertiesMatchExpression, OtherwiseEnforcedWhenAppropriate) {
+ auto filter = fromjson(R"(
+ {$_internalSchemaAllowedProperties: {
+ properties: [],
+ namePlaceholder: 'i',
+ patternProperties: [
+ {regex: /s$/, expression: {i: {$gt: 0}}},
+ {regex: /[nN]um/, expression: {i: {$type: 'number'}}}
+ ],
+ otherwise: {i: {$type: 'string'}}
+ }})");
+ auto expr = MatchExpressionParser::parse(
+ filter, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(expr.getStatus());
+
+ ASSERT_TRUE(expr.getValue()->matchesBSON(fromjson("{foo: 'bar'}")));
+ ASSERT_FALSE(expr.getValue()->matchesBSON(fromjson("{foo: 7}")));
+}
+
+TEST(InternalSchemaAllowedPropertiesMatchExpression, EquivalentToClone) {
+ auto filter = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: ['a'], namePlaceholder: 'i',"
+ "patternProperties: [{regex: /a/, expression: {i: 1}}], otherwise: {i: 7}}}");
+ auto expr = MatchExpressionParser::parse(
+ filter, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(expr.getStatus());
+ auto clone = expr.getValue()->shallowClone();
+ ASSERT_TRUE(expr.getValue()->equivalent(clone.get()));
+}
+} // namespace mongo
diff --git a/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp
index 8578898b1aa..74bded78933 100644
--- a/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp
+++ b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp
@@ -671,5 +671,272 @@ TEST(MatchExpressionParserSchemaTest, InternalTypeCanParseLongCode) {
ASSERT_FALSE(typeExpr->matchesAllNumbers());
ASSERT_EQ(typeExpr->getBSONType(), BSONType::NumberLong);
}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfAFieldIsMissing) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{namePlaceholder: 'i', patternProperties: [], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], patternProperties: [], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: []}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfNamePlaceholderNotAString) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 7, patternProperties: [], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: /i/, patternProperties: [], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfNamePlaceholderNotValid) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'Capital',"
+ "patternProperties: [], otherwise: {Capital: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::BadValue);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: '', patternProperties: [], otherwise: {'': 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::BadValue);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfPropertiesNotAllStrings) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [7], namePlaceholder: 'i', patternProperties: [], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: ['x', {}], namePlaceholder: 'i',"
+ "patternProperties: [], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+}
+
+TEST(MatchExpressionParserSchemaTest,
+ AllowedPropertiesFailsParsingIfPatternPropertiesNotAllObjects) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: ['blah'], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 1}, patternProperties: [{regex: /a/, expression: {i: 0}}, 'blah']}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+}
+
+TEST(MatchExpressionParserSchemaTest,
+ AllowedPropertiesFailsParsingIfPatternPropertiesHasUnknownFields) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "patternProperties: [{foo: 1, bar: 1}], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "patternProperties: [{regex: /a/, blah: 0}], otherwise: {i: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+}
+
+TEST(MatchExpressionParserSchemaTest,
+ AllowedPropertiesFailsParsingIfPatternPropertiesRegexMissingOrWrongType) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 0}, patternProperties: [{expression: {i: 0}}]}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 0}, patternProperties: [{regex: 7, expression: {i: 0}}]}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 0}, patternProperties: [{regex: 'notARegex', expression: {i: 0}}]}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+}
+
+TEST(MatchExpressionParserSchemaTest,
+ AllowedPropertiesFailsParsingIfPatternPropertiesExpressionInvalid) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 0}, patternProperties: [{regex: /a/}]}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 0}, patternProperties: [{regex: /a/, expression: 'blah'}]}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+}
+
+TEST(MatchExpressionParserSchemaTest,
+ AllowedPropertiesFailsParsingIfPatternPropertiesRegexHasFlags) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "otherwise: {i: 0}, patternProperties: [{regex: /a/i, expression: {i: 0}}]}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::BadValue);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfMismatchingNamePlaceholders) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: [], otherwise: {j: 1}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "patternProperties: [{regex: /a/, expression: {w: 7}}], otherwise: {i: 'foo'}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfOtherwiseIncorrectType) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: [], otherwise: false}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: [], otherwise: [{i: 7}]}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::TypeMismatch);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesFailsParsingIfOtherwiseNotAValidExpression) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: [], namePlaceholder: 'i',"
+ "patternProperties: [], otherwise: {i: {$invalid: 1}}}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::BadValue);
+
+ query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: [], otherwise: {}}}}}");
+ ASSERT_EQ(
+ MatchExpressionParser::parse(query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator)
+ .getStatus(),
+ ErrorCodes::FailedToParse);
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesParsesSuccessfully) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties: {properties: ['phoneNumber', 'address'],"
+ "namePlaceholder: 'i', otherwise: {i: {$gt: 10}},"
+ "patternProperties: [{regex: /[nN]umber/, expression: {i: {$type: 'number'}}}]}}}");
+ auto allowedProperties = MatchExpressionParser::parse(
+ query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(allowedProperties.getStatus());
+
+ ASSERT_TRUE(allowedProperties.getValue()->matchesBSON(
+ fromjson("{phoneNumber: 123, address: 'earth'}")));
+ ASSERT_TRUE(allowedProperties.getValue()->matchesBSON(
+ fromjson("{phoneNumber: 3.14, workNumber: 456}")));
+
+ ASSERT_FALSE(allowedProperties.getValue()->matchesBSON(fromjson("{otherNumber: 'blah'}")));
+ ASSERT_FALSE(allowedProperties.getValue()->matchesBSON(fromjson("{phoneNumber: 'blah'}")));
+ ASSERT_FALSE(allowedProperties.getValue()->matchesBSON(fromjson("{other: 'blah'}")));
+}
+
+TEST(MatchExpressionParserSchemaTest, AllowedPropertiesAcceptsEmptyPropertiesAndPatternProperties) {
+ auto query = fromjson(
+ "{$_internalSchemaAllowedProperties:"
+ "{properties: [], namePlaceholder: 'i', patternProperties: [], otherwise: {i: 1}}}}");
+ auto allowedProperties = MatchExpressionParser::parse(
+ query, ExtensionsCallbackDisallowExtensions(), kSimpleCollator);
+ ASSERT_OK(allowedProperties.getStatus());
+
+ ASSERT_TRUE(allowedProperties.getValue()->matchesBSON(BSONObj()));
+}
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp
index 1834fa2ba28..14625fc903e 100644
--- a/src/mongo/db/query/plan_cache.cpp
+++ b/src/mongo/db/query/plan_cache.cpp
@@ -177,6 +177,9 @@ const char* encodeMatchType(MatchExpression::MatchType mt) {
case MatchExpression::INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX:
return "internalSchemaAllElemMatchFromIndex";
+ case MatchExpression::INTERNAL_SCHEMA_ALLOWED_PROPERTIES:
+ return "internalSchemaAllowedProperties";
+
case MatchExpression::INTERNAL_SCHEMA_COND:
return "internalSchemaCond";