diff options
author | Kyle Suarez <kyle.suarez@mongodb.com> | 2017-08-22 18:22:12 -0400 |
---|---|---|
committer | Kyle Suarez <kyle.suarez@mongodb.com> | 2017-08-22 18:22:12 -0400 |
commit | 1bba024b024acf6d578796b53b26f5ba6fafd2c6 (patch) | |
tree | 5cfecdf5c13009298576766a16012919c42091ad | |
parent | 3ac3be976459d6499bda024fd17388877947d368 (diff) | |
download | mongo-1bba024b024acf6d578796b53b26f5ba6fafd2c6.tar.gz |
SERVER-29582 create a $_internalSchemaAllowedProperties match expression
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"; |