summaryrefslogtreecommitdiff
path: root/src/mongo/db/matcher/doc_validation_error.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/matcher/doc_validation_error.cpp')
-rw-r--r--src/mongo/db/matcher/doc_validation_error.cpp376
1 files changed, 282 insertions, 94 deletions
diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp
index 0175172f3b5..a54ef810064 100644
--- a/src/mongo/db/matcher/doc_validation_error.cpp
+++ b/src/mongo/db/matcher/doc_validation_error.cpp
@@ -49,6 +49,7 @@
#include "mongo/db/matcher/schema/expression_internal_schema_min_length.h"
#include "mongo/db/matcher/schema/expression_internal_schema_object_match.h"
#include "mongo/db/matcher/schema/expression_internal_schema_str_length.h"
+#include "mongo/db/matcher/schema/expression_internal_schema_xor.h"
namespace mongo::doc_validation_error {
namespace {
@@ -84,10 +85,13 @@ struct ValidationErrorFrame {
// nodes when generating an error. For instance, when generating an error for an AND in a
// normal context, we need to discern which of its clauses failed.
kErrorNeedChildrenInfo,
+ // This node contributes to error generation, but none of its children will contribute to
+ // the error output.
+ kErrorIgnoreChildren,
};
- ValidationErrorFrame(RuntimeState runtimeState, BSONObj currentDoc)
- : runtimeState(runtimeState), currentDoc(std::move(currentDoc)) {}
+ ValidationErrorFrame(RuntimeState runtimeState, BSONObj currentDoc, InvertError inversion)
+ : runtimeState(runtimeState), currentDoc(std::move(currentDoc)), inversion(inversion) {}
// BSONBuilders which construct the generated error.
BSONObjBuilder objBuilder;
@@ -98,6 +102,8 @@ struct ValidationErrorFrame {
RuntimeState runtimeState;
// Tracks the current subdocument that an error should be generated over.
BSONObj currentDoc;
+ // Tracks whether the generated error should be described normally or in an inverted context.
+ InvertError inversion;
};
using RuntimeState = ValidationErrorFrame::RuntimeState;
@@ -113,35 +119,39 @@ struct ValidationErrorContext {
*/
void pushNewFrame(const MatchExpression& expr, const BSONObj& subDoc) {
// Clear the last error that was generated.
- latestCompleteError = BSONObj();
+ latestCompleteError = std::monostate();
// If this is the first frame, then we know that we've failed validation, so we must be
// generating an error.
if (frames.empty()) {
- frames.emplace(RuntimeState::kError, subDoc);
+ frames.emplace(RuntimeState::kError, subDoc, InvertError::kNormal);
return;
}
auto parentRuntimeState = getCurrentRuntimeState();
+ auto inversion = getCurrentInversion();
// 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 ||
+ parentRuntimeState == RuntimeState::kErrorIgnoreChildren ||
expr.getErrorAnnotation()->mode == AnnotationMode::kIgnore) {
- frames.emplace(RuntimeState::kNoError, subDoc);
+ frames.emplace(RuntimeState::kNoError, subDoc, inversion);
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.matchesBSON(subDoc) ? inversion == InvertError::kInverted
: inversion == InvertError::kNormal;
frames.emplace(generateErrorValue ? RuntimeState::kError : RuntimeState::kNoError,
- subDoc);
+ subDoc,
+ inversion);
return;
}
- frames.emplace(RuntimeState::kError, subDoc);
+ frames.emplace(RuntimeState::kError, subDoc, inversion);
}
void popFrame() {
invariant(!frames.empty());
@@ -186,8 +196,43 @@ struct ValidationErrorContext {
}
return rootDoc;
}
- BSONObj getLatestCompleteError() const {
- return latestCompleteError;
+ InvertError getCurrentInversion() const {
+ invariant(!frames.empty());
+ return frames.top().inversion;
+ }
+ void setCurrentInversion(InvertError inversion) {
+ invariant(!frames.empty());
+ frames.top().inversion = inversion;
+ }
+
+ bool haveLatestCompleteError() {
+ return !std::holds_alternative<std::monostate>(latestCompleteError);
+ }
+
+ /**
+ * Appends the latest complete error to 'builder'.
+ */
+ void appendLatestCompleteError(BSONObjBuilder* builder) {
+ std::visit(
+ visit_helper::Overloaded{
+ [&](const auto& details) -> void { builder->append("details", details); },
+ [&](const std::monostate& arr) -> void { MONGO_UNREACHABLE }},
+ latestCompleteError);
+ }
+
+ /**
+ * Returns the latest complete error generated as an object. Should only be called when the
+ * caller expects an object.
+ */
+ BSONObj getLatestCompleteErrorObject() const {
+ return std::get<BSONObj>(latestCompleteError);
+ }
+
+ /**
+ * Returns whether 'expr' will produce an array as an error.
+ */
+ bool producesArray(const MatchExpression& expr) {
+ return expr.getErrorAnnotation()->operatorName == "_internalSubschema";
}
/**
@@ -196,7 +241,11 @@ struct ValidationErrorContext {
*/
void finishCurrentError(const MatchExpression* expr) {
if (shouldGenerateError(*expr)) {
- latestCompleteError = getCurrentObjBuilder().obj();
+ if (producesArray(*expr)) {
+ latestCompleteError = getCurrentArrayBuilder().arr();
+ } else {
+ latestCompleteError = getCurrentObjBuilder().obj();
+ }
}
popFrame();
}
@@ -205,8 +254,8 @@ struct ValidationErrorContext {
* Sets 'inversion' to the opposite of its current value.
*/
void flipInversion() {
- inversion =
- inversion == InvertError::kNormal ? InvertError::kInverted : InvertError::kNormal;
+ getCurrentInversion() == InvertError::kNormal ? setCurrentInversion(InvertError::kInverted)
+ : setCurrentInversion(InvertError::kNormal);
}
/**
@@ -221,12 +270,18 @@ struct ValidationErrorContext {
// to generate an error for one node. As such, each node must call 'pushNewFrame' as part of
// its pre-visit and 'popFrame' as part of its post-visit.
std::stack<ValidationErrorFrame> frames;
- // Tracks the most recently completed error. The final error will be stored here.
- BSONObj latestCompleteError;
+ // Tracks the most recently completed error. The error can be one of three types:
+ // - std::monostate indicates that no error was produced.
+ // - BSONArray indicates multiple errors produced by an expression which does not correspond
+ // to a user-facing operator. For example, consider the subschema {minimum: 2, multipleOf: 2}.
+ // Both schema operators can fail and produce errors, but the schema that they belong to
+ // doesn't correspond to an operator that the user specified. As such, the errors are stored
+ // in an array and passed to the parent expression.
+ // - Finally, BSONObj indicates the most common case of an error: a detailed object which
+ // describes the reasons for failure. The final error will be of this type.
+ std::variant<std::monostate, BSONObj, BSONArray> latestCompleteError = std::monostate();
// Document which failed to match against the collection's validator.
const BSONObj& rootDoc;
- // Tracks whether the generated error should be described normally or in an inverted context.
- InvertError inversion = InvertError::kNormal;
};
/**
@@ -235,19 +290,23 @@ struct ValidationErrorContext {
*/
void finishLogicalOperatorChildError(const ListOfMatchExpression* expr,
ValidationErrorContext* ctx) {
- BSONObj childError = ctx->latestCompleteError;
- if (!childError.isEmpty() && ctx->shouldGenerateError(*expr)) {
+ if (ctx->shouldGenerateError(*expr) &&
+ ctx->getCurrentRuntimeState() != RuntimeState::kErrorIgnoreChildren) {
auto operatorName = expr->getErrorAnnotation()->operatorName;
-
- // Only provide the indexes of non-matching clauses for explicit $and/$or/$nor in the
+ // Only provide the indexes of non-matching clauses for certain named operators 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);
+ static const stdx::unordered_set<std::string> operatorsWithOrderedClauses = {
+ "$and", "$or", "$nor", "allOf", "anyOf", "oneOf"};
+ if (ctx->haveLatestCompleteError()) {
+ if (operatorsWithOrderedClauses.find(operatorName) !=
+ operatorsWithOrderedClauses.end()) {
+ BSONObjBuilder subBuilder = ctx->getCurrentArrayBuilder().subobjStart();
+ subBuilder.appendNumber("index", ctx->getCurrentChildIndex());
+ ctx->appendLatestCompleteError(&subBuilder);
+ subBuilder.done();
+ } else {
+ ctx->getCurrentArrayBuilder().append(ctx->getLatestCompleteErrorObject());
+ }
}
}
ctx->incrementCurrentChildIndex();
@@ -267,15 +326,26 @@ public:
}
void visit(const AndMatchExpression* expr) final {
// $all is treated as a leaf operator.
- if (expr->getErrorAnnotation()->operatorName == "$all") {
- processAll(*expr);
+ auto operatorName = expr->getErrorAnnotation()->operatorName;
+ if (operatorName == "$all") {
+ static constexpr auto kNormalReason = "array did not contain all specified values";
+ static constexpr auto kInvertedReason = "array did contain all specified values";
+ generateLogicalLeafError(*expr, kNormalReason, kInvertedReason);
} else {
preVisitTreeOperator(expr);
// An AND needs its children to call 'matches' in a normal context to discern which
// clauses failed.
- if (_context->inversion == InvertError::kNormal) {
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
_context->setCurrentRuntimeState(RuntimeState::kErrorNeedChildrenInfo);
}
+ // If this is the root of a $jsonSchema and we're in an inverted context, do not attempt
+ // to provide a detailed error.
+ if (operatorName == "$jsonSchema" &&
+ _context->getCurrentInversion() == InvertError::kInverted) {
+ _context->setCurrentRuntimeState(RuntimeState::kErrorIgnoreChildren);
+ static constexpr auto kInvertedReason = "schema matched";
+ appendErrorReason("", kInvertedReason);
+ }
}
}
void visit(const BitsAllClearMatchExpression* expr) final {
@@ -300,25 +370,26 @@ public:
generateComparisonError(expr);
}
void visit(const ExistsMatchExpression* expr) final {
- static constexpr auto normalReason = "path does not exist";
- static constexpr auto invertedReason = "path does exist";
+ static constexpr auto kNormalReason = "path does not exist";
+ static constexpr auto kInvertedReason = "path does exist";
_context->pushNewFrame(*expr, _context->getCurrentDocument());
if (_context->shouldGenerateError(*expr)) {
appendErrorDetails(*expr);
- appendErrorReason(*expr, normalReason, invertedReason);
+ appendErrorReason(kNormalReason, kInvertedReason);
}
}
void visit(const ExprMatchExpression* expr) final {
- static constexpr auto normalReason = "$expr did not match";
- static constexpr auto invertedReason = "$expr did match";
+ static constexpr auto kNormalReason = "$expr did not match";
+ static constexpr auto kInvertedReason = "$expr did match";
_context->pushNewFrame(*expr, _context->getCurrentDocument());
if (_context->shouldGenerateError(*expr)) {
appendErrorDetails(*expr);
- appendErrorReason(*expr, normalReason, invertedReason);
+ appendErrorReason(kNormalReason, kInvertedReason);
BSONObjBuilder& bob = _context->getCurrentObjBuilder();
// Append the result of $expr's aggregate expression. The result of the
// aggregate expression can be determined from the current inversion.
- bob.append("expressionResult", _context->inversion == InvertError::kInverted);
+ bob.append("expressionResult",
+ _context->getCurrentInversion() == InvertError::kInverted);
}
}
void visit(const GTEMatchExpression* expr) final {
@@ -362,6 +433,8 @@ public:
void visit(const InternalSchemaAllowedPropertiesMatchExpression* expr) final {}
void visit(const InternalSchemaBinDataEncryptedTypeExpression* expr) final {
static constexpr auto kNormalReason = "encrypted value has wrong type";
+ // This node will never generate an error in the inverted case.
+ static constexpr auto kInvertedReason = "";
_context->pushNewFrame(*expr, _context->getCurrentDocument());
if (_context->shouldGenerateError(*expr)) {
ElementPath path(expr->path(), LeafArrayBehavior::kNoTraversal);
@@ -373,10 +446,10 @@ public:
// encrypted, in the inverted case, this node's sibling expression will generate an
// appropriate error.
if (elem.type() == BSONType::BinData && elem.binDataType() == BinDataType::Encrypt &&
- _context->inversion == InvertError::kNormal) {
+ _context->getCurrentInversion() == InvertError::kNormal) {
auto& builder = _context->getCurrentObjBuilder();
appendOperatorName(*expr->getErrorAnnotation(), &builder);
- builder.append("reason", kNormalReason);
+ appendErrorReason(kNormalReason, kInvertedReason);
} else {
_context->setCurrentRuntimeState(RuntimeState::kNoError);
}
@@ -389,7 +462,7 @@ public:
if (_context->shouldGenerateError(*expr)) {
auto& builder = _context->getCurrentObjBuilder();
appendOperatorName(*expr->getErrorAnnotation(), &builder);
- appendErrorReason(*expr, kNormalReason, kInvertedReason);
+ appendErrorReason(kNormalReason, kInvertedReason);
}
}
void visit(const InternalSchemaCondMatchExpression* expr) final {}
@@ -456,7 +529,34 @@ public:
generateTypeError(expr, LeafArrayBehavior::kNoTraversal);
}
void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final {}
- void visit(const InternalSchemaXorMatchExpression* expr) final {}
+ void visit(const InternalSchemaXorMatchExpression* expr) final {
+ preVisitTreeOperator(expr);
+ _context->setCurrentRuntimeState(RuntimeState::kErrorNeedChildrenInfo);
+ if (_context->shouldGenerateError(*expr)) {
+ auto currentDoc = _context->getCurrentDocument();
+
+ // If 'oneOf' has more than one matching subschema, then the generated error should
+ // be in terms of the subschemas which matched, not the ones which failed to match.
+ std::vector<int> matchingClauses;
+ for (size_t childIndex = 0; childIndex < expr->numChildren(); ++childIndex) {
+ auto child = expr->getChild(childIndex);
+ if (child->matchesBSON(currentDoc)) {
+ matchingClauses.push_back(childIndex);
+ }
+ }
+ if (!matchingClauses.empty()) {
+ _context->flipInversion();
+ _context->setCurrentRuntimeState(RuntimeState::kErrorIgnoreChildren);
+ auto& builder = _context->getCurrentObjBuilder();
+ // We only report the matching schema reason in an inverted context, so there is
+ // no need for a reason string in the normal case.
+ static constexpr auto kNormalReason = "";
+ static constexpr auto kInvertedReason = "more than one subschema matched";
+ appendErrorReason(kNormalReason, kInvertedReason);
+ builder.append("matchingSchemaIndexes", matchingClauses);
+ }
+ }
+ }
void visit(const LTEMatchExpression* expr) final {
generateComparisonError(expr);
}
@@ -476,7 +576,7 @@ public:
preVisitTreeOperator(expr);
// A NOR needs its children to call 'matches' in a normal context to discern which
// clauses matched.
- if (_context->inversion == InvertError::kNormal) {
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
_context->setCurrentRuntimeState(RuntimeState::kErrorNeedChildrenInfo);
}
_context->flipInversion();
@@ -484,13 +584,28 @@ public:
void visit(const NotMatchExpression* expr) final {
preVisitTreeOperator(expr);
_context->flipInversion();
+ // If this is a $jsonSchema not, then expr's children will not contribute to the error
+ // output.
+ if (_context->shouldGenerateError(*expr) &&
+ expr->getErrorAnnotation()->operatorName == "not") {
+ static constexpr auto kInvertedReason = "child expression matched";
+ appendErrorReason("", kInvertedReason);
+ _context->setCurrentRuntimeState(RuntimeState::kErrorIgnoreChildren);
+ }
}
void visit(const OrMatchExpression* expr) final {
- preVisitTreeOperator(expr);
- // An OR needs its children to call 'matches' in an inverted context to discern which
- // clauses matched.
- if (_context->inversion == InvertError::kInverted) {
- _context->setCurrentRuntimeState(RuntimeState::kErrorNeedChildrenInfo);
+ // The jsonSchema keyword 'enum' is treated as a leaf operator.
+ if (expr->getErrorAnnotation()->operatorName == "enum") {
+ static constexpr auto kNormalReason = "value was not found in enum";
+ static constexpr auto kInvertedReason = "value was found in enum";
+ generateLogicalLeafError(*expr, kNormalReason, kInvertedReason);
+ } else {
+ preVisitTreeOperator(expr);
+ // An OR needs its children to call 'matches' in an inverted context to discern which
+ // clauses matched.
+ if (_context->getCurrentInversion() == InvertError::kInverted) {
+ _context->setCurrentRuntimeState(RuntimeState::kErrorNeedChildrenInfo);
+ }
}
}
void visit(const RegexMatchExpression* expr) final {
@@ -542,6 +657,9 @@ private:
}
BSONArray createValuesArray(const ElementPath& path) {
+ // Empty path means that the match is against the root document.
+ if (path.fieldRef().empty())
+ return BSON_ARRAY(_context->rootDoc);
BSONMatchableDocument doc(_context->getCurrentDocument());
MatchableDocument::IteratorHolder cursor(&doc, &path);
BSONArrayBuilder bab;
@@ -555,6 +673,7 @@ private:
}
return bab.arr();
}
+
/**
* Appends a missing field error if 'arr' is empty.
*/
@@ -593,14 +712,22 @@ private:
bob.append("expectedTypes", types);
}
}
- void appendErrorReason(const MatchExpression& expr,
- const std::string& normalReason,
- const std::string& invertedReason) {
+
+ /**
+ * Given 'normalReason' and 'invertedReason' strings, appends the reason for failure to the
+ * current object builder tracked by 'ctx'.
+ */
+ void appendErrorReason(const std::string& normalReason, const std::string& invertedReason) {
+ if (normalReason.empty()) {
+ invariant(_context->getCurrentInversion() == InvertError::kInverted);
+ } else if (invertedReason.empty()) {
+ invariant(_context->getCurrentInversion() == InvertError::kNormal);
+ }
BSONObjBuilder& bob = _context->getCurrentObjBuilder();
if (bob.hasField("reason")) {
return; // there's already a reason for failure
}
- if (_context->inversion == InvertError::kNormal) {
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
bob.append("reason", normalReason);
} else {
bob.append("reason", invertedReason);
@@ -655,18 +782,18 @@ private:
if (_context->shouldGenerateError(expr)) {
appendErrorDetails(expr);
ElementPath path(expr.path(), leafArrayBehavior);
- BSONArray arr = createValuesArray(path);
+ auto arr = createValuesArray(path);
appendMissingField(arr);
appendTypeMismatch(arr, expectedTypes);
- appendErrorReason(expr, normalReason, invertedReason);
+ appendErrorReason(normalReason, invertedReason);
appendConsideredValues(arr);
}
}
void generateComparisonError(const ComparisonMatchExpression* expr) {
- static constexpr auto normalReason = "comparison failed";
- static constexpr auto invertedReason = "comparison succeeded";
- generatePathError(*expr, normalReason, invertedReason);
+ static constexpr auto kNormalReason = "comparison failed";
+ static constexpr auto kInvertedReason = "comparison succeeded";
+ generatePathError(*expr, kNormalReason, kInvertedReason);
}
void generateElemMatchError(const ArrayMatchingMatchExpression* expr) {
@@ -693,11 +820,12 @@ private:
ElementPath path(expr->path(), behavior);
BSONArray arr = createValuesArray(path);
appendMissingField(arr);
- appendErrorReason(*expr, kNormalReason, kInvertedReason);
+ appendErrorReason(kNormalReason, kInvertedReason);
appendConsideredValues(arr);
appendConsideredTypes(arr);
}
}
+
/**
* Generates a document validation error for a bit test expression 'expr'.
*/
@@ -716,31 +844,35 @@ private:
* Performs the setup necessary to generate an error for 'expr'.
*/
void preVisitTreeOperator(const MatchExpression* expr) {
- invariant(expr->numChildren() > 0);
+ invariant(expr->getCategory() == MatchExpression::MatchCategory::kLogical);
_context->pushNewFrame(*expr, _context->getCurrentDocument());
if (_context->shouldGenerateError(*expr)) {
auto annotation = expr->getErrorAnnotation();
- appendOperatorName(*annotation, &_context->getCurrentObjBuilder());
+ // Only append the operator name if it will produce an object error corresponding to
+ // a user-facing operator.
+ if (!_context->producesArray(*expr))
+ appendOperatorName(*annotation, &_context->getCurrentObjBuilder());
_context->getCurrentObjBuilder().appendElements(annotation->annotation);
}
}
/**
- * Utility to generate an error for $all. Though $all is internally translated to an 'AND'
- * over some child expressions, it is treated as a leaf operator for the purposes of error
- * reporting.
+ * Utility to generate an error for logical operators which are treated like leaves for the
+ * purposes of error reporting.
*/
- void processAll(const AndMatchExpression& expr) {
+ void generateLogicalLeafError(const ListOfMatchExpression& expr,
+ const std::string& normalReason,
+ const std::string& invertedReason) {
_context->pushNewFrame(expr, _context->getCurrentDocument());
if (_context->shouldGenerateError(expr)) {
+ // $all with no children should not translate to an 'AndMatchExpression' and 'enum'
+ // must have non-zero children.
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(), LeafArrayBehavior::kNoTraversal);
auto arr = createValuesArray(path);
appendMissingField(arr);
- appendErrorReason(expr, kNormalReason, kInvertedReason);
+ appendErrorReason(normalReason, invertedReason);
appendConsideredValues(arr);
}
}
@@ -757,14 +889,14 @@ private:
// 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);
+ invariant(_context->getCurrentInversion() == InvertError::kNormal);
} else {
- invariant(_context->inversion == InvertError::kInverted);
+ invariant(_context->getCurrentInversion() == 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);
+ appendErrorReason(kNormalReason, kInvertedReason);
}
}
@@ -825,7 +957,14 @@ public:
void visit(const InternalSchemaRootDocEqMatchExpression* expr) final {}
void visit(const InternalSchemaTypeExpression* expr) final {}
void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final {}
- void visit(const InternalSchemaXorMatchExpression* expr) final {}
+ void visit(const InternalSchemaXorMatchExpression* expr) final {
+ // Only check for child errors when we're in a normal context, that is, when none of expr's
+ // subschemas matched, as opposed to the inverted context, where more than one subschema
+ // matched.
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
+ inVisitTreeOperator(expr);
+ }
+ }
void visit(const LTEMatchExpression* expr) final {}
void visit(const LTMatchExpression* expr) final {}
void visit(const ModMatchExpression* expr) final {}
@@ -874,22 +1013,33 @@ public:
}
void visit(const AndMatchExpression* expr) final {
auto operatorName = expr->getErrorAnnotation()->operatorName;
- // 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)) {
+ auto inversion = _context->getCurrentInversion();
+ // Clean up the frame for this node if we're finishing the error for an $all, an inverted
+ // $jsonSchema, or this node shouldn't generate an error.
+ if (operatorName == "$all" ||
+ (operatorName == "$jsonSchema" && inversion == InvertError::kInverted) ||
+ !_context->shouldGenerateError(*expr)) {
_context->finishCurrentError(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);
+ // Specify a different details string based on the operatorName in expr's annotation where
+ // the first entry is the details string in the normal case and the second is the string
+ // for the inverted case.
+ static const StringMap<std::pair<std::string, std::string>> detailsStringMap = {
+ {"$and", {"clausesNotSatisfied", "clausesSatisfied"}},
+ {"allOf", {"schemasNotSatisfied", ""}},
+ {"properties", {"propertiesNotSatisfied", ""}},
+ {"$jsonSchema", {"schemaRulesNotSatisfied", ""}},
+ {"_internalSubschema", {"", ""}},
+ {"", {"details", ""}}};
+ auto detailsStringPair = detailsStringMap.find(operatorName);
+ invariant(detailsStringPair != detailsStringMap.end());
+ auto&& stringPair = detailsStringPair->second;
+ if (inversion == InvertError::kNormal) {
+ postVisitTreeOperator(expr, stringPair.first);
+ } else {
+ postVisitTreeOperator(expr, stringPair.second);
+ }
}
void visit(const BitsAllClearMatchExpression* expr) final {
_context->finishCurrentError(expr);
@@ -966,7 +1116,15 @@ public:
_context->finishCurrentError(expr);
}
void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final {}
- void visit(const InternalSchemaXorMatchExpression* expr) final {}
+ void visit(const InternalSchemaXorMatchExpression* expr) final {
+ static constexpr auto normalDetailString = "schemasNotSatisfied";
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
+ postVisitTreeOperator(expr, normalDetailString);
+ } else {
+ // In the inverted case, we treat 'oneOf' as a leaf.
+ _context->finishCurrentError(expr);
+ }
+ }
void visit(const LTEMatchExpression* expr) final {
_context->finishCurrentError(expr);
}
@@ -977,20 +1135,45 @@ public:
_context->finishCurrentError(expr);
}
void visit(const NorMatchExpression* expr) final {
- _context->flipInversion();
- static constexpr auto detailsString = "clausesNotSatisfied";
- postVisitTreeOperator(expr, detailsString);
+ static constexpr auto kNormalDetailsString = "clausesNotSatisfied";
+ static constexpr auto kInvertedDetailsString = "clausesSatisfied";
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
+ postVisitTreeOperator(expr, kNormalDetailsString);
+ } else {
+ postVisitTreeOperator(expr, kInvertedDetailsString);
+ }
}
void visit(const NotMatchExpression* expr) final {
- _context->flipInversion();
- if (_context->shouldGenerateError(*expr)) {
- _context->getCurrentObjBuilder().append("details", _context->getLatestCompleteError());
+ // In the case of a $jsonSchema "not", we do not report any error details
+ // explaining why the subschema did match.
+ if (_context->shouldGenerateError(*expr) &&
+ expr->getErrorAnnotation()->operatorName != "not") {
+ _context->appendLatestCompleteError(&_context->getCurrentObjBuilder());
}
_context->finishCurrentError(expr);
}
void visit(const OrMatchExpression* expr) final {
- static constexpr auto detailsString = "clausesNotSatisfied";
- postVisitTreeOperator(expr, detailsString);
+ auto operatorName = expr->getErrorAnnotation()->operatorName;
+ // Clean up the frame for this node if we're finishing the error for an 'enum' or this node
+ // shouldn't generate an error.
+ if (operatorName == "enum" || !_context->shouldGenerateError(*expr)) {
+ _context->finishCurrentError(expr);
+ return;
+ }
+ // Specify a different details string based on the operatorName in expr's annotation where
+ // the first entry is the details string in the normal case and the second is the string
+ // for the inverted case.
+ static const StringMap<std::pair<std::string, std::string>> detailsStringMap = {
+ {"$or", {"clausesNotSatisfied", "clausesSatisfied"}},
+ {"anyOf", {"schemasNotSatisfied", ""}}};
+ auto detailsStringPair = detailsStringMap.find(operatorName);
+ invariant(detailsStringPair != detailsStringMap.end());
+ auto stringPair = detailsStringPair->second;
+ if (_context->getCurrentInversion() == InvertError::kNormal) {
+ postVisitTreeOperator(expr, stringPair.first);
+ } else {
+ postVisitTreeOperator(expr, stringPair.second);
+ }
}
void visit(const RegexMatchExpression* expr) final {
_context->finishCurrentError(expr);
@@ -1019,7 +1202,10 @@ private:
void postVisitTreeOperator(const ListOfMatchExpression* expr,
const std::string& detailsString) {
finishLogicalOperatorChildError(expr, _context);
- if (_context->shouldGenerateError(*expr)) {
+ // Append the result of the current array builder to the current object builder under the
+ // field name 'detailsString' unless this node produces an array (i.e. in the case of a
+ // subschema).
+ if (_context->shouldGenerateError(*expr) && !_context->producesArray(*expr)) {
auto failedClauses = _context->getCurrentArrayBuilder().arr();
_context->getCurrentObjBuilder().append(detailsString, failedClauses);
}
@@ -1090,7 +1276,9 @@ BSONObj generateError(const MatchExpression& validatorExpr, const BSONObj& doc)
objBuilder.appendAs(objectIdElement, "failingDocumentId"_sd);
// Add errors from match expressions.
- objBuilder.append("details"_sd, context.getLatestCompleteError());
+ auto error = context.getLatestCompleteErrorObject();
+ invariant(!error.isEmpty());
+ objBuilder.append("details"_sd, std::move(error));
return objBuilder.obj();
}