diff options
Diffstat (limited to 'src/mongo/db/matcher')
-rw-r--r-- | src/mongo/db/matcher/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error.cpp | 249 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp | 444 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_test.cpp | 283 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_test.h | 45 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_util.cpp | 62 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_util.h | 55 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_always_boolean.h | 5 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.cpp | 257 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_type.h | 11 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp | 11 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/expression_internal_schema_object_match.h | 4 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 197 |
13 files changed, 1201 insertions, 424 deletions
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index 7ef7f899c9a..10977b44289 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -20,6 +20,7 @@ env.Library( target='expressions', source=[ 'doc_validation_error.cpp', + 'doc_validation_util.cpp', 'expression.cpp', 'expression_algo.cpp', 'expression_array.cpp', @@ -97,6 +98,7 @@ env.Library( env.CppUnitTest( target='db_matcher_test', source=[ + 'doc_validation_error_json_schema_test.cpp', 'doc_validation_error_test.cpp', 'expression_algo_test.cpp', 'expression_always_boolean_test.cpp', diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp index 52c3aaed3c3..f3e84fef1ae 100644 --- a/src/mongo/db/matcher/doc_validation_error.cpp +++ b/src/mongo/db/matcher/doc_validation_error.cpp @@ -44,6 +44,7 @@ #include "mongo/db/matcher/expression_type.h" #include "mongo/db/matcher/expression_visitor.h" #include "mongo/db/matcher/match_expression_walker.h" +#include "mongo/db/matcher/schema/expression_internal_schema_object_match.h" namespace mongo::doc_validation_error { namespace { @@ -51,6 +52,7 @@ MONGO_INIT_REGISTER_ERROR_EXTRA_INFO(DocumentValidationFailureInfo); using ErrorAnnotation = MatchExpression::ErrorAnnotation; using AnnotationMode = ErrorAnnotation::Mode; +using LeafArrayBehavior = ElementPath::LeafArrayBehavior; /** * Enumerated type which describes whether an error should be described normally or in an @@ -80,7 +82,8 @@ struct ValidationErrorFrame { kErrorNeedChildrenInfo, }; - ValidationErrorFrame(RuntimeState runtimeState) : runtimeState(runtimeState) {} + ValidationErrorFrame(RuntimeState runtimeState, BSONObj currentDoc) + : runtimeState(runtimeState), currentDoc(std::move(currentDoc)) {} // BSONBuilders which construct the generated error. BSONObjBuilder objBuilder; @@ -89,6 +92,8 @@ struct ValidationErrorFrame { size_t childIndex = 0; // Tracks runtime information about how the current node should generate an error. RuntimeState runtimeState; + // Tracks the current subdocument that an error should be generated over. + BSONObj currentDoc; }; using RuntimeState = ValidationErrorFrame::RuntimeState; @@ -97,38 +102,42 @@ using RuntimeState = ValidationErrorFrame::RuntimeState; * A struct which tracks context during error generation. */ struct ValidationErrorContext { - ValidationErrorContext(const MatchableDocument* doc) : doc(doc) {} + ValidationErrorContext(const BSONObj& rootDoc) : rootDoc(rootDoc) {} /** * Utilities which add/remove ValidationErrorFrames from 'frames'. */ - void pushNewFrame(const MatchExpression& expr) { + void pushNewFrame(const MatchExpression& expr, const BSONObj& subDoc) { // Clear the last error that was generated. latestCompleteError = BSONObj(); + + // If this is the first frame, then we know that we've failed validation, so we must be + // generating an error. if (frames.empty()) { - // If this is the first frame, then we know that we've failed validation, so we must be - // generating an error. - frames.emplace(RuntimeState::kError); + frames.emplace(RuntimeState::kError, subDoc); return; } + auto parentRuntimeState = getCurrentRuntimeState(); + // If we've determined at runtime or at parse time that this node shouldn't contribute to // error generation, then push a frame indicating that this node should not produce an // error and return. if (parentRuntimeState == RuntimeState::kNoError || expr.getErrorAnnotation()->mode == AnnotationMode::kIgnore) { - frames.emplace(RuntimeState::kNoError); + frames.emplace(RuntimeState::kNoError, subDoc); return; } // If our parent needs more information, call 'matches()' to determine whether we are // contributing to error output. if (parentRuntimeState == RuntimeState::kErrorNeedChildrenInfo) { - bool generateErrorValue = expr.matches(doc) ? inversion == InvertError::kInverted - : inversion == InvertError::kNormal; - frames.emplace(generateErrorValue ? RuntimeState::kError : RuntimeState::kNoError); + bool generateErrorValue = expr.matchesBSON(subDoc) ? inversion == InvertError::kInverted + : inversion == InvertError::kNormal; + frames.emplace(generateErrorValue ? RuntimeState::kError : RuntimeState::kNoError, + subDoc); return; } - frames.emplace(RuntimeState::kError); + frames.emplace(RuntimeState::kError, subDoc); } void popFrame() { invariant(!frames.empty()); @@ -158,9 +167,20 @@ struct ValidationErrorContext { invariant(!frames.empty()); return frames.top().runtimeState; } - RuntimeState setCurrentRuntimeState(RuntimeState runtimeState) { + void setCurrentRuntimeState(RuntimeState runtimeState) { invariant(!frames.empty()); - return frames.top().runtimeState = runtimeState; + + // If a node has RuntimeState::kNoError, then its runtime state value should never be + // modified since the node should never contribute to error generation. + if (getCurrentRuntimeState() != RuntimeState::kNoError) { + frames.top().runtimeState = runtimeState; + } + } + const BSONObj& getCurrentDocument() { + if (!frames.empty()) { + return frames.top().currentDoc; + } + return rootDoc; } BSONObj getLatestCompleteError() const { return latestCompleteError; @@ -200,7 +220,7 @@ struct ValidationErrorContext { // Tracks the most recently completed error. The final error will be stored here. BSONObj latestCompleteError; // Document which failed to match against the collection's validator. - const MatchableDocument* doc; + const BSONObj& rootDoc; // Tracks whether the generated error should be described normally or in an inverted context. InvertError inversion = InvertError::kNormal; }; @@ -213,10 +233,18 @@ void finishLogicalOperatorChildError(const ListOfMatchExpression* expr, ValidationErrorContext* ctx) { BSONObj childError = ctx->latestCompleteError; if (!childError.isEmpty() && ctx->shouldGenerateError(*expr)) { - BSONObjBuilder subBuilder = ctx->getCurrentArrayBuilder().subobjStart(); - subBuilder.appendNumber("index", ctx->getCurrentChildIndex()); - subBuilder.append("details", childError); - subBuilder.done(); + auto operatorName = expr->getErrorAnnotation()->operatorName; + + // Only provide the indexes of non-matching clauses for explicit $and/$or/$nor in the + // user's query. + if (operatorName == "$and" || operatorName == "$or" || operatorName == "$nor") { + BSONObjBuilder subBuilder = ctx->getCurrentArrayBuilder().subobjStart(); + subBuilder.appendNumber("index", ctx->getCurrentChildIndex()); + subBuilder.append("details", childError); + subBuilder.done(); + } else { + ctx->getCurrentArrayBuilder().append(childError); + } } ctx->incrementCurrentChildIndex(); } @@ -228,20 +256,11 @@ class ValidationErrorPreVisitor final : public MatchExpressionConstVisitor { public: ValidationErrorPreVisitor(ValidationErrorContext* context) : _context(context) {} void visit(const AlwaysFalseMatchExpression* expr) final { - _context->pushNewFrame(*expr); - if (_context->shouldGenerateError(*expr)) { - appendErrorDetails(*expr); - static constexpr auto kNormalReason = "expression always evaluates to false"; - // It is not possible to generate an error for 'AlwaysFalseMatchExpression' in an - // inverted context, since this means that an ancestor of this expression is either a - // 'not' or a 'nor', in which case the either the ancestor expression evaluates to true - // or this expression will not have contributed to the failure of its ancestor. - invariant(_context->inversion == InvertError::kNormal); - static constexpr auto kInvertedReason = ""; - appendErrorReason(*expr, kNormalReason, kInvertedReason); - } + generateAlwaysBooleanError(*expr); + } + void visit(const AlwaysTrueMatchExpression* expr) final { + generateAlwaysBooleanError(*expr); } - void visit(const AlwaysTrueMatchExpression* expr) final {} void visit(const AndMatchExpression* expr) final { // $all is treated as a leaf operator. if (expr->getErrorAnnotation()->operatorName == "$all") { @@ -279,7 +298,7 @@ public: void visit(const ExistsMatchExpression* expr) final { static constexpr auto normalReason = "path does not exist"; static constexpr auto invertedReason = "path does exist"; - _context->pushNewFrame(*expr); + _context->pushNewFrame(*expr, _context->getCurrentDocument()); if (_context->shouldGenerateError(*expr)) { appendErrorDetails(*expr); appendErrorReason(*expr, normalReason, invertedReason); @@ -288,7 +307,7 @@ public: void visit(const ExprMatchExpression* expr) final { static constexpr auto normalReason = "$expr did not match"; static constexpr auto invertedReason = "$expr did match"; - _context->pushNewFrame(*expr); + _context->pushNewFrame(*expr, _context->getCurrentDocument()); if (_context->shouldGenerateError(*expr)) { appendErrorDetails(*expr); appendErrorReason(*expr, normalReason, invertedReason); @@ -349,9 +368,41 @@ public: void visit(const InternalSchemaMinItemsMatchExpression* expr) final {} void visit(const InternalSchemaMinLengthMatchExpression* expr) final {} void visit(const InternalSchemaMinPropertiesMatchExpression* expr) final {} - void visit(const InternalSchemaObjectMatchExpression* expr) final {} + void visit(const InternalSchemaObjectMatchExpression* expr) final { + // This node should never be responsible for generating an error directly. + invariant(expr->getErrorAnnotation()->mode != AnnotationMode::kGenerateError); + BSONObj subDocument = _context->getCurrentDocument(); + ElementPath path(expr->path(), LeafArrayBehavior::kNoTraversal); + BSONMatchableDocument doc(_context->getCurrentDocument()); + MatchableDocument::IteratorHolder cursor(&doc, &path); + invariant(cursor->more()); + auto elem = cursor->next().element(); + + // If we do not find an object at expr's path, then the subtree rooted at this node will + // not contribute to error generation as there will either be an explicit + // ExistsMatchExpression which will explain a missing path error or an explicit + // InternalSchemaTypeExpression that will explain a type did not match error. + bool ignoreSubTree = false; + if (elem.type() == BSONType::Object) { + subDocument = elem.embeddedObject(); + } else { + ignoreSubTree = true; + } + + // This expression should match exactly one object; if there are any more elements, then + // ignore the subtree. + if (cursor->more()) { + ignoreSubTree = true; + } + _context->pushNewFrame(*expr, subDocument); + if (ignoreSubTree) { + _context->setCurrentRuntimeState(RuntimeState::kNoError); + } + } void visit(const InternalSchemaRootDocEqMatchExpression* expr) final {} - void visit(const InternalSchemaTypeExpression* expr) final {} + void visit(const InternalSchemaTypeExpression* expr) final { + generateTypeError(expr, LeafArrayBehavior::kNoTraversal); + } void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final {} void visit(const InternalSchemaXorMatchExpression* expr) final {} void visit(const LTEMatchExpression* expr) final { @@ -407,17 +458,7 @@ public: } void visit(const TwoDPtInAnnulusExpression* expr) final {} void visit(const TypeMatchExpression* expr) final { - static constexpr auto normalReason = "no matching types found for specified typeset"; - static constexpr auto invertedReason = "matching types found for specified typeset"; - _context->pushNewFrame(*expr); - if (_context->shouldGenerateError(*expr)) { - appendErrorDetails(*expr); - BSONArray attributeValues = createValuesArray(expr->path()); - appendMissingField(attributeValues); - appendErrorReason(*expr, normalReason, invertedReason); - appendConsideredValues(attributeValues); - appendConsideredTypes(attributeValues); - } + generateTypeError(expr, LeafArrayBehavior::kTraverse); } void visit(const WhereMatchExpression* expr) final { MONGO_UNREACHABLE; @@ -429,7 +470,11 @@ public: private: // Set of utilities responsible for appending various fields to build a descriptive error. void appendOperatorName(const ErrorAnnotation& annotation, BSONObjBuilder* bob) { - bob->append("operatorName", annotation.operatorName); + auto operatorName = annotation.operatorName; + // Only append the operator name if 'annotation' has one. + if (!operatorName.empty()) { + bob->append("operatorName", operatorName); + } } void appendSpecifiedAs(const ErrorAnnotation& annotation, BSONObjBuilder* bob) { bob->append("specifiedAs", annotation.annotation); @@ -442,7 +487,8 @@ private: } BSONArray createValuesArray(const ElementPath& path) { - MatchableDocument::IteratorHolder cursor(_context->doc, &path); + BSONMatchableDocument doc(_context->getCurrentDocument()); + MatchableDocument::IteratorHolder cursor(&doc, &path); BSONArrayBuilder bab; while (cursor->more()) { auto elem = cursor->next().element(); @@ -480,7 +526,7 @@ private: return; // an element has one of the expected types } } - bob.append("reason", "type mismatch"); + bob.append("reason", "type did not match"); appendConsideredTypes(arr); std::set<std::string> types; for (auto&& elem : *expectedTypes) { @@ -538,8 +584,8 @@ private: * BSONObjBuilder tracked by '_context' describing why the document failed to match against * 'expr'. In particular: * - Appends "reason: field was missing" if expr's path is missing from the document. - * - Appends "reason: type mismatch" along with 'expectedTypes' and 'consideredTypes' if none - * of the values at expr's path match any of the types specified in 'expectedTypes'. + * - Appends "reason: type did not match" along with 'expectedTypes' and 'consideredTypes' if + * none of the values at expr's path match any of the types specified in 'expectedTypes'. * - Appends the specified 'reason' along with 'consideredValue' if the 'path' in the * document resolves to a single value. * - Appends the specified 'reason' along with 'consideredValues' if the 'path' in the @@ -549,9 +595,8 @@ private: const std::string& normalReason, const std::string& invertedReason, const std::set<BSONType>* expectedTypes = nullptr, - ElementPath::LeafArrayBehavior leafArrayBehavior = - ElementPath::LeafArrayBehavior::kTraverse) { - _context->pushNewFrame(expr); + LeafArrayBehavior leafArrayBehavior = LeafArrayBehavior::kTraverse) { + _context->pushNewFrame(expr, _context->getCurrentDocument()); if (_context->shouldGenerateError(expr)) { appendErrorDetails(expr); ElementPath path(expr.path(), leafArrayBehavior); @@ -579,11 +624,24 @@ private: const std::string& normalReason, const std::string& invertedReason) { static const std::set<BSONType> expectedTypes{BSONType::Array}; - generatePathError(*expr, - normalReason, - invertedReason, - &expectedTypes, - ElementPath::LeafArrayBehavior::kNoTraversal); + generatePathError( + *expr, normalReason, invertedReason, &expectedTypes, LeafArrayBehavior::kNoTraversal); + } + + template <class T> + void generateTypeError(const TypeMatchExpressionBase<T>* expr, LeafArrayBehavior behavior) { + _context->pushNewFrame(*expr, _context->getCurrentDocument()); + static constexpr auto kNormalReason = "type did not match"; + static constexpr auto kInvertedReason = "type did match"; + if (_context->shouldGenerateError(*expr)) { + appendErrorDetails(*expr); + ElementPath path(expr->path(), behavior); + BSONArray arr = createValuesArray(path); + appendMissingField(arr); + appendErrorReason(*expr, kNormalReason, kInvertedReason); + appendConsideredValues(arr); + appendConsideredTypes(arr); + } } /** * Generates a document validation error for a bit test expression 'expr'. @@ -604,10 +662,11 @@ private: */ void preVisitTreeOperator(const MatchExpression* expr) { invariant(expr->numChildren() > 0); - _context->pushNewFrame(*expr); + _context->pushNewFrame(*expr, _context->getCurrentDocument()); if (_context->shouldGenerateError(*expr)) { auto annotation = expr->getErrorAnnotation(); appendOperatorName(*annotation, &_context->getCurrentObjBuilder()); + _context->getCurrentObjBuilder().appendElements(annotation->annotation); } } /** @@ -616,14 +675,14 @@ private: * reporting. */ void processAll(const AndMatchExpression& expr) { - _context->pushNewFrame(expr); + _context->pushNewFrame(expr, _context->getCurrentDocument()); if (_context->shouldGenerateError(expr)) { invariant(expr.numChildren() > 0); appendErrorDetails(expr); auto childExpr = expr.getChild(0); static constexpr auto kNormalReason = "array did not contain all specified values"; static constexpr auto kInvertedReason = "array did contain all specified values"; - ElementPath path(childExpr->path(), ElementPath::LeafArrayBehavior::kNoTraversal); + ElementPath path(childExpr->path(), LeafArrayBehavior::kNoTraversal); auto arr = createValuesArray(path); appendMissingField(arr); appendErrorReason(expr, kNormalReason, kInvertedReason); @@ -631,6 +690,29 @@ private: } } + /** + * For an AlwaysBooleanMatchExpression, we simply output the error information obtained at + * parse time. + */ + void generateAlwaysBooleanError(const AlwaysBooleanMatchExpression& expr) { + _context->pushNewFrame(expr, _context->getCurrentDocument()); + if (_context->shouldGenerateError(expr)) { + // An AlwaysBooleanMatchExpression can only contribute to error generation when the + // inversion matches the value of the 'expr'. More precisely, it is only possible + // to generate an error for 'expr' if it evaluates to false in a normal context or + // if it evaluates to true an inverted context. + if (expr.isTriviallyFalse()) { + invariant(_context->inversion == InvertError::kNormal); + } else { + invariant(_context->inversion == InvertError::kInverted); + } + appendErrorDetails(expr); + static constexpr auto kNormalReason = "expression always evaluates to false"; + static constexpr auto kInvertedReason = "expression always evaluates to true"; + appendErrorReason(expr, kNormalReason, kInvertedReason); + } + } + ValidationErrorContext* _context; }; @@ -724,14 +806,27 @@ public: void visit(const AlwaysFalseMatchExpression* expr) final { _context->finishCurrentError(expr); } - void visit(const AlwaysTrueMatchExpression* expr) final {} + void visit(const AlwaysTrueMatchExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const AndMatchExpression* expr) final { auto operatorName = expr->getErrorAnnotation()->operatorName; - if (operatorName == "$all") { + // Clean up the frame for this node if we're finishing the error for an $all or this node + // shouldn't generate an error. + if (operatorName == "$all" || !_context->shouldGenerateError(*expr)) { _context->finishCurrentError(expr); - } else { - postVisitTreeOperator(expr); + return; } + // Specify a different details string based on the operatorName. Note that if our node + // doesn't have an operator name specified, the default reason string is 'details'. + static const StringMap<std::string> detailsStringMap = { + {"$and", "clausesNotSatisfied"}, + {"properties", "propertiesNotSatisfied"}, + {"$jsonSchema", "schemaRulesNotSatisfied"}, + {"", "details"}}; + auto detailsString = detailsStringMap.find(operatorName); + invariant(detailsString != detailsStringMap.end()); + postVisitTreeOperator(expr, detailsString->second); } void visit(const BitsAllClearMatchExpression* expr) final { _context->finishCurrentError(expr); @@ -790,9 +885,13 @@ public: void visit(const InternalSchemaMinItemsMatchExpression* expr) final {} void visit(const InternalSchemaMinLengthMatchExpression* expr) final {} void visit(const InternalSchemaMinPropertiesMatchExpression* expr) final {} - void visit(const InternalSchemaObjectMatchExpression* expr) final {} + void visit(const InternalSchemaObjectMatchExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const InternalSchemaRootDocEqMatchExpression* expr) final {} - void visit(const InternalSchemaTypeExpression* expr) final {} + void visit(const InternalSchemaTypeExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final {} void visit(const InternalSchemaXorMatchExpression* expr) final {} void visit(const LTEMatchExpression* expr) final { @@ -806,7 +905,8 @@ public: } void visit(const NorMatchExpression* expr) final { _context->flipInversion(); - postVisitTreeOperator(expr); + static constexpr auto detailsString = "clausesNotSatisfied"; + postVisitTreeOperator(expr, detailsString); } void visit(const NotMatchExpression* expr) final { _context->flipInversion(); @@ -816,7 +916,8 @@ public: _context->finishCurrentError(expr); } void visit(const OrMatchExpression* expr) final { - postVisitTreeOperator(expr); + static constexpr auto detailsString = "clausesNotSatisfied"; + postVisitTreeOperator(expr, detailsString); } void visit(const RegexMatchExpression* expr) final { _context->finishCurrentError(expr); @@ -842,11 +943,12 @@ public: } private: - void postVisitTreeOperator(const ListOfMatchExpression* expr) { + void postVisitTreeOperator(const ListOfMatchExpression* expr, + const std::string& detailsString) { finishLogicalOperatorChildError(expr, _context); if (_context->shouldGenerateError(*expr)) { auto failedClauses = _context->getCurrentArrayBuilder().arr(); - _context->getCurrentObjBuilder().append("clausesNotSatisfied", failedClauses); + _context->getCurrentObjBuilder().append(detailsString, failedClauses); } _context->finishCurrentError(expr); } @@ -886,8 +988,7 @@ const BSONObj& DocumentValidationFailureInfo::getDetails() const { return _details; } BSONObj generateError(const MatchExpression& validatorExpr, const BSONObj& doc) { - BSONMatchableDocument matchableDoc(doc); - ValidationErrorContext context(&matchableDoc); + ValidationErrorContext context(doc); ValidationErrorPreVisitor preVisitor{&context}; ValidationErrorInVisitor inVisitor{&context}; ValidationErrorPostVisitor postVisitor{&context}; diff --git a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp new file mode 100644 index 00000000000..7de748f1f43 --- /dev/null +++ b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp @@ -0,0 +1,444 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/doc_validation_error_test.h" + +namespace mongo { +namespace { + +// properties +TEST(JSONSchemaValidation, BasicProperties) { + BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'minimum': 1}}}}}"); + BSONObj document = fromjson("{a: 0}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'minimum'," + " 'specifiedAs': {'minimum' : 1}," + " 'reason': 'comparison failed'," + " 'consideredValue': 0}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// Exclusive minimum/maximum +TEST(JSONSchemaValidation, ExclusiveMinimum) { + BSONObj query = fromjson( + "{'$jsonSchema': {'properties': {'a': {'minimum': 1, 'exclusiveMinimum': true}}}}}"); + BSONObj document = fromjson("{a: 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'minimum'," + " 'specifiedAs': {'minimum' : 1, 'exclusiveMinimum': true}," + " 'reason': 'comparison failed'," + " 'consideredValue': 1}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinimumAtTopLevelHasNoEffect) { + BSONObj query = fromjson("{'$nor': [{'$jsonSchema': {'minimum': 1}}]}"); + BSONObj document = fromjson("{a: 2}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$nor'," + " 'clausesNotSatisfied': [{'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'minimum', " + " 'specifiedAs': {'minimum': 1}, " + " 'reason': 'expression always evaluates to true'}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, ExclusiveMaximum) { + BSONObj query = fromjson( + "{'$jsonSchema': {'properties': {'a': {'maximum': 1, 'exclusiveMaximum': true}}}}}"); + BSONObj document = fromjson("{a: 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'maximum'," + " 'specifiedAs': {'maximum' : 1, 'exclusiveMaximum': true}," + " 'reason': 'comparison failed'," + " 'consideredValue': 1}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaximumAtTopLevelHasNoEffect) { + BSONObj query = fromjson("{'$nor': [{'$jsonSchema': {'maximum': 1}}]}"); + BSONObj document = fromjson("{a: 2}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$nor'," + " 'clausesNotSatisfied': [{'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'maximum', " + " 'specifiedAs': {'maximum': 1}, " + " 'reason': 'expression always evaluates to true'}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, NestedProperties) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'properties': " + " {'b': {'minimum': 1}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 0}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'minimum'," + " 'specifiedAs': {'minimum': 1}," + " 'reason': 'comparison failed'," + " 'consideredValue': 0}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, NestedPropertiesMultipleFailingClauses) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'properties': " + " {'b': {'minimum': 10, 'maximum': -10}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 5}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'maximum'," + " 'specifiedAs': {'maximum': -10}," + " 'reason': 'comparison failed'," + " 'consideredValue': 5}," + " {'operatorName': 'minimum'," + " 'specifiedAs': {'minimum': 10}," + " 'reason': 'comparison failed'," + " 'consideredValue': 5}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, InternalSchemaObjectMatchDottedPathsHandledCorrectly) { + BSONObj query = fromjson("{'a.b': {'$_internalSchemaObjectMatch': {'c': 1}}}"); + BSONObj document = fromjson("{'a': {'b': {'c': 0}}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$eq'," + " 'specifiedAs': {'c' : 1}," + "'reason': 'comparison failed'," + "'consideredValue': 0}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PropertiesObjectTypeMismatch) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'type': 'object', 'properties': {" + " 'b': {'minimum': 10}}}}}}"); + BSONObj document = fromjson("{'a': 'eleven'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'object'}," + " 'reason': 'type did not match'," + " 'consideredValue': 'eleven'," + " 'consideredType': 'string'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, TypeRestrictionContradictsSpecifiedType) { + BSONObj query = + fromjson("{$jsonSchema: {type: 'object', properties: {a: {type: 'string', minimum: 5}}}}"); + BSONObj document = fromjson("{a: 6}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'string'}," + " 'reason': 'type did not match'," + " 'consideredValue': 6," + " 'consideredType': 'int'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleNestedProperties) { + BSONObj query = fromjson( + "{'$jsonSchema': { " + " 'properties': {" + " 'a': {'properties': {'b': {'minimum': 1}, 'c': {'minimum': 10}}}," + " 'd': {'properties': {'e': {'minimum': 50}, 'f': {'minimum': 100}}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 0, 'c': 5}, 'd': {'e': 25, 'f': 50}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'minimum'," + " 'specifiedAs': {'minimum': 1}," + " 'reason': 'comparison failed'," + " 'consideredValue': 0}]}," + " {'propertyName': 'c', " + " 'details': [" + " {'operatorName': 'minimum'," + " 'specifiedAs': {'minimum': 10}," + " 'reason': 'comparison failed'," + " 'consideredValue': 5}]}]}]}," + " {'propertyName': 'd', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'e', " + " 'details': [" + " {'operatorName': 'minimum'," + " 'specifiedAs': {'minimum': 50}," + " 'reason': 'comparison failed'," + " 'consideredValue': 25}]}," + " {'propertyName': 'f', " + " 'details': [" + " {'operatorName': 'minimum'," + " 'specifiedAs': {'minimum': 100}," + " 'reason': 'comparison failed'," + " 'consideredValue': 50}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} +TEST(JSONSchemaValidation, JSONSchemaAndQueryOperators) { + BSONObj query = fromjson( + "{$and: [" + "{'$jsonSchema': {'properties': {'a': {'minimum': 1}}}}," + "{'$jsonSchema': {'properties': {'b': {'minimum': 1}}}}," + "{'$jsonSchema': {'properties': {'c': {'minimum': 1}}}}]}"); + BSONObj document = fromjson("{a: 0, b: 2, c: -1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$and'," + " 'clausesNotSatisfied': [" + " {'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'minimum'," + " 'specifiedAs': {'minimum' : 1}," + " 'reason': 'comparison failed'," + " 'consideredValue': 0}]}]}]}}," + " {'index': 2, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'c', 'details': " + " [{'operatorName': 'minimum'," + " 'specifiedAs': {'minimum' : 1}," + " 'reason': 'comparison failed'," + " 'consideredValue': -1}]}]}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, NoTopLevelObjectTypeRejectsAllDocuments) { + BSONObj query = fromjson( + " {'$jsonSchema':{ 'type': 'number'," + " 'properties': {" + " 'a': {'properties': " + " {'b': {'minimum': 1}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 1}}"); + BSONObj expectedError = BSON("operatorName" + << "$jsonSchema" + << "specifiedAs" << query << "reason" + << "expression always evaluates to false"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// type +TEST(JSONSchemaValidation, BasicType) { + BSONObj query = fromjson(" {'$jsonSchema': {'properties': {'a': {'type': 'string'}}}}"); + BSONObj document = fromjson("{'a': {'b': 1}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'string'}," + " 'reason': 'type did not match'," + " 'consideredValue': {'b': 1}," + " 'consideredType': 'object'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleTypeFailures) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'properties':" + " {'a': {'type': 'string'}, " + " 'b': {'type': 'number'}, " + " 'c': {'type': 'object'}}}}}"); + BSONObj document = fromjson("{'a': {'b': 1}, 'b': 4, 'c': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'string'}," + " 'reason': 'type did not match'," + " 'consideredValue': {'b': 1}," + " 'consideredType': 'object'}]}," + " {'propertyName': 'c', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'object'}," + " 'reason': 'type did not match'," + " 'consideredValue': 'foo'," + " 'consideredType': 'string'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, TypeNoImplicitArrayTraversal) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'properties': " + " {'a': {'type': 'string'}}}}"); + // Even though 'a' is an array of strings, this is a type mismatch in the world of $jsonSchema. + BSONObj document = fromjson("{'a': ['Mihai', 'was', 'here']}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'string'}," + " 'reason': 'type did not match'," + " 'consideredValue': ['Mihai', 'was', 'here']," + " 'consideredType': 'array'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// bsonType +TEST(JSONSchemaValidation, BasicBSONType) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'properties': " + " {'a': {'bsonType': 'double'}}}}"); + BSONObj document = fromjson("{'a': {'b': 1}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'bsonType'," + " 'specifiedAs': {'bsonType': 'double'}," + " 'reason': 'type did not match'," + " 'consideredValue': {'b': 1}," + " 'consideredType': 'object'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleBSONTypeFailures) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'properties': " + " {'a': {'bsonType': 'double'}, " + " 'b': {'bsonType': 'int'}, " + " 'c': {'bsonType': 'decimal'}}}}}"); + BSONObj document = fromjson("{'a': {'b': 1}, 'b': 4, 'c': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'bsonType'," + " 'specifiedAs': {'bsonType': 'double'}," + " 'reason': 'type did not match'," + " 'consideredValue': {'b': 1}," + " 'consideredType': 'object'}]}," + " {'propertyName': 'c', 'details': [" + " {'operatorName': 'bsonType'," + " 'specifiedAs': {'bsonType': 'decimal'}," + " 'reason': 'type did not match'," + " 'consideredValue': 'foo'," + " 'consideredType': 'string'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, BSONTypeNoImplicitArrayTraversal) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'properties': " + " {'a': {'bsonType': 'string'}}}}"); + // Even though 'a' is an array of strings, this is a type mismatch in the world of $jsonSchema. + BSONObj document = fromjson("{'a': ['Mihai', 'was', 'here']}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'bsonType'," + " 'specifiedAs': {'bsonType': 'string'}," + " 'reason': 'type did not match'," + " 'consideredValue': ['Mihai', 'was', 'here']," + " 'consideredType': 'array'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/doc_validation_error_test.cpp b/src/mongo/db/matcher/doc_validation_error_test.cpp index ee85db99849..a36786412e5 100644 --- a/src/mongo/db/matcher/doc_validation_error_test.cpp +++ b/src/mongo/db/matcher/doc_validation_error_test.cpp @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-present MongoDB, Inc. + * Copyright (C) 2020-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, @@ -29,27 +29,28 @@ #include "mongo/platform/basic.h" -#include "mongo/db/matcher/doc_validation_error.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/unittest/unittest.h" +#include "mongo/db/matcher/doc_validation_error_test.h" -namespace mongo { -namespace { -/** - * Utility function which parses a MatchExpression from 'query' and verifies that the error - * generated by the parsed MatchExpression and 'document' matches 'expectedError'. - */ -void verifyGeneratedError(BSONObj query, BSONObj document, BSONObj expectedError) { +namespace mongo::doc_validation_error { +void verifyGeneratedError(const BSONObj& query, + const BSONObj& document, + const BSONObj& expectedError) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); expCtx->isParsingCollectionValidator = true; StatusWithMatchExpression result = MatchExpressionParser::parse(query, expCtx); ASSERT_OK(result.getStatus()); MatchExpression* expr = result.getValue().get(); BSONObj generatedError = doc_validation_error::generateError(*expr, document); + + // Verify that the document fails to match against the query. + ASSERT_FALSE(expr->matchesBSON(document)); + + // Verify that the generated error matches the expected error. ASSERT_BSONOBJ_EQ(generatedError, expectedError); } +namespace { + // Comparison operators. // $eq TEST(ComparisonMatchExpression, BasicEq) { @@ -60,7 +61,7 @@ TEST(ComparisonMatchExpression, BasicEq) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 1); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, EqMissingPath) { @@ -70,7 +71,7 @@ TEST(ComparisonMatchExpression, EqMissingPath) { << "$eq" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, EqImplicitArrayTraversal) { @@ -82,7 +83,7 @@ TEST(ComparisonMatchExpression, EqImplicitArrayTraversal) { << "comparison failed" << "consideredValues" << BSON_ARRAY(3 << 4 << 5 << BSON_ARRAY(3 << 4 << 5))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedDocumentSingleElement) { @@ -93,7 +94,7 @@ TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedDocumentSingleElem << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 3); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedDocument) { @@ -104,7 +105,7 @@ TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedDocument) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValues" << BSON_ARRAY(3 << 4 << 5)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedArrays) { @@ -118,7 +119,7 @@ TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedArrays) { << "consideredValues" << BSON_ARRAY(1 << 2 << BSON_ARRAY(1 << 2) << 3 << 4 << BSON_ARRAY(3 << 4))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, EqNoOperator) { @@ -129,7 +130,7 @@ TEST(ComparisonMatchExpression, EqNoOperator) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 1); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $ne @@ -141,7 +142,7 @@ TEST(ComparisonMatchExpression, BasicNe) { << "specifiedAs" << query << "reason" << "comparison succeeded" << "consideredValue" << 2); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, NeImplicitArrayTraversal) { @@ -153,7 +154,7 @@ TEST(ComparisonMatchExpression, NeImplicitArrayTraversal) { << "comparison succeeded" << "consideredValues" << BSON_ARRAY(1 << 2 << 3 << BSON_ARRAY(1 << 2 << 3))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $lt @@ -165,7 +166,7 @@ TEST(ComparisonMatchExpression, BasicLt) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 1); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, LtMissingPath) { @@ -175,7 +176,7 @@ TEST(ComparisonMatchExpression, LtMissingPath) { << "$lt" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, LtImplicitArrayTraversal) { @@ -187,7 +188,7 @@ TEST(ComparisonMatchExpression, LtImplicitArrayTraversal) { << "comparison failed" << "consideredValues" << BSON_ARRAY(3 << 4 << 5 << BSON_ARRAY(3 << 4 << 5))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $lte @@ -199,7 +200,7 @@ TEST(ComparisonMatchExpression, BasicLte) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 1); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, LteMissingPath) { @@ -209,7 +210,7 @@ TEST(ComparisonMatchExpression, LteMissingPath) { << "$lte" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, LteImplicitArrayTraversal) { @@ -221,7 +222,7 @@ TEST(ComparisonMatchExpression, LteImplicitArrayTraversal) { << "comparison failed" << "consideredValues" << BSON_ARRAY(3 << 4 << 5 << BSON_ARRAY(3 << 4 << 5))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $gt @@ -233,7 +234,7 @@ TEST(ComparisonMatchExpression, BasicGt) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 0); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, GtMissingPath) { @@ -243,7 +244,7 @@ TEST(ComparisonMatchExpression, GtMissingPath) { << "$gt" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, GtImplicitArrayTraversal) { @@ -255,7 +256,7 @@ TEST(ComparisonMatchExpression, GtImplicitArrayTraversal) { << "comparison failed" << "consideredValues" << BSON_ARRAY(0 << 1 << 2 << BSON_ARRAY(0 << 1 << 2))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $gte @@ -267,7 +268,7 @@ TEST(ComparisonMatchExpression, BasicGte) { << "specifiedAs" << query << "reason" << "comparison failed" << "consideredValue" << 0); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, GteMissingPath) { @@ -277,7 +278,7 @@ TEST(ComparisonMatchExpression, GteMissingPath) { << "$gte" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, GteImplicitArrayTraversal) { @@ -289,7 +290,7 @@ TEST(ComparisonMatchExpression, GteImplicitArrayTraversal) { << "comparison failed" << "consideredValues" << BSON_ARRAY(0 << 1 << 2 << BSON_ARRAY(0 << 1 << 2))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $in @@ -301,7 +302,7 @@ TEST(ComparisonMatchExpression, BasicIn) { << "specifiedAs" << query << "reason" << "no matching value found in array" << "consideredValue" << 4); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, InMissingPath) { @@ -311,7 +312,7 @@ TEST(ComparisonMatchExpression, InMissingPath) { << "$in" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, InNestedDocumentsAndArrays) { @@ -326,7 +327,7 @@ TEST(ComparisonMatchExpression, InNestedDocumentsAndArrays) { << "consideredValues" << BSON_ARRAY(1 << 2 << BSON_ARRAY(1 << 2) << 3 << 4 << BSON_ARRAY(3 << 4))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $nin @@ -338,7 +339,7 @@ TEST(ComparisonMatchExpression, BasicNin) { << "specifiedAs" << query << "reason" << "matching value found in array" << "consideredValue" << 3); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, NinNestedDocumentsAndArrays) { @@ -352,7 +353,7 @@ TEST(ComparisonMatchExpression, NinNestedDocumentsAndArrays) { << "consideredValues" << BSON_ARRAY(1 << 2 << BSON_ARRAY(1 << 2) << 3 << 4 << BSON_ARRAY(3 << 4))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verify that Comparison operators which accept a regex ($in and $nin) work as expected. @@ -367,7 +368,7 @@ TEST(ComparisonMatchExpression, InAcceptsRegex) { << "no matching value found in array" << "consideredValue" << "Validation"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ComparisonMatchExpression, NinAcceptsRegex) { @@ -381,7 +382,7 @@ TEST(ComparisonMatchExpression, NinAcceptsRegex) { << "matching value found in array" << "consideredValue" << "berry"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Logical operators @@ -400,7 +401,7 @@ TEST(LogicalMatchExpression, BasicAnd) { << failingClause << "reason" << "comparison failed" << "consideredValue" << 11)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, ImplicitAnd) { @@ -417,7 +418,7 @@ TEST(LogicalMatchExpression, ImplicitAnd) { << failingClause << "reason" << "comparison failed" << "consideredValue" << 11)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, AndMultipleFailingClauses) { @@ -441,7 +442,20 @@ TEST(LogicalMatchExpression, AndMultipleFailingClauses) { << "specifiedAs" << secondFailingClause << "reason" << "comparison failed" << "consideredValue" << 15)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(LogicalMatchExpression, NestedAndDoesNotReportErrorDetailsIfItMatches) { + BSONObj query = fromjson("{$and: [{$and: [{a: 1}]}, {$and: [{b: 1}]}]}"); + BSONObj document = fromjson("{a: 1, b: 2}"); + BSONObj expectedError = fromjson( + "{operatorName: '$and', clausesNotSatisfied: [{index: 1, details: {" + " operatorName: '$and', clausesNotSatisfied: [{index: 0, details: {" + " operatorName: '$eq', " + " specifiedAs: {b: 1}, " + " reason: 'comparison failed', " + " consideredValue: 2}}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $or @@ -459,7 +473,7 @@ TEST(LogicalMatchExpression, BasicOr) { << failingClause << "reason" << "comparison failed" << "consideredValue" << 11)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, OrMultipleFailingClauses) { @@ -483,7 +497,7 @@ TEST(LogicalMatchExpression, OrMultipleFailingClauses) { << "specifiedAs" << secondFailingClause << "reason" << "comparison failed" << "consideredValue" << 15)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $nor @@ -502,7 +516,7 @@ TEST(LogicalMatchExpression, BasicNor) { << "specifiedAs" << secondFailingClause << "reason" << "comparison succeeded" << "consideredValue" << 9)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, NorAllSuccessfulClauses) { @@ -526,7 +540,7 @@ TEST(LogicalMatchExpression, NorAllSuccessfulClauses) { << "specifiedAs" << secondFailingClause << "reason" << "comparison succeeded" << "consideredValue" << 15)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $not @@ -543,7 +557,7 @@ TEST(LogicalMatchExpression, BasicNot) { << "specifiedAs" << failingQuery << "reason" << "comparison succeeded" << "consideredValue" << 9)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, NotOverImplicitAnd) { @@ -572,7 +586,7 @@ TEST(LogicalMatchExpression, NotOverImplicitAnd) { << BSON("a" << BSON("$gt" << 5)) << "reason" << "comparison succeeded" << "consideredValue" << 10))))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, NestedNot) { @@ -591,7 +605,7 @@ TEST(LogicalMatchExpression, NestedNot) { << "specifiedAs" << failingQuery << "reason" << "comparison failed" << "consideredValue" << 11))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Combine logical operators @@ -625,7 +639,7 @@ TEST(LogicalMatchExpression, NestedAndOr) { " 'specifiedAs': {'qty': {'$lt': 10}}," " 'reason': 'comparison failed'," " 'consideredValue': 30}}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, NestedAndOrOneFailingClause) { @@ -643,7 +657,7 @@ TEST(LogicalMatchExpression, NestedAndOrOneFailingClause) { " 'specifiedAs': {'qty': {'$lt': 10}}," " 'reason': 'comparison failed'," " 'consideredValue': 30}}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, NestedAndOrNorOneSuccessfulClause) { @@ -666,7 +680,7 @@ TEST(LogicalMatchExpression, NestedAndOrNorOneSuccessfulClause) { " 'specifiedAs': {'qty': {'$lt': 20}}," " 'reason': 'comparison succeeded'," " 'consideredValue': 15}}]}}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(LogicalMatchExpression, NestedAndOrNorNotOneFailingClause) { @@ -691,7 +705,7 @@ TEST(LogicalMatchExpression, NestedAndOrNorNotOneFailingClause) { " 'specifiedAs': {'qty': {'$lt': 20}}," " 'reason': 'comparison failed'," " 'consideredValue': 25}}}]}}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Miscellaneous operators. // $exists @@ -702,7 +716,7 @@ TEST(MiscellaneousMatchExpression, BasicExists) { << "$exists" << "specifiedAs" << query << "reason" << "path does not exist"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, NotExists) { BSONObj query = BSON("a" << BSON("$exists" << false)); @@ -711,7 +725,7 @@ TEST(MiscellaneousMatchExpression, NotExists) { << "$exists" << "specifiedAs" << query << "reason" << "path does exist"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $type TEST(MiscellaneousMatchExpression, BasicType) { @@ -722,12 +736,12 @@ TEST(MiscellaneousMatchExpression, BasicType) { BSONObj expectedError = BSON("operatorName" << "$type" << "specifiedAs" << query << "reason" - << "no matching types found for specified typeset" + << "type did not match" << "consideredValue" << "one" << "consideredType" << "string"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, NotType) { BSONObj failingClause = BSON("$type" @@ -742,12 +756,12 @@ TEST(MiscellaneousMatchExpression, NotType) { << BSON("operatorName" << "$type" << "specifiedAs" << failingQuery << "reason" - << "matching types found for specified typeset" + << "type did match" << "consideredValue" << "words" << "consideredType" << "string")); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, TypeMissingPath) { BSONObj query = BSON("a" << BSON("$type" @@ -757,7 +771,7 @@ TEST(MiscellaneousMatchExpression, TypeMissingPath) { << "$type" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, TypeImplicitArrayTraversal) { BSONObj query = BSON("a" << BSON("$type" @@ -768,7 +782,7 @@ TEST(MiscellaneousMatchExpression, TypeImplicitArrayTraversal) { BSONObj expectedError = BSON("operatorName" << "$type" << "specifiedAs" << query << "reason" - << "no matching types found for specified typeset" + << "type did not match" << "consideredValues" << BSON_ARRAY("x" << "y" @@ -779,7 +793,7 @@ TEST(MiscellaneousMatchExpression, TypeImplicitArrayTraversal) { << "consideredTypes" << BSON_ARRAY("array" << "string")); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $expr TEST(MiscellaneousMatchExpression, BasicExpr) { @@ -791,7 +805,7 @@ TEST(MiscellaneousMatchExpression, BasicExpr) { << "specifiedAs" << query << "reason" << "$expr did not match" << "expressionResult" << false); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, NorExpr) { BSONObj failingClause = BSON("$eq" << BSON_ARRAY("$a" @@ -809,7 +823,7 @@ TEST(MiscellaneousMatchExpression, NorExpr) { << "specifiedAs" << failingQuery << "reason" << "$expr did match" << "expressionResult" << true)))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, ExprImplicitArrayTraversal) { BSONObj query = BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" @@ -820,7 +834,7 @@ TEST(MiscellaneousMatchExpression, ExprImplicitArrayTraversal) { << "specifiedAs" << query << "reason" << "$expr did not match" << "expressionResult" << false); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $mod TEST(MiscellaneousMatchExpression, BasicMod) { @@ -831,10 +845,10 @@ TEST(MiscellaneousMatchExpression, BasicMod) { << "specifiedAs" << query << "reason" << "$mod did not evaluate to expected remainder" << "consideredValue" << 2); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, NotMod) { - BSONObj failingClause = BSON("$mod" << BSON_ARRAY(2 << 1)); + BSONObj failingClause = BSON("$mod" << BSON_ARRAY(2 << 0)); BSONObj failingQuery = BSON("a" << failingClause); BSONObj query = BSON("a" << BSON("$not" << failingClause)); BSONObj document = BSON("a" << 2); @@ -846,7 +860,7 @@ TEST(MiscellaneousMatchExpression, NotMod) { << "specifiedAs" << failingQuery << "reason" << "$mod did evaluate to expected remainder" << "consideredValue" << 2)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, ModMissingPath) { BSONObj query = BSON("a" << BSON("$mod" << BSON_ARRAY(2 << 1))); @@ -855,7 +869,7 @@ TEST(MiscellaneousMatchExpression, ModMissingPath) { << "$mod" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversal) { BSONObj query = BSON("a" << BSON("$mod" << BSON_ARRAY(2 << 1))); @@ -866,7 +880,7 @@ TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversal) { << "$mod did not evaluate to expected remainder" << "consideredValues" << BSON_ARRAY(0 << 2 << 4 << BSON_ARRAY(0 << 2 << 4))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, ModNonNumeric) { BSONObj query = BSON("a" << BSON("$mod" << BSON_ARRAY(2 << 1))); @@ -875,7 +889,7 @@ TEST(MiscellaneousMatchExpression, ModNonNumeric) { BSONObj expectedError = BSON("operatorName" << "$mod" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredType" << "string" << "expectedTypes" @@ -885,7 +899,7 @@ TEST(MiscellaneousMatchExpression, ModNonNumeric) { << "long") << "consideredValue" << "two"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversalNonNumeric) { BSONObj query = BSON("a" << BSON("$mod" << BSON_ARRAY(2 << 1))); @@ -895,7 +909,7 @@ TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversalNonNumeric) { BSONObj expectedError = BSON("operatorName" << "$mod" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredTypes" << BSON_ARRAY("array" << "string") @@ -911,7 +925,7 @@ TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversalNonNumeric) { << BSON_ARRAY("zero" << "two" << "four"))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversalMixedTypes) { BSONObj query = BSON("a" << BSON("$mod" << BSON_ARRAY(2 << 1))); @@ -926,7 +940,7 @@ TEST(MiscellaneousMatchExpression, ModImplicitArrayTraversalMixedTypes) { << "four" << BSON_ARRAY(0 << "two" << "four"))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $regex TEST(MiscellaneousMatchExpression, BasicRegex) { @@ -940,15 +954,15 @@ TEST(MiscellaneousMatchExpression, BasicRegex) { << "regular expression did not match" << "consideredValue" << "one"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, NotRegex) { - BSONObj failingClause = BSON("$regex" << BSONRegEx("/myRegex/", "") << "$options" + BSONObj failingClause = BSON("$regex" << BSONRegEx("myRegex", "") << "$options" << ""); BSONObj failingQuery = BSON("a" << failingClause); BSONObj query = BSON("a" << BSON("$not" << failingClause)); BSONObj document = BSON("a" - << "one"); + << "myRegex"); BSONObj expectedError = BSON("operatorName" << "$not" << "details" @@ -957,8 +971,8 @@ TEST(MiscellaneousMatchExpression, NotRegex) { << "specifiedAs" << failingQuery << "reason" << "regular expression did match" << "consideredValue" - << "one")); - verifyGeneratedError(query, document, expectedError); + << "myRegex")); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, RegexMissingPath) { BSONObj query = BSON("a" << BSON("$regex" << BSONRegEx("/myRegex/", "") << "$options" @@ -969,7 +983,7 @@ TEST(MiscellaneousMatchExpression, RegexMissingPath) { << "$regex" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversal) { BSONObj query = BSON("a" << BSON("$regex" << BSONRegEx("/myRegex/", "") << "$options" @@ -988,7 +1002,7 @@ TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversal) { << BSON_ARRAY("x" << "y" << "z"))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, RegexNonString) { BSONObj query = BSON("a" << BSON("$regex" << BSONRegEx("/myRegex/", "") << "$options" @@ -997,7 +1011,7 @@ TEST(MiscellaneousMatchExpression, RegexNonString) { BSONObj expectedError = BSON("operatorName" << "$regex" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredType" << "int" << "expectedTypes" @@ -1005,7 +1019,7 @@ TEST(MiscellaneousMatchExpression, RegexNonString) { << "string" << "symbol") << "consideredValue" << 1); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversalNonString) { BSONObj query = BSON("a" << BSON("$regex" << BSONRegEx("/myRegex/", "") << "$options" @@ -1014,7 +1028,7 @@ TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversalNonString) { BSONObj expectedError = BSON("operatorName" << "$regex" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredTypes" << BSON_ARRAY("array" << "int") @@ -1024,7 +1038,7 @@ TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversalNonString) { << "symbol") << "consideredValues" << BSON_ARRAY(0 << 1 << 2 << BSON_ARRAY(0 << 1 << 2))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversalMixedTypes) { BSONObj query = BSON("a" << BSON("$regex" << BSONRegEx("/myRegex/", "") << "$options" @@ -1036,7 +1050,7 @@ TEST(MiscellaneousMatchExpression, RegexImplicitArrayTraversalMixedTypes) { << "regular expression did not match" << "consideredValues" << BSON_ARRAY("x" << 1 << 2 << BSON_ARRAY("x" << 1 << 2))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllClear expression with numeric bitmask correctly generates a validation @@ -1049,7 +1063,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllClearNumeric) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 7); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllClear expression with numeric bitmask correctly generates a validation @@ -1066,7 +1080,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllClearNumericOnValueM << "reason" << "bitwise operator matched successfully" << "consideredValue" << 5)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllClear expression with position list correctly generates a validation @@ -1079,7 +1093,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllClearPositionList) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 7); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllClear expression with BinData bitmask correctly generates a validation @@ -1094,7 +1108,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllClearBinData) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 7); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllSet expression correctly generates a validation error. @@ -1106,7 +1120,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllSetNumeric) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 5); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllSet expression with position list correctly generates a validation @@ -1119,7 +1133,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllSetPositionList) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 5); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllSet expression with BinData bitmask correctly generates a validation @@ -1134,7 +1148,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllSetBinData) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 5); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnyClear expression correctly generates a validation error. @@ -1146,7 +1160,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnyClearNumeric) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 7); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnyClear expression with position list correctly generates a validation @@ -1159,7 +1173,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnyClearPositionList) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 7); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnyClear expression with BinData bitmask correctly generates a validation @@ -1174,7 +1188,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnyClearBinData) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 7); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnySet expression correctly generates a validation error. @@ -1186,7 +1200,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnySetNumeric) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 0); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnySet expression with position list correctly generates a validation @@ -1199,7 +1213,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnySetPositionList) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 0); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnySet expression with BinData bitmask correctly generates a validation @@ -1214,7 +1228,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnySetBinData) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValue" << 0); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAnyClear expression correctly generates a validation error on value type @@ -1226,7 +1240,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnyClearOnTypeMismatch) BSONObj expectedError = BSON("operatorName" << "$bitsAnyClear" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredType" << "string" << "expectedTypes" @@ -1237,7 +1251,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAnyClearOnTypeMismatch) << "long") << "consideredValue" << "someString"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $bitsAllClear expression with numeric bitmask correctly generates a validation @@ -1251,7 +1265,7 @@ TEST(BitTestMatchExpression, GeneratesValidationErrorBitsAllClearOnValueArray) { << "specifiedAs" << query << "reason" << "bitwise operator failed to match" << "consideredValues" << BSON_ARRAY(7 << 3 << attributeValue)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $geoIntersects expression correctly generates a validation error. @@ -1269,7 +1283,7 @@ TEST(GeoMatchExpression, GeneratesValidationErrorGeoIntersects) { << "specifiedAs" << query << "reason" << "none of considered geometries intersected the expression’s geometry" << "consideredValue" << point); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $geoIntersects expression correctly generates a validation error on unexpected @@ -1293,7 +1307,7 @@ TEST(GeoMatchExpression, GeneratesValidationErrorGeoIntersectsOnValueMatch) { << "specifiedAs" << BSON("a" << subquery) << "reason" << "at least one of considered geometries intersected the expression’s geometry" << "consideredValue" << point)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $geoIntersects expression correctly generates a correct validation error on value @@ -1309,14 +1323,14 @@ TEST(GeoMatchExpression, GeneratesValidationErrorGeoIntersectsOnTypeMismatch) { BSONObj expectedError = BSON("operatorName" << "$geoIntersects" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredType" << "int" << "expectedTypes" << BSON_ARRAY("array" << "object") << "consideredValue" << 2); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $geoIntersects expression correctly generates a validation error when applied on an @@ -1339,7 +1353,7 @@ TEST(GeoMatchExpression, GeneratesValidationErrorGeoIntersectsOnValueArray) { << "specifiedAs" << query << "reason" << "none of considered geometries intersected the expression’s geometry" << "consideredValues" << BSON_ARRAY(point1 << point2 << points)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $geoWithin expression correctly generates a validation error. @@ -1357,7 +1371,7 @@ TEST(GeoMatchExpression, GeneratesValidationErrorGeoWithin) { << "specifiedAs" << query << "reason" << "none of considered geometries was contained within the expression’s geometry" << "consideredValue" << point); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Verifies that $geoWithin expression correctly generates an inverse validation error. @@ -1379,7 +1393,7 @@ TEST(GeoMatchExpression, GeneratesValidationErrorForMatchGeoWithin) { << "at least one of considered geometries was contained " "within the expression’s geometry" << "consideredValue" << point)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // Array operators. @@ -1393,7 +1407,7 @@ TEST(ArrayMatchingMatchExpression, BasicSize) { << "specifiedAs" << query << "reason" << "array length was not equal to given size" << "consideredValue" << BSON_ARRAY(1 << 2 << 3)); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, SizeNonArray) { @@ -1402,13 +1416,13 @@ TEST(ArrayMatchingMatchExpression, SizeNonArray) { BSONObj expectedError = BSON("operatorName" << "$size" << "specifiedAs" << query << "reason" - << "type mismatch" + << "type did not match" << "consideredType" << "int" << "expectedType" << "array" << "consideredValue" << 3); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } @@ -1419,10 +1433,9 @@ TEST(ArrayMatchingMatchExpression, SizeMissingPath) { << "$size" << "specifiedAs" << query << "reason" << "field was missing"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } - TEST(ArrayMatchingMatchExpression, NotOverSize) { BSONObj query = BSON("a" << BSON("$not" << BSON("$size" << 2))); BSONObj document = BSON("a" << BSON_ARRAY(1 << 2)); @@ -1435,7 +1448,7 @@ TEST(ArrayMatchingMatchExpression, NotOverSize) { << "reason" << "array length was equal to given size" << "consideredValue" << BSON_ARRAY(1 << 2))); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $all @@ -1447,7 +1460,7 @@ TEST(ArrayMatchingMatchExpression, BasicAll) { "'specifiedAs': {'a': {'$all': [1,2,3]}}," "'reason': 'array did not contain all specified values'," "'consideredValue': [1,2,4]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, AllRegex) { @@ -1458,7 +1471,7 @@ TEST(ArrayMatchingMatchExpression, AllRegex) { "'specifiedAs': {'a': {'$all': [/^a/,/^b/]}}," "'reason': 'array did not contain all specified values'," "'consideredValue': ['abc', 'cbc']}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, AllMissingPath) { @@ -1468,7 +1481,7 @@ TEST(ArrayMatchingMatchExpression, AllMissingPath) { "{'operatorName': '$all'," "'specifiedAs': {'a': {'$all': [1,2,3]}}," "'reason': 'field was missing'}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, AllNoValues) { @@ -1478,7 +1491,7 @@ TEST(ArrayMatchingMatchExpression, AllNoValues) { "{'operatorName': '$all'," "'specifiedAs': {'a': {'$all': []}}," "'reason': 'expression always evaluates to false'}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, NotOverAll) { @@ -1491,7 +1504,7 @@ TEST(ArrayMatchingMatchExpression, NotOverAll) { " 'specifiedAs': {'a': {'$all': [1,2,3]}}," " 'reason': 'array did contain all specified values'," " 'consideredValue': [1,2,3]}}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $elemMatch @@ -1503,7 +1516,7 @@ TEST(ArrayMatchingMatchExpression, BasicElemMatchValue) { "'specifiedAs': {'a':{'$elemMatch':{'$gt': 0,'$lt': 10}}}," "'reason': 'array did not satisfy the child predicate'," "'consideredValue': [10,11,12]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, ElemMatchValueMissingPath) { @@ -1513,7 +1526,7 @@ TEST(ArrayMatchingMatchExpression, ElemMatchValueMissingPath) { "{'operatorName': '$elemMatch'," "'specifiedAs': {'a':{'$elemMatch':{'$gt': 0,'$lt': 10}}}," "'reason': 'field was missing'}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, ElemMatchValueNonArray) { @@ -1522,11 +1535,11 @@ TEST(ArrayMatchingMatchExpression, ElemMatchValueNonArray) { BSONObj expectedError = fromjson( "{'operatorName': '$elemMatch'," "'specifiedAs': {'a':{'$elemMatch':{'$gt': 0,'$lt': 10}}}," - "'reason': 'type mismatch'," + "'reason': 'type did not match'," "'consideredType': 'int'," "'expectedType': 'array'," "'consideredValue': 5}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, NotOverElemMatchValue) { @@ -1538,7 +1551,7 @@ TEST(ArrayMatchingMatchExpression, NotOverElemMatchValue) { " 'specifiedAs': {'a':{'$elemMatch':{'$gt': 0,'$lt': 10}}}," " 'reason': 'array did satisfy the child predicate'," " 'consideredValue': [3,4,5]}}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, BasicElemMatchObject) { @@ -1549,7 +1562,7 @@ TEST(ArrayMatchingMatchExpression, BasicElemMatchObject) { "'specifiedAs': {'a': {'$elemMatch': {'b': {'$gt': 0}, 'c': {'$lt': 0}}}}," "'reason': 'array did not satisfy the child predicate'," "'consideredValue': [{'b': 0, 'c': 0}, {'b': 1, 'c': 1}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, ElemMatchObjectMissingPath) { @@ -1559,7 +1572,7 @@ TEST(ArrayMatchingMatchExpression, ElemMatchObjectMissingPath) { "{'operatorName': '$elemMatch'," "'specifiedAs': {'a': {'$elemMatch': {'b': {'$gt': 0}, 'c': {'$lt': 0}}}}," "'reason': 'field was missing'}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, ElemMatchObjectNonArray) { @@ -1568,11 +1581,11 @@ TEST(ArrayMatchingMatchExpression, ElemMatchObjectNonArray) { BSONObj expectedError = fromjson( "{'operatorName': '$elemMatch'," "'specifiedAs': {'a': {'$elemMatch': {'b': {'$gt': 0}, 'c': {'$lt': 0}}}}," - "'reason': 'type mismatch'," + "'reason': 'type did not match'," "'consideredType': 'string'," "'expectedType': 'array'," "'consideredValue': 'foo'}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, NestedElemMatchObject) { @@ -1583,7 +1596,7 @@ TEST(ArrayMatchingMatchExpression, NestedElemMatchObject) { "'specifiedAs': {'a': {'$elemMatch': {'b': {$elemMatch: {'c': {'$lt': 0}}}}}}," "'reason': 'array did not satisfy the child predicate'," "'consideredValue': [{'b': [{'c': [1,2,3]}, {'c': [4,5,6]}]}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } TEST(ArrayMatchingMatchExpression, NotOverElemMatchObject) { @@ -1596,7 +1609,7 @@ TEST(ArrayMatchingMatchExpression, NotOverElemMatchObject) { " 'specifiedAs': {'a':{'$elemMatch': {'b': {'$gte': 0}, 'c': {'$lt': 10}}}}," " 'reason': 'array did satisfy the child predicate'," " 'consideredValue': [{'b': 0, 'c': 0}, {'b': 1, 'c': 1}]}}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } // $all and $elemMatch @@ -1612,7 +1625,7 @@ TEST(ArrayMatchingMatchExpression, AllOverElemMatch) { " [{'$elemMatch': {'b': {'$gte': 0}}}, {'$elemMatch': {'c': {'$lt': 0}}}]}}," "'reason': 'array did not contain all specified values'," "'consideredValue': [{'b': 0, 'c': 0}, {'b': 1, 'c': 1}]}"); - verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); } } // namespace -} // namespace mongo +} // namespace mongo::doc_validation_error diff --git a/src/mongo/db/matcher/doc_validation_error_test.h b/src/mongo/db/matcher/doc_validation_error_test.h new file mode 100644 index 00000000000..ba134a889fa --- /dev/null +++ b/src/mongo/db/matcher/doc_validation_error_test.h @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/matcher/doc_validation_error.h" +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo::doc_validation_error { +/** + * Utility function which parses a MatchExpression from 'query' and verifies that the error + * generated by the parsed MatchExpression and 'document' matches 'expectedError'. + */ +void verifyGeneratedError(const BSONObj& query, + const BSONObj& document, + const BSONObj& expectedError); +} // namespace mongo::doc_validation_error
\ No newline at end of file diff --git a/src/mongo/db/matcher/doc_validation_util.cpp b/src/mongo/db/matcher/doc_validation_util.cpp new file mode 100644 index 00000000000..92d85f466ad --- /dev/null +++ b/src/mongo/db/matcher/doc_validation_util.cpp @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/db/matcher/doc_validation_util.h" + +namespace mongo::doc_validation_error { +std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const std::string& operatorName, + const BSONObj& annotation) { + if (expCtx->isParsingCollectionValidator) { + return std::make_unique<MatchExpression::ErrorAnnotation>(operatorName, annotation); + } else { + return nullptr; + } +} + +std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + MatchExpression::ErrorAnnotation::Mode mode) { + if (expCtx->isParsingCollectionValidator) { + return std::make_unique<MatchExpression::ErrorAnnotation>(mode); + } else { + return nullptr; + } +} + +void annotateTreeToIgnoreForErrorDetails(const boost::intrusive_ptr<ExpressionContext>& expCtx, + MatchExpression* expr) { + expr->setErrorAnnotation( + createAnnotation(expCtx, MatchExpression::ErrorAnnotation::Mode::kIgnore)); + for (const auto childExpr : *expr) { + annotateTreeToIgnoreForErrorDetails(expCtx, childExpr); + } +} +} // namespace mongo::doc_validation_error
\ No newline at end of file diff --git a/src/mongo/db/matcher/doc_validation_util.h b/src/mongo/db/matcher/doc_validation_util.h new file mode 100644 index 00000000000..b8793c4d05b --- /dev/null +++ b/src/mongo/db/matcher/doc_validation_util.h @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/bson/bsonobj.h" +#include "mongo/db/matcher/expression.h" +#include "mongo/db/pipeline/expression_context.h" + +namespace mongo::doc_validation_error { +/** + * Set of functions which create an ErrorAnnotation provided that a validator expression is being + * parsed. + */ +std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const std::string& operatorName, + const BSONObj& annotation); + +std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + MatchExpression::ErrorAnnotation::Mode mode); + +/** + * Utility which tags an entire tree with 'AnnotationMode::kIgnore'. + */ +void annotateTreeToIgnoreForErrorDetails(const boost::intrusive_ptr<ExpressionContext>& expCtx, + MatchExpression* expr); +} // namespace mongo::doc_validation_error
\ No newline at end of file diff --git a/src/mongo/db/matcher/expression_always_boolean.h b/src/mongo/db/matcher/expression_always_boolean.h index 3b8a7b5f647..74587158040 100644 --- a/src/mongo/db/matcher/expression_always_boolean.h +++ b/src/mongo/db/matcher/expression_always_boolean.h @@ -126,14 +126,15 @@ class AlwaysTrueMatchExpression final : public AlwaysBooleanMatchExpression { public: static constexpr StringData kName = "$alwaysTrue"_sd; - AlwaysTrueMatchExpression() : AlwaysBooleanMatchExpression(MatchType::ALWAYS_TRUE, true) {} + AlwaysTrueMatchExpression(clonable_ptr<ErrorAnnotation> annotation = nullptr) + : AlwaysBooleanMatchExpression(MatchType::ALWAYS_TRUE, true, std::move(annotation)) {} StringData name() const final { return kName; } std::unique_ptr<MatchExpression> shallowClone() const final { - return std::make_unique<AlwaysTrueMatchExpression>(); + return std::make_unique<AlwaysTrueMatchExpression>(_errorAnnotation); } bool isTriviallyTrue() const final { diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index 7dbe5dc0dc5..1077c1ef8ec 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -39,6 +39,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/bsontypes.h" +#include "mongo/db/matcher/doc_validation_util.h" #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_array.h" #include "mongo/db/matcher/expression_expr.h" @@ -89,43 +90,6 @@ bool hasNode(const MatchExpression* root, MatchExpression::MatchType type) { return false; } -/** - * Set of functions which create an ErrorAnnotation provided that a validator expression is being - * parsed. - */ -std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const std::string& operatorName, - const BSONObj& annotation) { - if (expCtx->isParsingCollectionValidator) { - return std::make_unique<MatchExpression::ErrorAnnotation>(operatorName, annotation); - } else { - return nullptr; - } -} - -std::unique_ptr<MatchExpression::ErrorAnnotation> createAnnotation( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - MatchExpression::ErrorAnnotation::Mode mode) { - if (expCtx->isParsingCollectionValidator) { - return std::make_unique<MatchExpression::ErrorAnnotation>(mode); - } else { - return nullptr; - } -} - -/** - * Utility which tags an entire tree with 'AnnotationMode::kIgnore'. - */ -void annotateTreeToIgnoreForErrorDetails(const boost::intrusive_ptr<ExpressionContext>& expCtx, - MatchExpression* expr) { - expr->setErrorAnnotation( - createAnnotation(expCtx, MatchExpression::ErrorAnnotation::Mode::kIgnore)); - for (const auto childExpr : *expr) { - annotateTreeToIgnoreForErrorDetails(expCtx, childExpr); - } -} - } // namespace namespace mongo { @@ -283,7 +247,8 @@ StatusWithMatchExpression parse(const BSONObj& obj, const ExtensionsCallback* extensionsCallback, MatchExpressionParser::AllowedFeatureSet allowedFeatures, DocumentParseLevel currentLevel) { - auto root = std::make_unique<AndMatchExpression>(createAnnotation(expCtx, "$and", BSONObj())); + auto root = std::make_unique<AndMatchExpression>( + doc_validation_error::createAnnotation(expCtx, "$and", BSONObj())); const DocumentParseLevel nextLevel = (currentLevel == DocumentParseLevel::kPredicateTopLevel) ? DocumentParseLevel::kUserDocumentTopLevel @@ -339,13 +304,15 @@ StatusWithMatchExpression parse(const BSONObj& obj, continue; } - auto eq = parseComparison( - e.fieldNameStringData(), - std::make_unique<EqualityMatchExpression>( - e.fieldNameStringData(), e, createAnnotation(expCtx, "$eq", e.wrap())), - e, - expCtx, - allowedFeatures); + auto eq = + parseComparison(e.fieldNameStringData(), + std::make_unique<EqualityMatchExpression>( + e.fieldNameStringData(), + e, + doc_validation_error::createAnnotation(expCtx, "$eq", e.wrap())), + e, + expCtx, + allowedFeatures); if (!eq.isOK()) return eq; @@ -518,7 +485,8 @@ StatusWithMatchExpression parseExpr(StringData name, return {std::make_unique<ExprMatchExpression>( std::move(elem), expCtx, - createAnnotation(expCtx, elem.fieldNameStringData().toString(), elem.wrap()))}; + doc_validation_error::createAnnotation( + expCtx, elem.fieldNameStringData().toString(), elem.wrap()))}; } StatusWithMatchExpression parseMOD(StringData name, @@ -548,7 +516,7 @@ StatusWithMatchExpression parseMOD(StringData name, name, divisor.numberInt(), remainder.numberInt(), - createAnnotation( + doc_validation_error::createAnnotation( expCtx, elem.fieldNameStringData().toString(), BSON(name << elem.wrap())))}; } @@ -599,7 +567,10 @@ StatusWithMatchExpression parseRegexDocument( } return {std::make_unique<RegexMatchExpression>( - name, regex, regexOptions, createAnnotation(expCtx, "$regex", BSON(name << doc)))}; + name, + regex, + regexOptions, + doc_validation_error::createAnnotation(expCtx, "$regex", BSON(name << doc)))}; } Status parseInExpression(InMatchExpression* inExpression, @@ -642,7 +613,8 @@ StatusWithMatchExpression parseType(StringData name, return {std::make_unique<T>( name, std::move(typeSet.getValue()), - createAnnotation(expCtx, elt.fieldNameStringData().toString(), BSON(name << elt.wrap())))}; + doc_validation_error::createAnnotation( + expCtx, elt.fieldNameStringData().toString(), BSON(name << elt.wrap())))}; } /** @@ -718,8 +690,8 @@ StatusWithMatchExpression parseBitTest(StringData name, BSONElement e, const boost::intrusive_ptr<ExpressionContext>& expCtx) { std::unique_ptr<BitTestMatchExpression> bitTestMatchExpression; - auto annotation = - createAnnotation(expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())); + auto annotation = doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())); if (e.type() == BSONType::Array) { // Array of bit positions provided as value. @@ -1148,7 +1120,7 @@ StatusWithMatchExpression parseGeo(StringData name, name, gq.release(), section, - createAnnotation(expCtx, operatorName, BSON(name << section)))}; + doc_validation_error::createAnnotation(expCtx, operatorName, BSON(name << section)))}; } else { invariant(PathAcceptingKeyword::GEO_NEAR == type); @@ -1178,8 +1150,8 @@ StatusWithMatchExpression parseTreeTopLevel( return {Status(ErrorCodes::BadValue, str::stream() << T::kName << " must be an array")}; } - auto temp = std::make_unique<T>( - createAnnotation(expCtx, elem.fieldNameStringData().toString(), BSONObj())); + auto temp = std::make_unique<T>(doc_validation_error::createAnnotation( + expCtx, elem.fieldNameStringData().toString(), BSONObj())); auto arr = elem.Obj(); if (arr.isEmpty()) { @@ -1242,9 +1214,10 @@ StatusWithMatchExpression parseElemMatch(StringData name, auto emValueExpr = std::make_unique<ElemMatchValueMatchExpression>( name, - createAnnotation(expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); - annotateTreeToIgnoreForErrorDetails(expCtx, &theAnd); + doc_validation_error::annotateTreeToIgnoreForErrorDetails(expCtx, &theAnd); for (size_t i = 0; i < theAnd.numChildren(); i++) { emValueExpr->add(theAnd.getChild(i)); } @@ -1271,12 +1244,13 @@ StatusWithMatchExpression parseElemMatch(StringData name, return {Status(ErrorCodes::BadValue, "$elemMatch cannot contain $where expression")}; } - annotateTreeToIgnoreForErrorDetails(expCtx, sub.get()); + doc_validation_error::annotateTreeToIgnoreForErrorDetails(expCtx, sub.get()); return {std::make_unique<ElemMatchObjectMatchExpression>( name, sub.release(), - createAnnotation(expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())))}; + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())))}; } StatusWithMatchExpression parseAll(StringData name, @@ -1288,8 +1262,8 @@ StatusWithMatchExpression parseAll(StringData name, return {Status(ErrorCodes::BadValue, "$all needs an array")}; auto arr = e.Obj(); - auto myAnd = std::make_unique<AndMatchExpression>( - createAnnotation(expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); + auto myAnd = std::make_unique<AndMatchExpression>(doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); BSONObjIterator i(arr); if (arr.firstElement().type() == BSONType::Object && @@ -1317,7 +1291,8 @@ StatusWithMatchExpression parseAll(StringData name, allowedFeatures); if (!inner.isOK()) return inner; - annotateTreeToIgnoreForErrorDetails(expCtx, inner.getValue().get()); + doc_validation_error::annotateTreeToIgnoreForErrorDetails(expCtx, + inner.getValue().get()); myAnd->add(inner.getValue().release()); } @@ -1329,22 +1304,22 @@ StatusWithMatchExpression parseAll(StringData name, if (e.type() == BSONType::RegEx) { auto expr = std::make_unique<RegexMatchExpression>( - name, e, createAnnotation(expCtx, AnnotationMode::kIgnore)); + name, e, doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); myAnd->add(expr.release()); } else if (e.type() == BSONType::Object && MatchExpressionParser::parsePathAcceptingKeyword(e.Obj().firstElement())) { return {Status(ErrorCodes::BadValue, "no $ expressions in $all")}; } else { auto expr = std::make_unique<EqualityMatchExpression>( - name, e, createAnnotation(expCtx, AnnotationMode::kIgnore)); + name, e, doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); expr->setCollator(expCtx->getCollator()); myAnd->add(expr.release()); } } if (myAnd->numChildren() == 0) { - return {std::make_unique<AlwaysFalseMatchExpression>( - createAnnotation(expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())))}; + return {std::make_unique<AlwaysFalseMatchExpression>(doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())))}; } return {std::move(myAnd)}; @@ -1423,7 +1398,8 @@ StatusWithMatchExpression parseNot(StringData name, return {ErrorCodes::BadValue, "$not cannot be empty"}; } - auto theAnd = std::make_unique<AndMatchExpression>(createAnnotation(expCtx, "$and", BSONObj())); + auto theAnd = std::make_unique<AndMatchExpression>( + doc_validation_error::createAnnotation(expCtx, "$and", BSONObj())); auto parseStatus = parseSub( name, notObject, theAnd.get(), expCtx, extensionsCallback, allowedFeatures, currentLevel); if (!parseStatus.isOK()) { @@ -1432,11 +1408,12 @@ StatusWithMatchExpression parseNot(StringData name, // If the and has one child, it can be ignored when generating a document validation error. if (theAnd->numChildren() == 1 && theAnd->getErrorAnnotation()) { - theAnd->setErrorAnnotation(createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend)); + theAnd->setErrorAnnotation( + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend)); } - return {std::make_unique<NotMatchExpression>(theAnd.release(), - createAnnotation(expCtx, "$not", BSONObj()))}; + return {std::make_unique<NotMatchExpression>( + theAnd.release(), doc_validation_error::createAnnotation(expCtx, "$not", BSONObj()))}; } StatusWithMatchExpression parseInternalSchemaBinDataSubType(StringData name, BSONElement e) { @@ -1481,16 +1458,16 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, invariant(e); if ("$eq"_sd == e.fieldNameStringData()) { - return parseComparison(name, - std::make_unique<EqualityMatchExpression>( - name, - e, - createAnnotation(expCtx, - e.fieldNameStringData().toString(), - BSON(name << e.wrap()))), - e, - expCtx, - allowedFeatures); + return parseComparison( + name, + std::make_unique<EqualityMatchExpression>( + name, + e, + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); } if ("$not"_sd == e.fieldNameStringData()) { @@ -1510,49 +1487,49 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, switch (*parseExpMatchType) { case PathAcceptingKeyword::LESS_THAN: - return parseComparison(name, - std::make_unique<LTMatchExpression>( - name, - e, - createAnnotation(expCtx, - e.fieldNameStringData().toString(), - BSON(name << e.wrap()))), - e, - expCtx, - allowedFeatures); + return parseComparison( + name, + std::make_unique<LTMatchExpression>( + name, + e, + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); case PathAcceptingKeyword::LESS_THAN_OR_EQUAL: - return parseComparison(name, - std::make_unique<LTEMatchExpression>( - name, - e, - createAnnotation(expCtx, - e.fieldNameStringData().toString(), - BSON(name << e.wrap()))), - e, - expCtx, - allowedFeatures); + return parseComparison( + name, + std::make_unique<LTEMatchExpression>( + name, + e, + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); case PathAcceptingKeyword::GREATER_THAN: - return parseComparison(name, - std::make_unique<GTMatchExpression>( - name, - e, - createAnnotation(expCtx, - e.fieldNameStringData().toString(), - BSON(name << e.wrap()))), - e, - expCtx, - allowedFeatures); + return parseComparison( + name, + std::make_unique<GTMatchExpression>( + name, + e, + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); case PathAcceptingKeyword::GREATER_THAN_OR_EQUAL: - return parseComparison(name, - std::make_unique<GTEMatchExpression>( - name, - e, - createAnnotation(expCtx, - e.fieldNameStringData().toString(), - BSON(name << e.wrap()))), - e, - expCtx, - allowedFeatures); + return parseComparison( + name, + std::make_unique<GTEMatchExpression>( + name, + e, + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); case PathAcceptingKeyword::NOT_EQUAL: { if (BSONType::RegEx == e.type()) { // Just because $ne can be rewritten as the negation of an equality does not mean @@ -1564,26 +1541,26 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, std::make_unique<EqualityMatchExpression>( name, e, - createAnnotation( + doc_validation_error::createAnnotation( expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), e, expCtx, allowedFeatures); return {std::make_unique<NotMatchExpression>( s.getValue().release(), - createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; } case PathAcceptingKeyword::EQUALITY: - return parseComparison(name, - std::make_unique<EqualityMatchExpression>( - name, - e, - createAnnotation(expCtx, - e.fieldNameStringData().toString(), - BSON(name << e.wrap()))), - e, - expCtx, - allowedFeatures); + return parseComparison( + name, + std::make_unique<EqualityMatchExpression>( + name, + e, + doc_validation_error::createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); case PathAcceptingKeyword::IN_EXPR: { if (e.type() != BSONType::Array) { @@ -1591,7 +1568,7 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, } auto temp = std::make_unique<InMatchExpression>( name, - createAnnotation( + doc_validation_error::createAnnotation( expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); auto parseStatus = parseInExpression(temp.get(), e.Obj(), expCtx); if (!parseStatus.isOK()) { @@ -1606,14 +1583,15 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, } auto temp = std::make_unique<InMatchExpression>( name, - createAnnotation( + doc_validation_error::createAnnotation( expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); auto parseStatus = parseInExpression(temp.get(), e.Obj(), expCtx); if (!parseStatus.isOK()) { return parseStatus; } return {std::make_unique<NotMatchExpression>( - temp.release(), createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; + temp.release(), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; } case PathAcceptingKeyword::SIZE: { @@ -1643,7 +1621,7 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, return {std::make_unique<SizeMatchExpression>( name, size, - createAnnotation( + doc_validation_error::createAnnotation( expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap())))}; } @@ -1654,14 +1632,15 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, auto existsExpr = std::make_unique<ExistsMatchExpression>( name, - createAnnotation( + doc_validation_error::createAnnotation( expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); if (e.trueValue()) { return {std::move(existsExpr)}; } return {std::make_unique<NotMatchExpression>( - existsExpr.release(), createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; + existsExpr.release(), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; } case PathAcceptingKeyword::TYPE: @@ -1760,7 +1739,9 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, } return {std::make_unique<InternalSchemaObjectMatchExpression>( - name, std::move(parsedSubObjExpr.getValue()))}; + name, + std::move(parsedSubObjExpr.getValue()), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; } case PathAcceptingKeyword::INTERNAL_SCHEMA_UNIQUE_ITEMS: { diff --git a/src/mongo/db/matcher/expression_type.h b/src/mongo/db/matcher/expression_type.h index 13839e47190..437390c4139 100644 --- a/src/mongo/db/matcher/expression_type.h +++ b/src/mongo/db/matcher/expression_type.h @@ -197,11 +197,14 @@ class InternalSchemaBinDataSubTypeExpression final : public LeafMatchExpression public: static constexpr StringData kName = "$_internalSchemaBinDataSubType"_sd; - InternalSchemaBinDataSubTypeExpression(StringData path, BinDataType binDataSubType) + InternalSchemaBinDataSubTypeExpression(StringData path, + BinDataType binDataSubType, + clonable_ptr<ErrorAnnotation> annotation = nullptr) : LeafMatchExpression(MatchExpression::INTERNAL_SCHEMA_BIN_DATA_SUBTYPE, path, ElementPath::LeafArrayBehavior::kNoTraversal, - ElementPath::NonLeafArrayBehavior::kTraverse), + ElementPath::NonLeafArrayBehavior::kTraverse, + std::move(annotation)), _binDataSubType(binDataSubType) {} StringData name() const { @@ -214,8 +217,8 @@ public: } std::unique_ptr<MatchExpression> shallowClone() const final { - auto expr = - std::make_unique<InternalSchemaBinDataSubTypeExpression>(path(), _binDataSubType); + auto expr = std::make_unique<InternalSchemaBinDataSubTypeExpression>( + path(), _binDataSubType, _errorAnnotation); if (getTag()) { expr->setTag(getTag()->clone()); } diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp index 22940d101fa..ec5a0943fad 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp +++ b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp @@ -36,11 +36,14 @@ namespace mongo { constexpr StringData InternalSchemaObjectMatchExpression::kName; InternalSchemaObjectMatchExpression::InternalSchemaObjectMatchExpression( - StringData path, std::unique_ptr<MatchExpression> expr) + StringData path, + std::unique_ptr<MatchExpression> expr, + clonable_ptr<ErrorAnnotation> annotation) : PathMatchExpression(INTERNAL_SCHEMA_OBJECT_MATCH, path, ElementPath::LeafArrayBehavior::kNoTraversal, - ElementPath::NonLeafArrayBehavior::kTraverse), + ElementPath::NonLeafArrayBehavior::kTraverse, + std::move(annotation)), _sub(std::move(expr)) {} bool InternalSchemaObjectMatchExpression::matchesSingleElement(const BSONElement& elem, @@ -75,8 +78,8 @@ bool InternalSchemaObjectMatchExpression::equivalent(const MatchExpression* othe } std::unique_ptr<MatchExpression> InternalSchemaObjectMatchExpression::shallowClone() const { - auto clone = - std::make_unique<InternalSchemaObjectMatchExpression>(path(), _sub->shallowClone()); + auto clone = std::make_unique<InternalSchemaObjectMatchExpression>( + path(), _sub->shallowClone(), _errorAnnotation); if (getTag()) { clone->setTag(getTag()->clone()); } diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h index e3b40eb9329..dee55fbb5c5 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h @@ -39,7 +39,9 @@ class InternalSchemaObjectMatchExpression final : public PathMatchExpression { public: static constexpr StringData kName = "$_internalSchemaObjectMatch"_sd; - InternalSchemaObjectMatchExpression(StringData path, std::unique_ptr<MatchExpression> expr); + InternalSchemaObjectMatchExpression(StringData path, + std::unique_ptr<MatchExpression> expr, + clonable_ptr<ErrorAnnotation> annotation = nullptr); bool matchesSingleElement(const BSONElement& elem, MatchDetails* details = nullptr) const final; diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index a3fec7baf1d..9e11324c732 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -38,6 +38,7 @@ #include "mongo/bson/bsontypes.h" #include "mongo/bson/unordered_fields_bsonelement_comparator.h" #include "mongo/db/commands/feature_compatibility_version_documentation.h" +#include "mongo/db/matcher/doc_validation_util.h" #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/matcher/matcher_type_set.h" @@ -68,6 +69,8 @@ namespace mongo { using PatternSchema = InternalSchemaAllowedPropertiesMatchExpression::PatternSchema; using Pattern = InternalSchemaAllowedPropertiesMatchExpression::Pattern; using AllowedFeatureSet = MatchExpressionParser::AllowedFeatureSet; +using ErrorAnnotation = MatchExpression::ErrorAnnotation; +using AnnotationMode = ErrorAnnotation::Mode; namespace { @@ -114,10 +117,12 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& * enforce this restriction, should the types match (e.g. $_internalSchemaMaxItems). 'statedType' is * a parsed representation of the JSON Schema type keyword which is in effect. */ -std::unique_ptr<MatchExpression> makeRestriction(const MatcherTypeSet& restrictionType, - StringData path, - std::unique_ptr<MatchExpression> restrictionExpr, - InternalSchemaTypeExpression* statedType) { +std::unique_ptr<MatchExpression> makeRestriction( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const MatcherTypeSet& restrictionType, + StringData path, + std::unique_ptr<MatchExpression> restrictionExpr, + InternalSchemaTypeExpression* statedType) { invariant(restrictionType.isSingleType()); if (statedType && statedType->typeSet().isSingleType()) { @@ -132,8 +137,9 @@ std::unique_ptr<MatchExpression> makeRestriction(const MatcherTypeSet& restricti return restrictionExpr; } else { // This restriction doesn't take any effect, since the type of the schema is different - // from the type to which this retriction applies. - return std::make_unique<AlwaysTrueMatchExpression>(); + // from the type to which this restriction applies. + return std::make_unique<AlwaysTrueMatchExpression>( + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); } } @@ -143,11 +149,17 @@ std::unique_ptr<MatchExpression> makeRestriction(const MatcherTypeSet& restricti // // We need to do this because restriction keywords do not apply when a field is either not // present or of a different type. - auto typeExpr = std::make_unique<InternalSchemaTypeExpression>(path, restrictionType); + auto typeExpr = std::make_unique<InternalSchemaTypeExpression>( + path, + restrictionType, + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); - auto notExpr = std::make_unique<NotMatchExpression>(typeExpr.release()); + auto notExpr = std::make_unique<NotMatchExpression>( + typeExpr.release(), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); - auto orExpr = std::make_unique<OrMatchExpression>(); + auto orExpr = std::make_unique<OrMatchExpression>( + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend)); orExpr->add(notExpr.release()); orExpr->add(restrictionExpr.release()); @@ -155,6 +167,7 @@ std::unique_ptr<MatchExpression> makeRestriction(const MatcherTypeSet& restricti } StatusWith<std::unique_ptr<InternalSchemaTypeExpression>> parseType( + const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, StringData keywordName, BSONElement typeElt, @@ -171,13 +184,17 @@ StatusWith<std::unique_ptr<InternalSchemaTypeExpression>> parseType( << "' must name at least one type")}; } - auto typeExpr = - std::make_unique<InternalSchemaTypeExpression>(path, std::move(typeSet.getValue())); + auto typeExpr = std::make_unique<InternalSchemaTypeExpression>( + path, + std::move(typeSet.getValue()), + doc_validation_error::createAnnotation( + expCtx, typeElt.fieldNameStringData().toString(), typeElt.wrap())); return {std::move(typeExpr)}; } -StatusWithMatchExpression parseMaximum(StringData path, +StatusWithMatchExpression parseMaximum(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringData path, BSONElement maximum, InternalSchemaTypeExpression* typeExpr, bool isExclusiveMaximum) { @@ -188,24 +205,36 @@ StatusWithMatchExpression parseMaximum(StringData path, << "' must be a number")}; } + clonable_ptr<ErrorAnnotation> annotation; + if (isExclusiveMaximum) { + annotation = + doc_validation_error::createAnnotation(expCtx, + maximum.fieldNameStringData().toString(), + BSON(maximum << "exclusiveMaximum" << true)); + } else { + annotation = doc_validation_error::createAnnotation( + expCtx, maximum.fieldNameStringData().toString(), maximum.wrap()); + } + if (path.empty()) { // This restriction has no effect in a top-level schema, since we only store objects. - return {std::make_unique<AlwaysTrueMatchExpression>()}; + return {std::make_unique<AlwaysTrueMatchExpression>(std::move(annotation))}; } std::unique_ptr<ComparisonMatchExpression> expr; if (isExclusiveMaximum) { - expr = std::make_unique<LTMatchExpression>(path, maximum); + expr = std::make_unique<LTMatchExpression>(path, maximum, std::move(annotation)); } else { - expr = std::make_unique<LTEMatchExpression>(path, maximum); + expr = std::make_unique<LTEMatchExpression>(path, maximum, std::move(annotation)); } MatcherTypeSet restrictionType; restrictionType.allNumbers = true; - return makeRestriction(restrictionType, path, std::move(expr), typeExpr); + return makeRestriction(expCtx, restrictionType, path, std::move(expr), typeExpr); } -StatusWithMatchExpression parseMinimum(StringData path, +StatusWithMatchExpression parseMinimum(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringData path, BSONElement minimum, InternalSchemaTypeExpression* typeExpr, bool isExclusiveMinimum) { @@ -216,28 +245,40 @@ StatusWithMatchExpression parseMinimum(StringData path, << "' must be a number")}; } + clonable_ptr<ErrorAnnotation> annotation; + if (isExclusiveMinimum) { + annotation = + doc_validation_error::createAnnotation(expCtx, + minimum.fieldNameStringData().toString(), + BSON(minimum << "exclusiveMinimum" << true)); + } else { + annotation = doc_validation_error::createAnnotation( + expCtx, minimum.fieldNameStringData().toString(), minimum.wrap()); + } + if (path.empty()) { // This restriction has no effect in a top-level schema, since we only store objects. - return {std::make_unique<AlwaysTrueMatchExpression>()}; + return {std::make_unique<AlwaysTrueMatchExpression>(std::move(annotation))}; } std::unique_ptr<ComparisonMatchExpression> expr; if (isExclusiveMinimum) { - expr = std::make_unique<GTMatchExpression>(path, minimum); + expr = std::make_unique<GTMatchExpression>(path, minimum, std::move(annotation)); } else { - expr = std::make_unique<GTEMatchExpression>(path, minimum); + expr = std::make_unique<GTEMatchExpression>(path, minimum, std::move(annotation)); } MatcherTypeSet restrictionType; restrictionType.allNumbers = true; - return makeRestriction(restrictionType, path, std::move(expr), typeExpr); + return makeRestriction(expCtx, restrictionType, path, std::move(expr), typeExpr); } /** * Parses length-related keywords that expect a nonnegative long as an argument. */ template <class T> -StatusWithMatchExpression parseLength(StringData path, +StatusWithMatchExpression parseLength(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringData path, BSONElement length, InternalSchemaTypeExpression* typeExpr, BSONType restrictionType) { @@ -251,10 +292,11 @@ StatusWithMatchExpression parseLength(StringData path, } auto expr = std::make_unique<T>(path, parsedLength.getValue()); - return makeRestriction(restrictionType, path, std::move(expr), typeExpr); + return makeRestriction(expCtx, restrictionType, path, std::move(expr), typeExpr); } -StatusWithMatchExpression parsePattern(StringData path, +StatusWithMatchExpression parsePattern(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringData path, BSONElement pattern, InternalSchemaTypeExpression* typeExpr) { if (pattern.type() != BSONType::String) { @@ -272,10 +314,11 @@ StatusWithMatchExpression parsePattern(StringData path, constexpr auto emptyFlags = ""; auto expr = std::make_unique<RegexMatchExpression>(path, pattern.valueStringData(), emptyFlags); - return makeRestriction(BSONType::String, path, std::move(expr), typeExpr); + return makeRestriction(expCtx, BSONType::String, path, std::move(expr), typeExpr); } -StatusWithMatchExpression parseMultipleOf(StringData path, +StatusWithMatchExpression parseMultipleOf(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringData path, BSONElement multipleOf, InternalSchemaTypeExpression* typeExpr) { if (!multipleOf.isNumber()) { @@ -300,7 +343,7 @@ StatusWithMatchExpression parseMultipleOf(StringData path, MatcherTypeSet restrictionType; restrictionType.allNumbers = true; - return makeRestriction(restrictionType, path, std::move(expr), typeExpr); + return makeRestriction(expCtx, restrictionType, path, std::move(expr), typeExpr); } template <class T> @@ -438,7 +481,8 @@ StatusWith<StringDataSet> parseRequired(BSONElement requiredElt) { * Given the already-parsed set of required properties, returns a MatchExpression which ensures that * those properties exist. Returns a parsing error if the translation fails. */ -StatusWithMatchExpression translateRequired(const StringDataSet& requiredProperties, +StatusWithMatchExpression translateRequired(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const StringDataSet& requiredProperties, StringData path, InternalSchemaTypeExpression* typeExpr) { auto andExpr = std::make_unique<AndMatchExpression>(); @@ -458,7 +502,7 @@ StatusWithMatchExpression translateRequired(const StringDataSet& requiredPropert auto objectMatch = std::make_unique<InternalSchemaObjectMatchExpression>(path, std::move(andExpr)); - return makeRestriction(BSONType::Object, path, std::move(objectMatch), typeExpr); + return makeRestriction(expCtx, BSONType::Object, path, std::move(objectMatch), typeExpr); } StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionContext>& expCtx, @@ -476,7 +520,8 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC } auto propertiesObj = propertiesElt.embeddedObject(); - auto andExpr = std::make_unique<AndMatchExpression>(); + auto andExpr = std::make_unique<AndMatchExpression>(doc_validation_error::createAnnotation( + expCtx, propertiesElt.fieldNameStringData().toString(), BSONObj())); for (auto&& property : propertiesObj) { if (property.type() != BSONType::Object) { return {ErrorCodes::TypeMismatch, @@ -493,6 +538,8 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC return nestedSchemaMatch.getStatus(); } + nestedSchemaMatch.getValue()->setErrorAnnotation(doc_validation_error::createAnnotation( + expCtx, "", BSON("propertyName" << property.fieldNameStringData().toString()))); if (requiredProperties.find(property.fieldNameStringData()) != requiredProperties.end()) { // The field name for which we created the nested schema is a required property. This // property must exist and therefore must match 'nestedSchemaMatch'. @@ -500,12 +547,16 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC } else { // This property either must not exist or must match the nested schema. Therefore, we // generate the match expression (OR (NOT (EXISTS)) <nestedSchemaMatch>). - auto existsExpr = - std::make_unique<ExistsMatchExpression>(property.fieldNameStringData()); + auto existsExpr = std::make_unique<ExistsMatchExpression>( + property.fieldNameStringData(), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); - auto notExpr = std::make_unique<NotMatchExpression>(existsExpr.release()); + auto notExpr = std::make_unique<NotMatchExpression>( + existsExpr.release(), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); - auto orExpr = std::make_unique<OrMatchExpression>(); + auto orExpr = std::make_unique<OrMatchExpression>( + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend)); orExpr->add(notExpr.release()); orExpr->add(nestedSchemaMatch.getValue().release()); @@ -519,10 +570,12 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC return {std::move(andExpr)}; } - auto objectMatch = - std::make_unique<InternalSchemaObjectMatchExpression>(path, std::move(andExpr)); + auto objectMatch = std::make_unique<InternalSchemaObjectMatchExpression>( + path, + std::move(andExpr), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend)); - return makeRestriction(BSONType::Object, path, std::move(objectMatch), typeExpr); + return makeRestriction(expCtx, BSONType::Object, path, std::move(objectMatch), typeExpr); } StatusWith<std::vector<PatternSchema>> parsePatternProperties( @@ -665,14 +718,15 @@ StatusWithMatchExpression parseAllowedProperties( auto objectMatch = std::make_unique<InternalSchemaObjectMatchExpression>( path, std::move(allowedPropertiesExpr)); - return makeRestriction(BSONType::Object, path, std::move(objectMatch), typeExpr); + return makeRestriction(expCtx, BSONType::Object, path, std::move(objectMatch), typeExpr); } /** * Parses 'minProperties' and 'maxProperties' JSON Schema keywords. */ template <class T> -StatusWithMatchExpression parseNumProperties(StringData path, +StatusWithMatchExpression parseNumProperties(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringData path, BSONElement numProperties, InternalSchemaTypeExpression* typeExpr) { auto parsedNumProps = numProperties.parseIntegerElementToNonNegativeLong(); @@ -689,7 +743,7 @@ StatusWithMatchExpression parseNumProperties(StringData path, auto objectMatch = std::make_unique<InternalSchemaObjectMatchExpression>(path, std::move(expr)); - return makeRestriction(BSONType::Object, path, std::move(objectMatch), typeExpr); + return makeRestriction(expCtx, BSONType::Object, path, std::move(objectMatch), typeExpr); } StatusWithMatchExpression makeDependencyExistsClause(StringData path, StringData dependencyName) { @@ -825,7 +879,8 @@ StatusWithMatchExpression parseDependencies(const boost::intrusive_ptr<Expressio return {std::move(andExpr)}; } -StatusWithMatchExpression parseUniqueItems(BSONElement uniqueItemsElt, +StatusWithMatchExpression parseUniqueItems(const boost::intrusive_ptr<ExpressionContext>& expCtx, + BSONElement uniqueItemsElt, StringData path, InternalSchemaTypeExpression* typeExpr) { if (!uniqueItemsElt.isBoolean()) { @@ -837,7 +892,7 @@ StatusWithMatchExpression parseUniqueItems(BSONElement uniqueItemsElt, return {std::make_unique<AlwaysTrueMatchExpression>()}; } else if (uniqueItemsElt.boolean()) { auto uniqueItemsExpr = std::make_unique<InternalSchemaUniqueItemsMatchExpression>(path); - return makeRestriction(BSONType::Array, path, std::move(uniqueItemsExpr), typeExpr); + return makeRestriction(expCtx, BSONType::Array, path, std::move(uniqueItemsExpr), typeExpr); } return {std::make_unique<AlwaysTrueMatchExpression>()}; @@ -894,7 +949,8 @@ StatusWith<boost::optional<long long>> parseItems( andExpr->add(std::make_unique<AlwaysTrueMatchExpression>().release()); } else { andExpr->add( - makeRestriction(BSONType::Array, path, std::move(andExprForSubschemas), typeExpr) + makeRestriction( + expCtx, BSONType::Array, path, std::move(andExprForSubschemas), typeExpr) .release()); } } else if (itemsElt.type() == BSONType::Object) { @@ -919,8 +975,9 @@ StatusWith<boost::optional<long long>> parseItems( auto allElemMatch = std::make_unique<InternalSchemaAllElemMatchFromIndexMatchExpression>( path, startIndexForItems, std::move(exprWithPlaceholder)); - andExpr->add(makeRestriction(BSONType::Array, path, std::move(allElemMatch), typeExpr) - .release()); + andExpr->add( + makeRestriction(expCtx, BSONType::Array, path, std::move(allElemMatch), typeExpr) + .release()); } } else { return {ErrorCodes::TypeMismatch, @@ -976,8 +1033,9 @@ Status parseAdditionalItems(const boost::intrusive_ptr<ExpressionContext>& expCt auto allElemMatch = std::make_unique<InternalSchemaAllElemMatchFromIndexMatchExpression>( path, *startIndexForAdditionalItems, std::move(otherwiseExpr)); - andExpr->add(makeRestriction(BSONType::Array, path, std::move(allElemMatch), typeExpr) - .release()); + andExpr->add( + makeRestriction(expCtx, BSONType::Array, path, std::move(allElemMatch), typeExpr) + .release()); } } return Status::OK(); @@ -1106,7 +1164,7 @@ Status translateArrayKeywords(StringMap<BSONElement>& keywordMap, AndMatchExpression* andExpr) { if (auto minItemsElt = keywordMap[JSONSchemaParser::kSchemaMinItemsKeyword]) { auto minItemsExpr = parseLength<InternalSchemaMinItemsMatchExpression>( - path, minItemsElt, typeExpr, BSONType::Array); + expCtx, path, minItemsElt, typeExpr, BSONType::Array); if (!minItemsExpr.isOK()) { return minItemsExpr.getStatus(); } @@ -1115,7 +1173,7 @@ Status translateArrayKeywords(StringMap<BSONElement>& keywordMap, if (auto maxItemsElt = keywordMap[JSONSchemaParser::kSchemaMaxItemsKeyword]) { auto maxItemsExpr = parseLength<InternalSchemaMaxItemsMatchExpression>( - path, maxItemsElt, typeExpr, BSONType::Array); + expCtx, path, maxItemsElt, typeExpr, BSONType::Array); if (!maxItemsExpr.isOK()) { return maxItemsExpr.getStatus(); } @@ -1123,7 +1181,7 @@ Status translateArrayKeywords(StringMap<BSONElement>& keywordMap, } if (auto uniqueItemsElt = keywordMap[JSONSchemaParser::kSchemaUniqueItemsKeyword]) { - auto uniqueItemsExpr = parseUniqueItems(uniqueItemsElt, path, typeExpr); + auto uniqueItemsExpr = parseUniqueItems(expCtx, uniqueItemsElt, path, typeExpr); if (!uniqueItemsExpr.isOK()) { return uniqueItemsExpr.getStatus(); } @@ -1200,7 +1258,7 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, } if (!requiredProperties.empty()) { - auto requiredExpr = translateRequired(requiredProperties, path, typeExpr); + auto requiredExpr = translateRequired(expCtx, requiredProperties, path, typeExpr); if (!requiredExpr.isOK()) { return requiredExpr.getStatus(); } @@ -1209,7 +1267,7 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, if (auto minPropertiesElt = keywordMap[JSONSchemaParser::kSchemaMinPropertiesKeyword]) { auto minPropExpr = parseNumProperties<InternalSchemaMinPropertiesMatchExpression>( - path, minPropertiesElt, typeExpr); + expCtx, path, minPropertiesElt, typeExpr); if (!minPropExpr.isOK()) { return minPropExpr.getStatus(); } @@ -1218,7 +1276,7 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, if (auto maxPropertiesElt = keywordMap[JSONSchemaParser::kSchemaMaxPropertiesKeyword]) { auto maxPropExpr = parseNumProperties<InternalSchemaMaxPropertiesMatchExpression>( - path, maxPropertiesElt, typeExpr); + expCtx, path, maxPropertiesElt, typeExpr); if (!maxPropExpr.isOK()) { return maxPropExpr.getStatus(); } @@ -1251,13 +1309,14 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, * - pattern * - multipleOf */ -Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, +Status translateScalarKeywords(const boost::intrusive_ptr<ExpressionContext>& expCtx, + StringMap<BSONElement>& keywordMap, StringData path, InternalSchemaTypeExpression* typeExpr, AndMatchExpression* andExpr) { // String keywords. if (auto patternElt = keywordMap[JSONSchemaParser::kSchemaPatternKeyword]) { - auto patternExpr = parsePattern(path, patternElt, typeExpr); + auto patternExpr = parsePattern(expCtx, path, patternElt, typeExpr); if (!patternExpr.isOK()) { return patternExpr.getStatus(); } @@ -1266,7 +1325,7 @@ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, if (auto maxLengthElt = keywordMap[JSONSchemaParser::kSchemaMaxLengthKeyword]) { auto maxLengthExpr = parseLength<InternalSchemaMaxLengthMatchExpression>( - path, maxLengthElt, typeExpr, BSONType::String); + expCtx, path, maxLengthElt, typeExpr, BSONType::String); if (!maxLengthExpr.isOK()) { return maxLengthExpr.getStatus(); } @@ -1275,7 +1334,7 @@ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, if (auto minLengthElt = keywordMap[JSONSchemaParser::kSchemaMinLengthKeyword]) { auto minLengthExpr = parseLength<InternalSchemaMinLengthMatchExpression>( - path, minLengthElt, typeExpr, BSONType::String); + expCtx, path, minLengthElt, typeExpr, BSONType::String); if (!minLengthExpr.isOK()) { return minLengthExpr.getStatus(); } @@ -1284,7 +1343,7 @@ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, // Numeric keywords. if (auto multipleOfElt = keywordMap[JSONSchemaParser::kSchemaMultipleOfKeyword]) { - auto multipleOfExpr = parseMultipleOf(path, multipleOfElt, typeExpr); + auto multipleOfExpr = parseMultipleOf(expCtx, path, multipleOfElt, typeExpr); if (!multipleOfExpr.isOK()) { return multipleOfExpr.getStatus(); } @@ -1304,7 +1363,7 @@ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, isExclusiveMaximum = exclusiveMaximumElt.boolean(); } } - auto maxExpr = parseMaximum(path, maximumElt, typeExpr, isExclusiveMaximum); + auto maxExpr = parseMaximum(expCtx, path, maximumElt, typeExpr, isExclusiveMaximum); if (!maxExpr.isOK()) { return maxExpr.getStatus(); } @@ -1330,7 +1389,7 @@ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, isExclusiveMinimum = exclusiveMinimumElt.boolean(); } } - auto minExpr = parseMinimum(path, minimumElt, typeExpr, isExclusiveMinimum); + auto minExpr = parseMinimum(expCtx, path, minimumElt, typeExpr, isExclusiveMinimum); if (!minExpr.isOK()) { return minExpr.getStatus(); } @@ -1543,7 +1602,8 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& std::unique_ptr<InternalSchemaTypeExpression> typeExpr; if (typeElem) { - auto parsed = parseType(path, + auto parsed = parseType(expCtx, + path, JSONSchemaParser::kSchemaTypeKeyword, typeElem, MatcherTypeSet::findJsonSchemaTypeAlias); @@ -1552,8 +1612,11 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& } typeExpr = std::move(parsed.getValue()); } else if (bsonTypeElem) { - auto parseBsonTypeResult = parseType( - path, JSONSchemaParser::kSchemaBsonTypeKeyword, bsonTypeElem, findBSONTypeAlias); + auto parseBsonTypeResult = parseType(expCtx, + path, + JSONSchemaParser::kSchemaBsonTypeKeyword, + bsonTypeElem, + findBSONTypeAlias); if (!parseBsonTypeResult.isOK()) { return parseBsonTypeResult.getStatus(); } @@ -1565,10 +1628,11 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& std::make_unique<InternalSchemaTypeExpression>(path, MatcherTypeSet(BSONType::BinData)); } - auto andExpr = std::make_unique<AndMatchExpression>(); + auto andExpr = std::make_unique<AndMatchExpression>( + doc_validation_error::createAnnotation(expCtx, "$jsonSchema", BSONObj())); auto translationStatus = - translateScalarKeywords(keywordMap, path, typeExpr.get(), andExpr.get()); + translateScalarKeywords(expCtx, keywordMap, path, typeExpr.get(), andExpr.get()); if (!translationStatus.isOK()) { return translationStatus; } @@ -1610,7 +1674,8 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& if (path.empty() && typeExpr && !typeExpr->typeSet().hasType(BSONType::Object)) { // This is a top-level schema which requires that the type is something other than // "object". Since we only know how to store objects, this schema matches nothing. - return {std::make_unique<AlwaysFalseMatchExpression>()}; + return {std::make_unique<AlwaysFalseMatchExpression>(doc_validation_error::createAnnotation( + expCtx, "$jsonSchema", BSON("$jsonSchema" << schema)))}; } if (!path.empty() && typeExpr) { |