diff options
author | Mihai Andrei <mihai.andrei@10gen.com> | 2020-06-30 09:39:19 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-07-15 21:54:42 +0000 |
commit | eaaee39e2c4eaf9938c5a75bce30648435ae10cc (patch) | |
tree | 07a9d7840070233da4ce2cb45d5fdc6ce98fa398 | |
parent | b52de8036887396dec91c32b531790e7b1fe01cc (diff) | |
download | mongo-eaaee39e2c4eaf9938c5a75bce30648435ae10cc.tar.gz |
SERVER-49022 Implement validation error generation for comparison query operators
-rw-r--r-- | src/mongo/db/matcher/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error.cpp | 147 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_test.cpp | 389 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression.h | 55 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf.cpp | 18 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf.h | 63 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.cpp | 141 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_path.h | 5 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_tree.h | 6 |
10 files changed, 754 insertions, 74 deletions
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index 471dd951305..7ef7f899c9a 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -97,6 +97,7 @@ env.Library( env.CppUnitTest( target='db_matcher_test', source=[ + 'doc_validation_error_test.cpp', 'expression_algo_test.cpp', 'expression_always_boolean_test.cpp', 'expression_array_test.cpp', diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp index 4018443dbd4..4f19849af73 100644 --- a/src/mongo/db/matcher/doc_validation_error.cpp +++ b/src/mongo/db/matcher/doc_validation_error.cpp @@ -32,6 +32,8 @@ #include "mongo/base/init.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/matcher/doc_validation_error.h" +#include "mongo/db/matcher/expression_leaf.h" +#include "mongo/db/matcher/expression_tree.h" #include "mongo/db/matcher/expression_visitor.h" #include "mongo/db/matcher/match_expression_walker.h" @@ -51,7 +53,41 @@ enum class InvertError { kNormal, kInverted }; /** * A struct which tracks context during error generation. */ -struct ValidationErrorContext {}; +struct ValidationErrorContext { + ValidationErrorContext(const MatchableDocument* doc) : doc(doc) {} + + /** + * Returns the complete document validation error as a BSONObj once error generation has + * finished. + */ + BSONObj done() { + // When error generation is finished, there must be exactly one BSONObjBuilder which + // contains the complete error. + return objBuilder.obj(); + } + + BSONObjBuilder& getCurrentObjBuilder() { + return objBuilder; + } + + /** + * Sets 'inversion' to the opposite of its current value. + */ + void flipInversion() { + inversion = + inversion == InvertError::kNormal ? InvertError::kInverted : InvertError::kNormal; + } + + // BSONObjBuilder which is used to construct the generated error. + BSONObjBuilder objBuilder; + // Document which failed to match against the collection's validator. + const MatchableDocument* doc; + // Tracks whether the generated error should be described normally or in an inverted context. + InvertError inversion = InvertError::kNormal; +}; + +using ErrorAnnotation = MatchExpression::ErrorAnnotation; +using Mode = ErrorAnnotation::Mode; /** * Visitor which is primarily responsible for error generation. @@ -68,16 +104,26 @@ public: void visit(const BitsAnySetMatchExpression* expr) final {} void visit(const ElemMatchObjectMatchExpression* expr) final {} void visit(const ElemMatchValueMatchExpression* expr) final {} - void visit(const EqualityMatchExpression* expr) final {} + void visit(const EqualityMatchExpression* expr) final { + generateComparisonError(expr); + } void visit(const ExistsMatchExpression* expr) final {} void visit(const ExprMatchExpression* expr) final {} - void visit(const GTEMatchExpression* expr) final {} - void visit(const GTMatchExpression* expr) final {} + void visit(const GTEMatchExpression* expr) final { + generateComparisonError(expr); + } + void visit(const GTMatchExpression* expr) final { + generateComparisonError(expr); + } void visit(const GeoMatchExpression* expr) final {} void visit(const GeoNearMatchExpression* expr) final { MONGO_UNREACHABLE; } - void visit(const InMatchExpression* expr) final {} + void visit(const InMatchExpression* expr) final { + static constexpr auto kNormalReason = "no matching value found in array"; + static constexpr auto kInvertedReason = "matching value found in array"; + generateLeafError(expr, kNormalReason, kInvertedReason); + } void visit(const InternalExprEqMatchExpression* expr) final {} void visit(const InternalSchemaAllElemMatchFromIndexMatchExpression* expr) final {} void visit(const InternalSchemaAllowedPropertiesMatchExpression* expr) final {} @@ -98,11 +144,17 @@ public: void visit(const InternalSchemaTypeExpression* expr) final {} void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final {} void visit(const InternalSchemaXorMatchExpression* expr) final {} - void visit(const LTEMatchExpression* expr) final {} - void visit(const LTMatchExpression* expr) final {} + void visit(const LTEMatchExpression* expr) final { + generateComparisonError(expr); + } + void visit(const LTMatchExpression* expr) final { + generateComparisonError(expr); + } void visit(const ModMatchExpression* expr) final {} void visit(const NorMatchExpression* expr) final {} - void visit(const NotMatchExpression* expr) final {} + void visit(const NotMatchExpression* expr) final { + _context->flipInversion(); + } void visit(const OrMatchExpression* expr) final {} void visit(const RegexMatchExpression* expr) final {} void visit(const SizeMatchExpression* expr) final {} @@ -122,6 +174,75 @@ 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); + } + void appendSpecifiedAs(const ErrorAnnotation& annotation, BSONObjBuilder* bob) { + bob->append("specifiedAs", annotation.annotation); + } + + /** + * Given a pointer to a LeafMatchExpression 'expr', appends details to the current + * 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 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 + * document resolves to an array of values that is implicitly traversed by 'expr'. + */ + void appendLeafErrorDetails(const LeafMatchExpression* expr, const std::string& reason) { + BSONObjBuilder* bob = &(_context->getCurrentObjBuilder()); + ElementPath path(expr->path()); + MatchableDocument::IteratorHolder cursor(_context->doc, &path); + BSONArrayBuilder bab; + while (cursor->more()) { + auto elem = cursor->next().element(); + if (elem.eoo()) { + break; + } else { + bab.append(elem); + } + } + auto size = bab.arrSize(); + if (size == 0) { + bob->append("reason", "field was missing"); + } else { + bob->append("reason", reason); + auto arr = bab.arr(); + if (size == 1) { + bob->appendAs(arr[0], "consideredValue"); + } else { + bob->append("consideredValues", arr); + } + } + } + + void generateLeafError(const LeafMatchExpression* expr, + const std::string& normalReason, + const std::string& invertedReason) { + if (auto annotationPtr = expr->getErrorAnnotation()) { + const auto& annotation = *annotationPtr; + BSONObjBuilder& bob = _context->getCurrentObjBuilder(); + if (annotation.mode == Mode::kGenerateError) { + appendOperatorName(annotation, &bob); + appendSpecifiedAs(annotation, &bob); + if (_context->inversion == InvertError::kNormal) { + appendLeafErrorDetails(expr, normalReason); + } else { + appendLeafErrorDetails(expr, invertedReason); + } + } + } + } + + void generateComparisonError(const ComparisonMatchExpression* expr) { + static constexpr auto normalReason = "comparison failed"; + static constexpr auto invertedReason = "comparison succeeded"; + generateLeafError(expr, normalReason, invertedReason); + } + ValidationErrorContext* _context; }; @@ -246,7 +367,9 @@ public: void visit(const LTMatchExpression* expr) final {} void visit(const ModMatchExpression* expr) final {} void visit(const NorMatchExpression* expr) final {} - void visit(const NotMatchExpression* expr) final {} + void visit(const NotMatchExpression* expr) final { + _context->flipInversion(); + } void visit(const OrMatchExpression* expr) final {} void visit(const RegexMatchExpression* expr) final {} void visit(const SizeMatchExpression* expr) final {} @@ -287,13 +410,13 @@ const BSONObj& DocumentValidationFailureInfo::getDetails() const { } BSONObj generateError(const MatchExpression& validatorExpr, const BSONObj& doc) { BSONMatchableDocument matchableDoc(doc); - ValidationErrorContext context; + ValidationErrorContext context(&matchableDoc); ValidationErrorPreVisitor preVisitor{&context}; ValidationErrorInVisitor inVisitor{&context}; ValidationErrorPostVisitor postVisitor{&context}; MatchExpressionWalker walker{&preVisitor, &inVisitor, &postVisitor}; tree_walker::walk<true, MatchExpression>(&validatorExpr, &walker); - return BSONObj(); + return context.done(); } -} // namespace mongo::doc_validation_error +} // namespace mongo::doc_validation_error
\ No newline at end of file diff --git a/src/mongo/db/matcher/doc_validation_error_test.cpp b/src/mongo/db/matcher/doc_validation_error_test.cpp new file mode 100644 index 00000000000..cf2199d8529 --- /dev/null +++ b/src/mongo/db/matcher/doc_validation_error_test.cpp @@ -0,0 +1,389 @@ +/** + * Copyright (C) 2018-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.h" +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.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) { + 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); + ASSERT_BSONOBJ_EQ(generatedError, expectedError); +} + +// Comparison operators. +// $eq +TEST(ComparisonMatchExpression, BasicEq) { + BSONObj query = BSON("a" << BSON("$eq" << 2)); + BSONObj document = BSON("a" << 1); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 1); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, EqMissingPath) { + BSONObj query = BSON("a" << BSON("$eq" << 2)); + BSONObj document = BSON("b" << 1); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "field was missing"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, EqImplicitArrayTraversal) { + BSONObj query = BSON("a" << BSON("$eq" << 2)); + BSONObj document = BSON("a" << BSON_ARRAY(3 << 4 << 5)); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" + << BSON_ARRAY(3 << 4 << 5 << BSON_ARRAY(3 << 4 << 5))); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedDocumentSingleElement) { + BSONObj query = BSON("a.b" << BSON("$eq" << 2)); + BSONObj document = BSON("a" << BSON_ARRAY(BSON("b" << 3))); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 3); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedDocument) { + BSONObj query = BSON("a.b" << BSON("$eq" << 2)); + BSONObj document = BSON("a" << BSON_ARRAY(BSON("b" << 3) << BSON("b" << 4) << BSON("b" << 5))); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" << BSON_ARRAY(3 << 4 << 5)); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, EqImplicitArrayTraversalNestedArrays) { + BSONObj query = BSON("a.b" << BSON("$eq" << 2)); + BSONObj document = + BSON("a" << BSON_ARRAY(BSON("b" << BSON_ARRAY(1 << 2)) << BSON("b" << BSON_ARRAY(3 << 4)))); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" + << BSON_ARRAY(1 << 2 << BSON_ARRAY(1 << 2) << 3 << 4 + << BSON_ARRAY(3 << 4))); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, EqNoOperator) { + BSONObj query = BSON("a" << 2); + BSONObj document = BSON("a" << 1); + BSONObj expectedError = BSON("operatorName" + << "$eq" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 1); + verifyGeneratedError(query, document, expectedError); +} + +// $ne +TEST(ComparisonMatchExpression, BasicNe) { + BSONObj query = BSON("a" << BSON("$ne" << 2)); + BSONObj document = BSON("a" << 2); + BSONObj expectedError = BSON("operatorName" + << "$ne" + << "specifiedAs" << query << "reason" + << "comparison succeeded" + << "consideredValue" << 2); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, NeImplicitArrayTraversal) { + BSONObj query = BSON("a" << BSON("$ne" << 2)); + BSONObj document = BSON("a" << BSON_ARRAY(1 << 2 << 3)); + BSONObj expectedError = BSON("operatorName" + << "$ne" + << "specifiedAs" << query << "reason" + << "comparison succeeded" + << "consideredValues" + << BSON_ARRAY(1 << 2 << 3 << BSON_ARRAY(1 << 2 << 3))); + verifyGeneratedError(query, document, expectedError); +} + +// $lt +TEST(ComparisonMatchExpression, BasicLt) { + BSONObj query = BSON("a" << BSON("$lt" << 0)); + BSONObj document = BSON("a" << 1); + BSONObj expectedError = BSON("operatorName" + << "$lt" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 1); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, LtMissingPath) { + BSONObj query = BSON("a" << BSON("$lt" << 0)); + BSONObj document = BSON("b" << 1); + BSONObj expectedError = BSON("operatorName" + << "$lt" + << "specifiedAs" << query << "reason" + << "field was missing"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, LtImplicitArrayTraversal) { + BSONObj query = BSON("a" << BSON("$lt" << 0)); + BSONObj document = BSON("a" << BSON_ARRAY(3 << 4 << 5)); + BSONObj expectedError = BSON("operatorName" + << "$lt" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" + << BSON_ARRAY(3 << 4 << 5 << BSON_ARRAY(3 << 4 << 5))); + verifyGeneratedError(query, document, expectedError); +} + +// $lte +TEST(ComparisonMatchExpression, BasicLte) { + BSONObj query = BSON("a" << BSON("$lte" << 0)); + BSONObj document = BSON("a" << 1); + BSONObj expectedError = BSON("operatorName" + << "$lte" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 1); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, LteMissingPath) { + BSONObj query = BSON("a" << BSON("$lte" << 0)); + BSONObj document = BSON("b" << 1); + BSONObj expectedError = BSON("operatorName" + << "$lte" + << "specifiedAs" << query << "reason" + << "field was missing"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, LteImplicitArrayTraversal) { + BSONObj query = BSON("a" << BSON("$lte" << 0)); + BSONObj document = BSON("a" << BSON_ARRAY(3 << 4 << 5)); + BSONObj expectedError = BSON("operatorName" + << "$lte" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" + << BSON_ARRAY(3 << 4 << 5 << BSON_ARRAY(3 << 4 << 5))); + verifyGeneratedError(query, document, expectedError); +} + +// $gt +TEST(ComparisonMatchExpression, BasicGt) { + BSONObj query = BSON("a" << BSON("$gt" << 3)); + BSONObj document = BSON("a" << 0); + BSONObj expectedError = BSON("operatorName" + << "$gt" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 0); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, GtMissingPath) { + BSONObj query = BSON("a" << BSON("$gt" << 3)); + BSONObj document = BSON("b" << 1); + BSONObj expectedError = BSON("operatorName" + << "$gt" + << "specifiedAs" << query << "reason" + << "field was missing"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, GtImplicitArrayTraversal) { + BSONObj query = BSON("a" << BSON("$gt" << 3)); + BSONObj document = BSON("a" << BSON_ARRAY(0 << 1 << 2)); + BSONObj expectedError = BSON("operatorName" + << "$gt" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" + << BSON_ARRAY(0 << 1 << 2 << BSON_ARRAY(0 << 1 << 2))); + verifyGeneratedError(query, document, expectedError); +} + +// $gte +TEST(ComparisonMatchExpression, BasicGte) { + BSONObj query = BSON("a" << BSON("$gte" << 3)); + BSONObj document = BSON("a" << 0); + BSONObj expectedError = BSON("operatorName" + << "$gte" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValue" << 0); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, GteMissingPath) { + BSONObj query = BSON("a" << BSON("$gte" << 3)); + BSONObj document = BSON("b" << 1); + BSONObj expectedError = BSON("operatorName" + << "$gte" + << "specifiedAs" << query << "reason" + << "field was missing"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, GteImplicitArrayTraversal) { + BSONObj query = BSON("a" << BSON("$gte" << 3)); + BSONObj document = BSON("a" << BSON_ARRAY(0 << 1 << 2)); + BSONObj expectedError = BSON("operatorName" + << "$gte" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" + << BSON_ARRAY(0 << 1 << 2 << BSON_ARRAY(0 << 1 << 2))); + verifyGeneratedError(query, document, expectedError); +} + +// $in +TEST(ComparisonMatchExpression, BasicIn) { + BSONObj query = BSON("a" << BSON("$in" << BSON_ARRAY(1 << 2 << 3))); + BSONObj document = BSON("a" << 4); + BSONObj expectedError = BSON("operatorName" + << "$in" + << "specifiedAs" << query << "reason" + << "no matching value found in array" + << "consideredValue" << 4); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, InMissingPath) { + BSONObj query = BSON("a" << BSON("$in" << BSON_ARRAY(1 << 2 << 3))); + BSONObj document = BSON("b" << 1); + BSONObj expectedError = BSON("operatorName" + << "$in" + << "specifiedAs" << query << "reason" + << "field was missing"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, InNestedDocumentsAndArrays) { + BSONObj query = + BSON("a.b" << BSON("$in" << BSON_ARRAY(5 << 6 << 7 << BSON_ARRAY(2 << 3 << 4)))); + BSONObj document = + BSON("a" << BSON_ARRAY(BSON("b" << BSON_ARRAY(1 << 2)) << BSON("b" << BSON_ARRAY(3 << 4)))); + BSONObj expectedError = BSON("operatorName" + << "$in" + << "specifiedAs" << query << "reason" + << "no matching value found in array" + << "consideredValues" + << BSON_ARRAY(1 << 2 << BSON_ARRAY(1 << 2) << 3 << 4 + << BSON_ARRAY(3 << 4))); + verifyGeneratedError(query, document, expectedError); +} + +// $nin +TEST(ComparisonMatchExpression, BasicNin) { + BSONObj query = BSON("a" << BSON("$nin" << BSON_ARRAY(1 << 2 << 3))); + BSONObj document = BSON("a" << 3); + BSONObj expectedError = BSON("operatorName" + << "$nin" + << "specifiedAs" << query << "reason" + << "matching value found in array" + << "consideredValue" << 3); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, NinNestedDocumentsAndArrays) { + BSONObj query = BSON("a.b" << BSON("$nin" << BSON_ARRAY(1 << BSON_ARRAY(2 << 3 << 4)))); + BSONObj document = + BSON("a" << BSON_ARRAY(BSON("b" << BSON_ARRAY(1 << 2)) << BSON("b" << BSON_ARRAY(3 << 4)))); + BSONObj expectedError = BSON("operatorName" + << "$nin" + << "specifiedAs" << query << "reason" + << "matching value found in array" + << "consideredValues" + << BSON_ARRAY(1 << 2 << BSON_ARRAY(1 << 2) << 3 << 4 + << BSON_ARRAY(3 << 4))); + verifyGeneratedError(query, document, expectedError); +} + +// Verify that Comparison operators which accept a regex ($in and $nin) work as expected. +TEST(ComparisonMatchExpression, InAcceptsRegex) { + BSONObj query = BSON("a" << BSON("$in" << BSON_ARRAY("/^v/" + << "/^b/" + << "/^c/"))); + BSONObj document = BSON("a" + << "Validation"); + BSONObj expectedError = BSON("operatorName" + << "$in" + << "specifiedAs" << query << "reason" + << "no matching value found in array" + << "consideredValue" + << "Validation"); + verifyGeneratedError(query, document, expectedError); +} + +TEST(ComparisonMatchExpression, NinAcceptsRegex) { + BSONObj query = BSON("a" << BSON("$nin" << BSON_ARRAY("/^v/" + << "/^b/" + << "/^c/"))); + BSONObj document = BSON("a" + << "berry"); + BSONObj expectedError = BSON("operatorName" + << "$nin" + << "specifiedAs" << query << "reason" + << "matching value found in array" + << "consideredValue" + << "berry"); + verifyGeneratedError(query, document, expectedError); +} +} // namespace +} // namespace mongo
\ No newline at end of file diff --git a/src/mongo/db/matcher/expression.cpp b/src/mongo/db/matcher/expression.cpp index 680b75a8001..5c7908ff70f 100644 --- a/src/mongo/db/matcher/expression.cpp +++ b/src/mongo/db/matcher/expression.cpp @@ -97,7 +97,8 @@ bool matchExpressionLessThan(const MatchExpression* lhs, const MatchExpression* } // namespace -MatchExpression::MatchExpression(MatchType type) : _matchType(type) {} +MatchExpression::MatchExpression(MatchType type, clonable_ptr<ErrorAnnotation> annotation) + : _errorAnnotation(std::move(annotation)), _matchType(type) {} // static void MatchExpression::sortTree(MatchExpression* tree) { diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h index e84e3ee0982..3207f08d5bf 100644 --- a/src/mongo/db/matcher/expression.h +++ b/src/mongo/db/matcher/expression.h @@ -33,6 +33,7 @@ #include <functional> #include <memory> +#include "mongo/base/clonable_ptr.h" #include "mongo/base/status.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" @@ -203,6 +204,51 @@ public: using ConstIterator = MatchExpressionIterator<true>; /** + * Tracks the information needed to generate a document validation error for a + * MatchExpression node. + */ + struct ErrorAnnotation { + /** + * Enumerated type describing what action to take when generating errors for document + * validation failures. + */ + enum class Mode { + // Do not generate an error for this MatchExpression or any of its children. + kIgnore, + // Do not generate an error for this MatchExpression, but iterate over any children + // as they may provide useful information. This is particularly useful for translated + // jsonSchema keywords. + kIgnoreButDescend, + // Generate an error message. + kGenerateError, + }; + + /** + * Constructs an annotation for a MatchExpression which does not contribute to error output. + */ + ErrorAnnotation(Mode mode) : operatorName(""), annotation(BSONObj()), mode(mode) { + invariant(mode != Mode::kGenerateError); + } + + /** + * Constructs a complete annotation for a MatchExpression which contributes to error output. + */ + ErrorAnnotation(std::string operatorName, BSONObj annotation) + : operatorName(std::move(operatorName)), + annotation(annotation.getOwned()), + mode(Mode::kGenerateError) {} + + std::unique_ptr<ErrorAnnotation> clone() const { + return std::make_unique<ErrorAnnotation>(*this); + } + + const std::string operatorName; + // Tracks the original expression as specified by the user. + const BSONObj annotation; + const Mode mode; + }; + + /** * Make simplifying changes to the structure of a MatchExpression tree without altering its * semantics. This function may return: * - a pointer to the original, unmodified MatchExpression, @@ -243,7 +289,7 @@ public: return tree; } - MatchExpression(MatchType type); + MatchExpression(MatchType type, clonable_ptr<ErrorAnnotation> annotation = nullptr); virtual ~MatchExpression() {} // @@ -411,6 +457,11 @@ public: virtual void acceptVisitor(MatchExpressionMutableVisitor* visitor) = 0; virtual void acceptVisitor(MatchExpressionConstVisitor* visitor) const = 0; + // Returns nullptr if this MatchExpression node has no annotation. + ErrorAnnotation* getErrorAnnotation() const { + return _errorAnnotation.get(); + } + // // Debug information // @@ -449,6 +500,8 @@ protected: void _debugAddSpace(StringBuilder& debug, int indentationLevel) const; + clonable_ptr<ErrorAnnotation> _errorAnnotation; + private: /** * Subclasses should implement this function to provide an ExpressionOptimizerFunc specific to diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp index 18f850ac1e3..d73a611963b 100644 --- a/src/mongo/db/matcher/expression_leaf.cpp +++ b/src/mongo/db/matcher/expression_leaf.cpp @@ -54,8 +54,10 @@ ComparisonMatchExpressionBase::ComparisonMatchExpressionBase( StringData path, const BSONElement& rhs, ElementPath::LeafArrayBehavior leafArrBehavior, - ElementPath::NonLeafArrayBehavior nonLeafArrBehavior) - : LeafMatchExpression(type, path, leafArrBehavior, nonLeafArrBehavior), _rhs(rhs) { + ElementPath::NonLeafArrayBehavior nonLeafArrBehavior, + clonable_ptr<ErrorAnnotation> annotation) + : LeafMatchExpression(type, path, leafArrBehavior, nonLeafArrBehavior, std::move(annotation)), + _rhs(rhs) { invariant(_rhs); } @@ -93,12 +95,14 @@ BSONObj ComparisonMatchExpressionBase::getSerializedRightHandSide() const { ComparisonMatchExpression::ComparisonMatchExpression(MatchType type, StringData path, - const BSONElement& rhs) + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation) : ComparisonMatchExpressionBase(type, path, rhs, ElementPath::LeafArrayBehavior::kTraverse, - ElementPath::NonLeafArrayBehavior::kTraverse) { + ElementPath::NonLeafArrayBehavior::kTraverse, + std::move(annotation)) { uassert( ErrorCodes::BadValue, "cannot compare to undefined", _rhs.type() != BSONType::Undefined); @@ -368,12 +372,12 @@ bool ExistsMatchExpression::equivalent(const MatchExpression* other) const { // ---- -InMatchExpression::InMatchExpression(StringData path) - : LeafMatchExpression(MATCH_IN, path), +InMatchExpression::InMatchExpression(StringData path, clonable_ptr<ErrorAnnotation> annotation) + : LeafMatchExpression(MATCH_IN, path, std::move(annotation)), _eltCmp(BSONElementComparator::FieldNamesMode::kIgnore, _collator) {} std::unique_ptr<MatchExpression> InMatchExpression::shallowClone() const { - auto next = std::make_unique<InMatchExpression>(path()); + auto next = std::make_unique<InMatchExpression>(path(), _errorAnnotation); next->setCollator(_collator); if (getTag()) { next->setTag(getTag()->clone()); diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h index 1cb2b77e545..8f2ac98ffa5 100644 --- a/src/mongo/db/matcher/expression_leaf.h +++ b/src/mongo/db/matcher/expression_leaf.h @@ -50,17 +50,22 @@ class CollatorInterface; class LeafMatchExpression : public PathMatchExpression { public: - LeafMatchExpression(MatchType matchType, StringData path) + LeafMatchExpression(MatchType matchType, + StringData path, + clonable_ptr<ErrorAnnotation> annotation = nullptr) : LeafMatchExpression(matchType, path, ElementPath::LeafArrayBehavior::kTraverse, - ElementPath::NonLeafArrayBehavior::kTraverse) {} + ElementPath::NonLeafArrayBehavior::kTraverse, + std::move(annotation)) {} LeafMatchExpression(MatchType matchType, StringData path, ElementPath::LeafArrayBehavior leafArrBehavior, - ElementPath::NonLeafArrayBehavior nonLeafArrBehavior) - : PathMatchExpression(matchType, path, leafArrBehavior, nonLeafArrBehavior) {} + ElementPath::NonLeafArrayBehavior nonLeafArrBehavior, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : PathMatchExpression( + matchType, path, leafArrBehavior, nonLeafArrBehavior, std::move(annotation)) {} virtual ~LeafMatchExpression() = default; @@ -102,7 +107,8 @@ public: StringData path, const BSONElement& rhs, ElementPath::LeafArrayBehavior, - ElementPath::NonLeafArrayBehavior); + ElementPath::NonLeafArrayBehavior, + clonable_ptr<ErrorAnnotation> annotation = nullptr); virtual ~ComparisonMatchExpressionBase() = default; @@ -173,7 +179,10 @@ public: } } - ComparisonMatchExpression(MatchType type, StringData path, const BSONElement& rhs); + ComparisonMatchExpression(MatchType type, + StringData path, + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation = nullptr); virtual ~ComparisonMatchExpression() = default; @@ -184,8 +193,10 @@ class EqualityMatchExpression final : public ComparisonMatchExpression { public: static constexpr StringData kName = "$eq"_sd; - EqualityMatchExpression(StringData path, const BSONElement& rhs) - : ComparisonMatchExpression(EQ, path, rhs) {} + EqualityMatchExpression(StringData path, + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : ComparisonMatchExpression(EQ, path, rhs, std::move(annotation)) {} StringData name() const final { return kName; @@ -193,7 +204,7 @@ public: virtual std::unique_ptr<MatchExpression> shallowClone() const { std::unique_ptr<ComparisonMatchExpression> e = - std::make_unique<EqualityMatchExpression>(path(), _rhs); + std::make_unique<EqualityMatchExpression>(path(), _rhs, _errorAnnotation); if (getTag()) { e->setTag(getTag()->clone()); } @@ -214,8 +225,10 @@ class LTEMatchExpression final : public ComparisonMatchExpression { public: static constexpr StringData kName = "$lte"_sd; - LTEMatchExpression(StringData path, const BSONElement& rhs) - : ComparisonMatchExpression(LTE, path, rhs) {} + LTEMatchExpression(StringData path, + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : ComparisonMatchExpression(LTE, path, rhs, std::move(annotation)) {} StringData name() const final { return kName; @@ -223,7 +236,7 @@ public: virtual std::unique_ptr<MatchExpression> shallowClone() const { std::unique_ptr<ComparisonMatchExpression> e = - std::make_unique<LTEMatchExpression>(path(), _rhs); + std::make_unique<LTEMatchExpression>(path(), _rhs, _errorAnnotation); if (getTag()) { e->setTag(getTag()->clone()); } @@ -244,8 +257,10 @@ class LTMatchExpression final : public ComparisonMatchExpression { public: static constexpr StringData kName = "$lt"_sd; - LTMatchExpression(StringData path, const BSONElement& rhs) - : ComparisonMatchExpression(LT, path, rhs) {} + LTMatchExpression(StringData path, + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : ComparisonMatchExpression(LT, path, rhs, std::move(annotation)) {} StringData name() const final { return kName; @@ -253,7 +268,7 @@ public: virtual std::unique_ptr<MatchExpression> shallowClone() const { std::unique_ptr<ComparisonMatchExpression> e = - std::make_unique<LTMatchExpression>(path(), _rhs); + std::make_unique<LTMatchExpression>(path(), _rhs, _errorAnnotation); if (getTag()) { e->setTag(getTag()->clone()); } @@ -274,8 +289,10 @@ class GTMatchExpression final : public ComparisonMatchExpression { public: static constexpr StringData kName = "$gt"_sd; - GTMatchExpression(StringData path, const BSONElement& rhs) - : ComparisonMatchExpression(GT, path, rhs) {} + GTMatchExpression(StringData path, + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : ComparisonMatchExpression(GT, path, rhs, std::move(annotation)) {} StringData name() const final { return kName; @@ -283,7 +300,7 @@ public: virtual std::unique_ptr<MatchExpression> shallowClone() const { std::unique_ptr<ComparisonMatchExpression> e = - std::make_unique<GTMatchExpression>(path(), _rhs); + std::make_unique<GTMatchExpression>(path(), _rhs, _errorAnnotation); if (getTag()) { e->setTag(getTag()->clone()); } @@ -304,8 +321,10 @@ class GTEMatchExpression final : public ComparisonMatchExpression { public: static constexpr StringData kName = "$gte"_sd; - GTEMatchExpression(StringData path, const BSONElement& rhs) - : ComparisonMatchExpression(GTE, path, rhs) {} + GTEMatchExpression(StringData path, + const BSONElement& rhs, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : ComparisonMatchExpression(GTE, path, rhs, std::move(annotation)) {} StringData name() const final { return kName; @@ -313,7 +332,7 @@ public: virtual std::unique_ptr<MatchExpression> shallowClone() const { std::unique_ptr<ComparisonMatchExpression> e = - std::make_unique<GTEMatchExpression>(path(), _rhs); + std::make_unique<GTEMatchExpression>(path(), _rhs, _errorAnnotation); if (getTag()) { e->setTag(getTag()->clone()); } @@ -474,7 +493,7 @@ private: */ class InMatchExpression : public LeafMatchExpression { public: - explicit InMatchExpression(StringData path); + explicit InMatchExpression(StringData path, clonable_ptr<ErrorAnnotation> annotation = nullptr); virtual std::unique_ptr<MatchExpression> shallowClone() const; diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index a9d64868e56..ab5df8b97ae 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -89,10 +89,38 @@ 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; + } +} + } // namespace namespace mongo { +using ErrorAnnotation = MatchExpression::ErrorAnnotation; +using AnnotationMode = ErrorAnnotation::Mode; + constexpr StringData AlwaysFalseMatchExpression::kName; constexpr StringData AlwaysTrueMatchExpression::kName; constexpr StringData OrMatchExpression::kName; @@ -299,12 +327,13 @@ StatusWithMatchExpression parse(const BSONObj& obj, continue; } - auto eq = - parseComparison(e.fieldNameStringData(), - std::make_unique<EqualityMatchExpression>(e.fieldNameStringData(), e), - e, - expCtx, - allowedFeatures); + auto eq = parseComparison( + e.fieldNameStringData(), + std::make_unique<EqualityMatchExpression>( + e.fieldNameStringData(), e, createAnnotation(expCtx, "$eq", e.wrap())), + e, + expCtx, + allowedFeatures); if (!eq.isOK()) return eq; @@ -1390,8 +1419,16 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, invariant(e); if ("$eq"_sd == e.fieldNameStringData()) { - return parseComparison( - name, std::make_unique<EqualityMatchExpression>(name, e), e, expCtx, allowedFeatures); + return parseComparison(name, + std::make_unique<EqualityMatchExpression>( + name, + e, + createAnnotation(expCtx, + e.fieldNameStringData().toString(), + BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); } if ("$not"_sd == e.fieldNameStringData()) { @@ -1411,34 +1448,77 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, switch (*parseExpMatchType) { case PathAcceptingKeyword::LESS_THAN: - return parseComparison( - name, std::make_unique<LTMatchExpression>(name, e), e, expCtx, allowedFeatures); + return parseComparison(name, + std::make_unique<LTMatchExpression>( + name, + e, + 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), e, expCtx, allowedFeatures); + return parseComparison(name, + std::make_unique<LTEMatchExpression>( + name, + e, + 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), e, expCtx, allowedFeatures); + return parseComparison(name, + std::make_unique<GTMatchExpression>( + name, + e, + 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), e, expCtx, allowedFeatures); + return parseComparison(name, + std::make_unique<GTEMatchExpression>( + name, + e, + 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 // that $ne of a regex is allowed. See SERVER-1705. return {Status(ErrorCodes::BadValue, "Can't have regex as arg to $ne.")}; } - StatusWithMatchExpression s = - parseComparison(name, - std::make_unique<EqualityMatchExpression>(name, e), - e, - expCtx, - allowedFeatures); - return {std::make_unique<NotMatchExpression>(s.getValue().release())}; + StatusWithMatchExpression s = parseComparison( + name, + std::make_unique<EqualityMatchExpression>( + name, + e, + createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))), + e, + expCtx, + allowedFeatures); + return {std::make_unique<NotMatchExpression>( + s.getValue().release(), + createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; } case PathAcceptingKeyword::EQUALITY: return parseComparison(name, - std::make_unique<EqualityMatchExpression>(name, e), + std::make_unique<EqualityMatchExpression>( + name, + e, + createAnnotation(expCtx, + e.fieldNameStringData().toString(), + BSON(name << e.wrap()))), e, expCtx, allowedFeatures); @@ -1447,7 +1527,10 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, if (e.type() != BSONType::Array) { return {Status(ErrorCodes::BadValue, "$in needs an array")}; } - auto temp = std::make_unique<InMatchExpression>(name); + auto temp = std::make_unique<InMatchExpression>( + name, + createAnnotation( + expCtx, e.fieldNameStringData().toString(), BSON(name << e.wrap()))); auto parseStatus = parseInExpression(temp.get(), e.Obj(), expCtx); if (!parseStatus.isOK()) { return parseStatus; @@ -1459,12 +1542,16 @@ StatusWithMatchExpression parseSubField(const BSONObj& context, if (e.type() != Array) { return {Status(ErrorCodes::BadValue, "$nin needs an array")}; } - auto temp = std::make_unique<InMatchExpression>(name); + auto temp = std::make_unique<InMatchExpression>( + name, + 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())}; + return {std::make_unique<NotMatchExpression>( + temp.release(), createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend))}; } case PathAcceptingKeyword::SIZE: { diff --git a/src/mongo/db/matcher/expression_path.h b/src/mongo/db/matcher/expression_path.h index a7f8941b928..1ae09636e8d 100644 --- a/src/mongo/db/matcher/expression_path.h +++ b/src/mongo/db/matcher/expression_path.h @@ -45,8 +45,9 @@ public: PathMatchExpression(MatchType matchType, StringData path, ElementPath::LeafArrayBehavior leafArrBehavior, - ElementPath::NonLeafArrayBehavior nonLeafArrayBehavior) - : MatchExpression(matchType), _path(path) { + ElementPath::NonLeafArrayBehavior nonLeafArrayBehavior, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : MatchExpression(matchType, std::move(annotation)), _path(path) { _elementPath.init(_path); _elementPath.setLeafArrayBehavior(leafArrBehavior); _elementPath.setNonLeafArrayBehavior(nonLeafArrayBehavior); diff --git a/src/mongo/db/matcher/expression_tree.h b/src/mongo/db/matcher/expression_tree.h index d6d1fec2159..4da0af5d8fc 100644 --- a/src/mongo/db/matcher/expression_tree.h +++ b/src/mongo/db/matcher/expression_tree.h @@ -215,11 +215,13 @@ public: class NotMatchExpression final : public MatchExpression { public: - explicit NotMatchExpression(MatchExpression* e) : MatchExpression(NOT), _exp(e) {} + explicit NotMatchExpression(MatchExpression* e, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : MatchExpression(NOT, std::move(annotation)), _exp(e) {} virtual std::unique_ptr<MatchExpression> shallowClone() const { std::unique_ptr<NotMatchExpression> self = - std::make_unique<NotMatchExpression>(_exp->shallowClone().release()); + std::make_unique<NotMatchExpression>(_exp->shallowClone().release(), _errorAnnotation); if (getTag()) { self->setTag(getTag()->clone()); } |