summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMihai Andrei <mihai.andrei@10gen.com>2020-08-07 08:17:59 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-09-23 21:31:18 +0000
commit10b0f02da33d62c992671e86ce882459f5a3184c (patch)
treef355e586a8e65f97ef9e3b8351de0ce040e64fbc /src
parent64746898ab41516760bf21a7ba7f6372fc0ec094 (diff)
downloadmongo-10b0f02da33d62c992671e86ce882459f5a3184c.tar.gz
SERVER-49444 Implement validation error generation for jsonSchema logical keywords
Diffstat (limited to 'src')
-rw-r--r--src/mongo/db/matcher/doc_validation_error.cpp376
-rw-r--r--src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp649
-rw-r--r--src/mongo/db/matcher/doc_validation_error_test.cpp12
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp9
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_eq.h4
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp3
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h6
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp42
8 files changed, 941 insertions, 160 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();
}
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
index 9af4f7f0a60..147cd3cf8f5 100644
--- a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp
+++ b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp
@@ -69,17 +69,47 @@ TEST(JSONSchemaValidation, ExclusiveMinimum) {
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaValidation, ExclusiveMinimumInverted) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'not': {'properties': {'a': {'minimum': 1, 'exclusiveMinimum': "
+ "true}}}}}}");
+ BSONObj document = fromjson("{a: 2}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema',"
+ " 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, ExclusiveMinimumInvertedTypeMismatch) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'not': {'properties': {'a': {'minimum': 1, 'exclusiveMinimum': "
+ "true}}}}}}");
+ BSONObj document = fromjson("{a: 'foo'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, ExclusiveMinimumInvertedMissingField) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'not': {'properties': {'a': {'minimum': 1, 'exclusiveMinimum': "
+ "true}}}}}}");
+ BSONObj document = fromjson("{b: 100}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ 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'}]}}]}");
+ " 'clausesSatisfied': [{'index': 0, 'details': "
+ " {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -100,17 +130,25 @@ TEST(JSONSchemaValidation, ExclusiveMaximum) {
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaValidation, ExclusiveMaximumInverted) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'not': {'properties': {'a': {'maximum': 1, 'exclusiveMaximum': "
+ "true}}}}}}");
+ BSONObj document = fromjson("{a: 0}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema',"
+ " 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ 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'}]}}]}");
+ " 'clausesSatisfied': [{'index': 0, 'details': "
+ " {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -218,7 +256,7 @@ TEST(JSONSchemaValidation, TypeRestrictionContradictsSpecifiedType) {
TEST(JSONSchemaValidation, MultipleNestedProperties) {
BSONObj query = fromjson(
- "{'$jsonSchema': { "
+ "{'$jsonSchema': {"
" 'properties': {"
" 'a': {'properties': {'b': {'minimum': 1}, 'c': {'minimum': 10}}},"
" 'd': {'properties': {'e': {'minimum': 50}, 'f': {'minimum': 100}}}}}}}}}");
@@ -297,7 +335,7 @@ TEST(JSONSchemaValidation, JSONSchemaAndQueryOperators) {
TEST(JSONSchemaValidation, NoTopLevelObjectTypeRejectsAllDocuments) {
BSONObj query = fromjson(
- " {'$jsonSchema':{ 'type': 'number',"
+ " {'$jsonSchema': {'type': 'number',"
" 'properties': {"
" 'a': {'properties': "
" {'b': {'minimum': 1}}}}}}}}");
@@ -440,7 +478,6 @@ TEST(JSONSchemaValidation, BSONTypeNoImplicitArrayTraversal) {
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
-
// Scalar keywords
// minLength
@@ -528,12 +565,35 @@ TEST(JSONSchemaValidation, MinLengthAtTopLevelHasNoEffect) {
BSONObj document = fromjson("{a: 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$nor',"
- " 'clausesNotSatisfied': [{'index': 0, 'details': "
- " {'operatorName': '$jsonSchema',"
- " 'schemaRulesNotSatisfied': ["
- " {'operatorName': 'minLength', "
- " 'specifiedAs': {'minLength': 1}, "
- " 'reason': 'expression always evaluates to true'}]}}]}");
+ " 'clausesSatisfied': [{'index': 0, 'details': "
+ " {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MinLengthInvertedLengthDoesMatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'minLength': 4}}}}}");
+ BSONObj document = fromjson("{'a': 'this string is long'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MinLengthInvertedTypeMismatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'minLength': 4}}}}}");
+ BSONObj document = fromjson("{'a': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MinLengthInvertedMissingProperty) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'minLength': 4}}}}}");
+ BSONObj document = fromjson("{'b': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -622,12 +682,35 @@ TEST(JSONSchemaValidation, MaxLengthAtTopLevelHasNoEffect) {
BSONObj document = fromjson("{a: 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$nor',"
- " 'clausesNotSatisfied': [{'index': 0, 'details': "
- " {'operatorName': '$jsonSchema',"
- " 'schemaRulesNotSatisfied': ["
- " {'operatorName': 'maxLength', "
- " 'specifiedAs': {'maxLength': 1000}, "
- " 'reason': 'expression always evaluates to true'}]}}]}");
+ " 'clausesSatisfied': [{'index': 0, 'details': "
+ " {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MaxLengthInvertedLengthDoesMatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'maxLength': 30}}}}}");
+ BSONObj document = fromjson("{'a': 'this string is long'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MaxLengthInvertedTypeMismatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'maxLength': 4}}}}}");
+ BSONObj document = fromjson("{'a': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MaxLengthInvertedMissingProperty) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'maxLength': 4}}}}}");
+ BSONObj document = fromjson("{'b': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -715,12 +798,35 @@ TEST(JSONSchemaValidation, PatternAtTopLevelHasNoEffect) {
BSONObj document = fromjson("{a: 'Slow'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$nor',"
- " 'clausesNotSatisfied': [{'index': 0, 'details': "
- " {'operatorName': '$jsonSchema',"
- " 'schemaRulesNotSatisfied': ["
- " {'operatorName': 'pattern', "
- " 'specifiedAs': {'pattern': '^S'}, "
- " 'reason': 'expression always evaluates to true'}]}}]}");
+ " 'clausesSatisfied': [{'index': 0, 'details': "
+ " {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, PatternInvertedRegexDoesMatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'pattern': '^S'}}}}}");
+ BSONObj document = fromjson("{'a': 'String'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, PatternInvertedTypeMismatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'pattern': '^S'}}}}}");
+ BSONObj document = fromjson("{'a': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, PatternInvertedMissingProperty) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'pattern': '^S'}}}}}");
+ BSONObj document = fromjson("{'b': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
@@ -751,7 +857,7 @@ TEST(JSONSchemaValidation, MultipleOfNoExplicitType) {
" 'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties',"
" 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', details: ["
+ " {'propertyName': 'a', 'details': ["
" {'operatorName': 'multipleOf',"
" 'specifiedAs': {'multipleOf': 2.1},"
" 'reason': 'considered value is not a multiple of the specified "
@@ -813,15 +919,37 @@ TEST(JSONSchemaValidation, MultipleOfAtTopLevelHasNoEffect) {
BSONObj document = fromjson("{a: 'foo'}");
BSONObj expectedError = fromjson(
"{'operatorName': '$nor',"
- " 'clausesNotSatisfied': [{'index': 0, 'details': "
- " {'operatorName': '$jsonSchema',"
- " 'schemaRulesNotSatisfied': ["
- " {'operatorName': 'multipleOf', "
- " 'specifiedAs': {'multipleOf': 1}, "
- " 'reason': 'expression always evaluates to true'}]}}]}");
+ " 'clausesSatisfied': [{'index': 0, 'details': "
+ " {'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MultipleOfInvertedMultipleDoesMatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'multipleOf': 1}}}}}");
+ BSONObj document = fromjson("{'a': 2}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, MultipleOfInvertedTypeMismatch) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'multipleOf': 1}}}}}");
+ BSONObj document = fromjson("{'a': 'not a number!'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaValidation, MultipleOfInvertedMissingProperty) {
+ BSONObj query = fromjson("{'$jsonSchema': {'not': {'properties': {'a': {'multipleOf': 1}}}}}");
+ BSONObj document = fromjson("{'b': 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
// encrypt
TEST(JSONSchemaValidation, BasicEncrypt) {
BSONObj query =
@@ -832,12 +960,34 @@ TEST(JSONSchemaValidation, BasicEncrypt) {
" 'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties',"
" 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', details: ["
+ " {'propertyName': 'a', 'details': ["
" {'operatorName': 'encrypt',"
" 'reason': 'value was not encrypted'}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaValidation, EncryptInvertedValueIsEncrypted) {
+ BSONObj query = fromjson(
+ "{$nor: [{'$jsonSchema': {bsonType: 'object', properties: {a: {encrypt: "
+ "{}}}}}]}");
+ BSONObj document = BSON("a" << BSONBinData("abc", 3, BinDataType::Encrypt));
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$nor','clausesSatisfied': ["
+ " {'index': 0, 'details':{'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, EncryptMissingProperty) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {not: {bsonType: 'object', properties: {a: {encrypt: "
+ "{}}}}}}");
+ BSONObj document = BSON("b" << BSONBinData("abc", 3, BinDataType::Encrypt));
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
TEST(JSONSchemaValidation, EncryptWithSubtypeFailsBecauseNotEncrypted) {
BSONObj query = fromjson(
"{'$jsonSchema':"
@@ -849,7 +999,7 @@ TEST(JSONSchemaValidation, EncryptWithSubtypeFailsBecauseNotEncrypted) {
" 'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties',"
" 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', details: ["
+ " {'propertyName': 'a', 'details': ["
" {'operatorName': 'encrypt',"
" 'reason': 'value was not encrypted'}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
@@ -873,11 +1023,424 @@ TEST(JSONSchemaValidation, EncryptWithSubtypeFailsDueToMismatchedSubtype) {
" 'schemaRulesNotSatisfied': ["
" {'operatorName': 'properties',"
" 'propertiesNotSatisfied': ["
- " {'propertyName': 'a', details: ["
+ " {'propertyName': 'a', 'details': ["
" {'operatorName': 'encrypt',"
" 'reason': 'encrypted value has wrong type'}]}]}]}");
doc_validation_error::verifyGeneratedError(query, document, expectedError);
}
+TEST(JSONSchemaValidation, EncryptWithSubtypeInvertedValueIsEncrypted) {
+ BSONObj query = fromjson(
+ "{$nor: [{'$jsonSchema': {bsonType: 'object', properties: "
+ " {a: {encrypt: {bsonType: 'number'}}}}}]}");
+ FleBlobHeader blob;
+ blob.fleBlobSubtype = FleBlobSubtype::Deterministic;
+ memset(blob.keyUUID, 0, sizeof(blob.keyUUID));
+ blob.originalBsonType = BSONType::NumberInt;
+
+ BSONObj document = BSON("a" << BSONBinData(reinterpret_cast<const void*>(&blob),
+ sizeof(FleBlobHeader),
+ BinDataType::Encrypt));
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$nor','clausesSatisfied': ["
+ " {'index': 0, 'details':{'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaValidation, EncryptWithSubtypeInvertedMissingProperty) {
+ BSONObj query =
+ fromjson("{$nor: [{'$jsonSchema': {bsonType: 'object', properties: {a: {encrypt:{}}}}}]}");
+ BSONObj document = BSON("b" << BSONBinData("abc", 3, BinDataType::Encrypt));
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$nor','clausesSatisfied': ["
+ " {'index': 0, 'details':{'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+// Logical keywords
+
+// allOf
+TEST(JSONSchemaLogicalKeywordValidation, TopLevelAllOf) {
+ BSONObj query = fromjson(
+ "{$jsonSchema: "
+ "{allOf: [{properties: {a: {minimum: 1}}},{properties:{b:{minimum:2}}}]}}");
+ BSONObj document = fromjson("{a: 1, b: 0}");
+ BSONObj expectedError = fromjson(
+ "{operatorName: '$jsonSchema', "
+ " schemaRulesNotSatisfied: ["
+ " {operatorName: 'allOf', "
+ " schemasNotSatisfied: [ "
+ " {index: 1, details: ["
+ " {operatorName: 'properties', propertiesNotSatisfied: ["
+ " {'propertyName': 'b', details: ["
+ " {'operatorName': 'minimum', specifiedAs: {minimum: 2},"
+ " reason: 'comparison failed', consideredValue: 0}]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NestedAllOf) {
+ BSONObj query =
+ fromjson("{$jsonSchema: {properties: {a: {allOf: [{minimum: 1},{maximum: 3}]}}}}");
+ BSONObj document = fromjson("{a: 4}");
+ BSONObj expectedError = fromjson(
+ "{operatorName: '$jsonSchema', "
+ " 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'allOf',"
+ " 'schemasNotSatisfied': ["
+ " {'index': 1, 'details': ["
+ " {'operatorName': 'maximum', "
+ " 'specifiedAs': {'maximum': 3},"
+ " 'reason': 'comparison failed',"
+ " 'consideredValue': 4}]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotOverAllOf) {
+ BSONObj query =
+ fromjson("{$jsonSchema: {properties: {a: {not: {allOf: [{minimum: 1},{maximum: 3}]}}}}}");
+ BSONObj document = fromjson("{a: 2}");
+ BSONObj expectedError = fromjson(
+ "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {operatorName: 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', 'details': ["
+ " {operatorName: 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+// anyOf
+TEST(JSONSchemaLogicalKeywordValidation, TopLevelAnyOf) {
+ BSONObj query = fromjson(
+ "{$jsonSchema: {anyOf: [{properties: {a: {minimum: 1}}}, "
+ "{properties:{b:{minimum: 1}}}]}}");
+ BSONObj document = fromjson("{a: 0, b: 0}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {'operatorName': 'anyOf', schemasNotSatisfied: ["
+ " {index: 0, details: [ "
+ " {'operatorName': 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', 'details': [ "
+ " {'operatorName': 'minimum', "
+ " 'specifiedAs': {'minimum': 1 }, "
+ " 'reason': 'comparison failed', "
+ " 'consideredValue': 0}]}]}]}, "
+ " {index: 1, 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(JSONSchemaLogicalKeywordValidation, NestedAnyOf) {
+ BSONObj query =
+ fromjson("{$jsonSchema: {properties: {a: {anyOf: [{type: 'string'},{maximum: 3}]}}}}");
+ BSONObj document = fromjson("{a: 4}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'anyOf', 'schemasNotSatisfied': ["
+ " {'index': 0, 'details': ["
+ " {'operatorName': 'type', "
+ " 'specifiedAs': {'type': 'string' }, "
+ " 'reason': 'type did not match', "
+ " 'consideredValue': 4, "
+ " consideredType: 'int' }]},"
+ " {'index': 1, 'details': ["
+ " {'operatorName': 'maximum',"
+ " 'specifiedAs': {maximum: 3}, "
+ " 'reason': 'comparison failed', "
+ " 'consideredValue': 4 }]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+// Combine anyOf and allOf
+TEST(JSONSchemaLogicalKeywordValidation, TopLevelAnyOfAllOf) {
+ BSONObj query = fromjson(
+ "{$jsonSchema: {anyOf: ["
+ " {allOf: [{properties: "
+ " {d: {type: 'string', 'minLength': 3},"
+ " e: {maxLength: 10}}}]},"
+ " {allOf: [{properties: "
+ " {a: {type: 'number', 'maximum': 3}, "
+ " b: {type: 'number', 'minimum': 3},"
+ " c: {type: 'number', 'enum': [1,2,3]}}}]}]}}");
+ BSONObj document =
+ fromjson("{a: 0, b: 5, c: 0, d: 'foobar', e: 'a string that is over ten characters'}");
+ BSONObj expectedError = fromjson(
+ "{operatorName: '$jsonSchema',"
+ " 'schemaRulesNotSatisfied': [{operatorName: 'anyOf', 'schemasNotSatisfied': ["
+ " {index: 0, details: ["
+ " {operatorName: 'allOf', schemasNotSatisfied:[{'index': 0, 'details': "
+ " [{operatorName: 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'e', details: ["
+ " {operatorName: 'maxLength',"
+ " specifiedAs: {maxLength: 10}, "
+ " reason: 'specified string length was not satisfied',"
+ " consideredValue: 'a string that is over ten "
+ "characters'}]}]}]}]}]}, "
+ " {index: 1,details: ["
+ " {operatorName: 'allOf', schemasNotSatisfied:[{'index': 0, 'details': ["
+ " {operatorName: 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'c', details: ["
+ " {operatorName: 'enum',"
+ " specifiedAs: {enum: [1,2,3]}, "
+ " reason: 'value was not found in enum',"
+ " consideredValue: 0}]}]}]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, TopLevelAllOfAnyOf) {
+ BSONObj query = fromjson(
+ "{$jsonSchema: {allOf: ["
+ " {anyOf: [{properties: "
+ " {d: {type: 'string', 'minLength': 3},"
+ " e: {maxLength: 10}}}]},"
+ " {anyOf: [{properties: "
+ " {a: {type: 'number', 'maximum': 3}, "
+ " b: {type: 'number', 'minimum': 3},"
+ " c: {type: 'number', 'enum': [1,2,3]}}}]}]}}");
+ BSONObj document = fromjson("{a: 10, b: 0, c: 0, d: 'foobar', e: 1}");
+ BSONObj expectedError = fromjson(
+ "{operatorName: '$jsonSchema',"
+ " 'schemaRulesNotSatisfied': [{operatorName: 'allOf', 'schemasNotSatisfied': ["
+ " {index: 1,details: ["
+ " {operatorName: 'anyOf', schemasNotSatisfied:[{'index': 0, 'details': ["
+ " {operatorName: 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', details: ["
+ " {operatorName: 'maximum',"
+ " specifiedAs: {maximum: 3}, "
+ " reason: 'comparison failed',"
+ " consideredValue: 10}]},"
+ " {propertyName: 'b', details: ["
+ " {operatorName: 'minimum',"
+ " specifiedAs: {minimum: 3}, "
+ " reason: 'comparison failed',"
+ " consideredValue: 0}]},"
+ " {propertyName: 'c', details: ["
+ " {operatorName: 'enum',"
+ " specifiedAs: {enum: [1,2,3]}, "
+ " reason: 'value was not found in enum',"
+ " consideredValue: 0}]}"
+ "]}]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotOverAnyOf) {
+ BSONObj query =
+ fromjson("{$jsonSchema: {properties: {a: {not: {anyOf: [{minimum: 1},{maximum: 6}]}}}}}");
+ BSONObj document = fromjson("{a: 4}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+// oneOf
+TEST(JSONSchemaLogicalKeywordValidation, OneOfMoreThanOneMatchingClause) {
+ BSONObj query =
+ fromjson("{$jsonSchema: {properties: {a: {oneOf: [{minimum: 1},{maximum: 3}]}}}}");
+ BSONObj document = fromjson("{a: 2}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema',schemaRulesNotSatisfied: ["
+ " {'operatorName': 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', 'details': ["
+ " {'operatorName': 'oneOf', "
+ " 'reason': 'more than one subschema matched', "
+ " 'matchingSchemaIndexes': [0, 1]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NorOverOneOf) {
+ BSONObj query = fromjson(
+ "{$nor: [{$jsonSchema: {properties: {a: {oneOf: [{minimum: 1},{maximum: 3}]}}}}]}");
+ BSONObj document = fromjson("{a: 4}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$nor','clausesSatisfied': ["
+ " {'index': 0, 'details':{'operatorName': '$jsonSchema', 'reason': 'schema matched'}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, OneOfAllFailingClauses) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema':"
+ "{'properties':{'a':{'oneOf': [{'minimum': 4},{'maximum': 1},{'bsonType':'int'}]}}}}");
+ BSONObj document = fromjson("{a: 2.1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {'operatorName': 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', 'details': ["
+ " {'operatorName': 'oneOf', schemasNotSatisfied: ["
+ " {index: 0, details: ["
+ " {'operatorName': 'minimum', "
+ " 'specifiedAs': {minimum: 4}, "
+ " 'reason': 'comparison failed', "
+ " 'consideredValue': 2.1 }]}, "
+ " {index: 1, details: ["
+ " {'operatorName': 'maximum', "
+ " 'specifiedAs': {maximum: 1}, "
+ " 'reason': 'comparison failed', "
+ " 'consideredValue': 2.1 }]}, "
+ " {index: 2, details: ["
+ " {'operatorName': 'bsonType', "
+ " 'specifiedAs': {'bsonType': 'int' }, "
+ " 'reason': 'type did not match', "
+ " 'consideredValue': 2.1, "
+ " 'consideredType': 'double'}]}]}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotOverOneOf) {
+ BSONObj query =
+ fromjson("{$jsonSchema: {properties: {a: {not: {oneOf: [{minimum: 1},{maximum: 3}]}}}}}");
+ BSONObj document = fromjson("{a: 4}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotOverOneOfOneClause) {
+ BSONObj query = fromjson("{$jsonSchema: {properties: {a: {not: {oneOf: [{minimum: 1}]}}}}}");
+ BSONObj document = fromjson("{a: 4}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {propertyName: 'a', 'details': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+// not
+TEST(JSONSchemaLogicalKeywordValidation, BasicNot) {
+ BSONObj query = fromjson("{$jsonSchema: {not: {properties: {a: {minimum: 3}}}}}");
+ BSONObj document = fromjson("{a: 6}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NestedNot) {
+ BSONObj query = fromjson("{$jsonSchema: {not: {not: {properties: {a: {minimum: 3}}}}}}");
+ BSONObj document = fromjson("{a: 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotOverEmptySchema) {
+ BSONObj query = fromjson("{$jsonSchema: {not: {}}}");
+ BSONObj document = fromjson("{}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', "
+ " 'schemaRulesNotSatisfied': [{'operatorName': 'not', 'reason': 'child expression "
+ "matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotFailsDueToType) {
+ BSONObj query = fromjson("{$jsonSchema: {not: {properties: {a: {minimum: 3}}}}}");
+ BSONObj document = fromjson("{a: 'foo'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotFailsDueToExistence) {
+ BSONObj query = fromjson("{$jsonSchema: {not: {properties: {a: {minimum: 3}}}}}");
+ BSONObj document = fromjson("{b: 6}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotUnderProperties) {
+ BSONObj query = fromjson("{$jsonSchema: {properties: {a: {not: {}}}}}}");
+ BSONObj document = fromjson("{a: 'foo'}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'not', "
+ " 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+// enum
+TEST(JSONSchemaLogicalKeywordValidation, BasicEnum) {
+ BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'enum': [1,2,3]}}}}}");
+ BSONObj document = fromjson("{a: 0}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {'operatorName': 'properties', propertiesNotSatisfied: ["
+ " {propertyName: 'a', 'details': ["
+ " {'operatorName': 'enum', "
+ " specifiedAs: {enum: [ 1, 2, 3 ]}, "
+ " reason: 'value was not found in enum', "
+ " consideredValue: 0 }]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, TopLevelEnum) {
+ BSONObj query =
+ fromjson("{'$jsonSchema': {'enum': [{'a': 1, 'b': 1}, {'a': 0, 'b': {'c': [1,2,3]}}]}}");
+ BSONObj document = fromjson("{'a': 0, b: 1, _id: 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', schemaRulesNotSatisfied: ["
+ " {'operatorName': 'enum', "
+ " specifiedAs: {enum: [{a: 1, b: 1 }, {a: 0, b: {c: [ 1, 2, 3 ]}}]}, "
+ " reason: 'value was not found in enum', "
+ " 'consideredValue': {a: 0, b: 1, _id: 1}}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+TEST(JSONSchemaLogicalKeywordValidation, NotOverEnum) {
+ BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'not': {'enum': [1,2,3]}}}}}}");
+ BSONObj document = fromjson("{a: 1}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'not', 'reason': 'child expression matched'}]}]}]}");
+ doc_validation_error::verifyGeneratedError(query, document, expectedError);
+}
+
+// Combine logical keywords
+TEST(JSONSchemaLogicalKeywordValidation, CombineLogicalKeywords) {
+ BSONObj query = fromjson(
+ "{'$jsonSchema': {'properties': {'a': "
+ " {'allOf': [{'bsonType': 'int'},"
+ " {'oneOf': [{minimum: 1}, {enum: [6,7,8]}]}]}}}}}");
+ BSONObj document = fromjson("{a: 0}");
+ BSONObj expectedError = fromjson(
+ "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': ["
+ " {'operatorName': 'properties', 'propertiesNotSatisfied': ["
+ " {'propertyName': 'a', 'details': ["
+ " {'operatorName': 'allOf', 'schemasNotSatisfied': ["
+ " {'index': 1, 'details': [{'operatorName': 'oneOf', "
+ " 'schemasNotSatisfied': ["
+ " {'index': 0, 'details':["
+ " {'operatorName': 'minimum',"
+ " 'specifiedAs': {'minimum': 1 },"
+ " 'reason': 'comparison failed', "
+ " 'consideredValue': 0}]},"
+ " {'index': 1, 'details': ["
+ " {'operatorName': 'enum', "
+ " 'specifiedAs': {'enum': [ 6, 7, 8 ]}, "
+ " 'reason': 'value was not found in enum', "
+ " 'consideredValue': 0 }]}]}]}]}]}]}]}");
+ 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 e71bd15a88f..64ad187e818 100644
--- a/src/mongo/db/matcher/doc_validation_error_test.cpp
+++ b/src/mongo/db/matcher/doc_validation_error_test.cpp
@@ -538,7 +538,7 @@ TEST(LogicalMatchExpression, BasicNor) {
BSONObj expectedError =
BSON("operatorName"
<< "$nor"
- << "clausesNotSatisfied"
+ << "clausesSatisfied"
<< BSON_ARRAY(BSON("index" << 1 << "details"
<< BSON("operatorName"
<< "$lt"
@@ -556,7 +556,7 @@ TEST(LogicalMatchExpression, NorAllSuccessfulClauses) {
BSONObj expectedError = BSON(
"operatorName"
<< "$nor"
- << "clausesNotSatisfied"
+ << "clausesSatisfied"
<< BSON_ARRAY(BSON("index" << 0 << "details"
<< BSON("operatorName"
<< "$lt"
@@ -599,7 +599,7 @@ TEST(LogicalMatchExpression, NotOverImplicitAnd) {
<< "details"
<< BSON("operatorName"
<< "$and"
- << "clausesNotSatisfied"
+ << "clausesSatisfied"
<< BSON_ARRAY(
BSON("index" << 0 << "details"
<< BSON("operatorName"
@@ -703,7 +703,7 @@ TEST(LogicalMatchExpression, NestedAndOrNorOneSuccessfulClause) {
"'clausesNotSatisfied': ["
" {'index': 1, 'details': "
" {'operatorName': '$nor',"
- " 'clausesNotSatisfied': ["
+ " 'clausesSatisfied': ["
" {'index': 1, 'details':"
" {'operatorName': '$lt',"
" 'specifiedAs': {'qty': {'$lt': 20}},"
@@ -726,7 +726,7 @@ TEST(LogicalMatchExpression, NestedAndOrNorNotOneFailingClause) {
"'clausesNotSatisfied': ["
" {'index': 1, 'details': "
" {'operatorName': '$nor',"
- " 'clausesNotSatisfied': ["
+ " 'clausesSatisfied': ["
" {'index': 1, 'details':"
" {'operatorName': '$not',"
" 'details': "
@@ -844,7 +844,7 @@ TEST(MiscellaneousMatchExpression, NorExpr) {
BSONObj document = BSON("a" << 1 << "b" << 1);
BSONObj expectedError = BSON("operatorName"
<< "$nor"
- << "clausesNotSatisfied"
+ << "clausesSatisfied"
<< BSON_ARRAY(BSON(
"index" << 0 << "details"
<< BSON("operatorName"
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp
index b3a0554982c..263b23cc4e9 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp
@@ -39,11 +39,13 @@ namespace mongo {
constexpr StringData InternalSchemaEqMatchExpression::kName;
-InternalSchemaEqMatchExpression::InternalSchemaEqMatchExpression(StringData path, BSONElement rhs)
+InternalSchemaEqMatchExpression::InternalSchemaEqMatchExpression(
+ StringData path, BSONElement rhs, clonable_ptr<ErrorAnnotation> annotation)
: LeafMatchExpression(MatchType::INTERNAL_SCHEMA_EQ,
path,
ElementPath::LeafArrayBehavior::kNoTraversal,
- ElementPath::NonLeafArrayBehavior::kTraverse),
+ ElementPath::NonLeafArrayBehavior::kTraverse,
+ std::move(annotation)),
_rhsElem(rhs) {
invariant(_rhsElem);
}
@@ -83,7 +85,8 @@ bool InternalSchemaEqMatchExpression::equivalent(const MatchExpression* other) c
}
std::unique_ptr<MatchExpression> InternalSchemaEqMatchExpression::shallowClone() const {
- auto clone = std::make_unique<InternalSchemaEqMatchExpression>(path(), _rhsElem);
+ auto clone =
+ std::make_unique<InternalSchemaEqMatchExpression>(path(), _rhsElem, _errorAnnotation);
if (getTag()) {
clone->setTag(getTag()->clone());
}
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_eq.h
index ebe8da7e3bd..d7fd43a57ba 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_eq.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq.h
@@ -48,7 +48,9 @@ class InternalSchemaEqMatchExpression final : public LeafMatchExpression {
public:
static constexpr StringData kName = "$_internalSchemaEq"_sd;
- InternalSchemaEqMatchExpression(StringData path, BSONElement rhs);
+ InternalSchemaEqMatchExpression(StringData path,
+ BSONElement rhs,
+ clonable_ptr<ErrorAnnotation> annotation = nullptr);
std::unique_ptr<MatchExpression> shallowClone() const final;
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp
index 02f1d116825..c4b1e3f7aa2 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp
@@ -71,7 +71,8 @@ bool InternalSchemaRootDocEqMatchExpression::equivalent(const MatchExpression* o
}
std::unique_ptr<MatchExpression> InternalSchemaRootDocEqMatchExpression::shallowClone() const {
- auto clone = std::make_unique<InternalSchemaRootDocEqMatchExpression>(_rhsObj.copy());
+ auto clone =
+ std::make_unique<InternalSchemaRootDocEqMatchExpression>(_rhsObj.copy(), _errorAnnotation);
if (getTag()) {
clone->setTag(getTag()->clone());
}
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h
index 805529be8f4..9e374b655c9 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h
@@ -51,8 +51,10 @@ public:
/**
* Constructs a new match expression, taking ownership of 'rhs'.
*/
- explicit InternalSchemaRootDocEqMatchExpression(BSONObj rhs)
- : MatchExpression(MatchExpression::INTERNAL_SCHEMA_ROOT_DOC_EQ), _rhsObj(std::move(rhs)) {}
+ explicit InternalSchemaRootDocEqMatchExpression(
+ BSONObj rhs, clonable_ptr<ErrorAnnotation> annotation = nullptr)
+ : MatchExpression(MatchExpression::INTERNAL_SCHEMA_ROOT_DOC_EQ, std::move(annotation)),
+ _rhsObj(std::move(rhs)) {}
bool matches(const MatchableDocument* doc, 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 7de01f1ee43..16baae059db 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp
@@ -372,7 +372,8 @@ StatusWithMatchExpression parseLogicalKeyword(const boost::intrusive_ptr<Express
<< "' must be a non-empty array"};
}
- std::unique_ptr<T> listOfExpr = std::make_unique<T>();
+ std::unique_ptr<T> listOfExpr = std::make_unique<T>(doc_validation_error::createAnnotation(
+ expCtx, logicalElement.fieldNameStringData().toString(), BSONObj()));
for (const auto& elem : logicalElementObj) {
if (elem.type() != BSONType::Object) {
return {ErrorCodes::TypeMismatch,
@@ -393,7 +394,9 @@ StatusWithMatchExpression parseLogicalKeyword(const boost::intrusive_ptr<Express
return {std::move(listOfExpr)};
}
-StatusWithMatchExpression parseEnum(StringData path, BSONElement enumElement) {
+StatusWithMatchExpression parseEnum(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ StringData path,
+ BSONElement enumElement) {
if (enumElement.type() != BSONType::Array) {
return {ErrorCodes::TypeMismatch,
str::stream() << "$jsonSchema keyword '" << JSONSchemaParser::kSchemaEnumKeyword
@@ -408,7 +411,8 @@ StatusWithMatchExpression parseEnum(StringData path, BSONElement enumElement) {
<< "' cannot be an empty array"};
}
- auto orExpr = std::make_unique<OrMatchExpression>();
+ auto orExpr = std::make_unique<OrMatchExpression>(doc_validation_error::createAnnotation(
+ expCtx, enumElement.fieldNameStringData().toString(), enumElement.wrap()));
UnorderedFieldsBSONElementComparator eltComp;
BSONEltSet eqSet = eltComp.makeBSONEltSet();
for (auto&& arrayElem : enumArray) {
@@ -425,19 +429,23 @@ StatusWithMatchExpression parseEnum(StringData path, BSONElement enumElement) {
// objects, not scalars or arrays.
if (arrayElem.type() == BSONType::Object) {
auto rootDocEq = std::make_unique<InternalSchemaRootDocEqMatchExpression>(
- arrayElem.embeddedObject());
+ arrayElem.embeddedObject(),
+ doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore));
orExpr->add(rootDocEq.release());
}
} else {
- auto eqExpr = std::make_unique<InternalSchemaEqMatchExpression>(path, arrayElem);
-
+ auto eqExpr = std::make_unique<InternalSchemaEqMatchExpression>(
+ path,
+ arrayElem,
+ doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore));
orExpr->add(eqExpr.release());
}
}
// Make sure that the OR expression has at least 1 child.
if (orExpr->numChildren() == 0) {
- return {std::make_unique<AlwaysFalseMatchExpression>()};
+ return {std::make_unique<AlwaysFalseMatchExpression>(doc_validation_error::createAnnotation(
+ expCtx, enumElement.fieldNameStringData().toString(), enumElement.wrap()))};
}
return {std::move(orExpr)};
@@ -1136,12 +1144,14 @@ Status translateLogicalKeywords(StringMap<BSONElement>& keywordMap,
return parsedExpr.getStatus();
}
- auto notMatchExpr = std::make_unique<NotMatchExpression>(parsedExpr.getValue().release());
+ auto notMatchExpr = std::make_unique<NotMatchExpression>(
+ parsedExpr.getValue().release(),
+ doc_validation_error::createAnnotation(expCtx, "not", BSONObj()));
andExpr->add(notMatchExpr.release());
}
if (auto enumElt = keywordMap[JSONSchemaParser::kSchemaEnumKeyword]) {
- auto enumExpr = parseEnum(path, enumElt);
+ auto enumExpr = parseEnum(expCtx, path, enumElt);
if (!enumExpr.isOK()) {
return enumExpr.getStatus();
}
@@ -1645,8 +1655,12 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>&
doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore));
}
+ // All schemas are tagged with an operator name of '_internalSubschema' to indicate during
+ // error generation that 'andExpr' logically corresponds to a subschema. If this is a top
+ // level schema corresponding to '$jsonSchema', the caller is responsible for providing this
+ // information by overwriting this annotation.
auto andExpr = std::make_unique<AndMatchExpression>(
- doc_validation_error::createAnnotation(expCtx, "$jsonSchema", BSONObj()));
+ doc_validation_error::createAnnotation(expCtx, "_internalSubschema", BSONObj()));
auto translationStatus =
translateScalarKeywords(expCtx, keywordMap, path, typeExpr.get(), andExpr.get());
@@ -1767,6 +1781,14 @@ StatusWithMatchExpression JSONSchemaParser::parse(
"Translated schema match expression",
"expression"_attr = translation.getValue()->debugString());
}
+ // Tag the top level MatchExpression as '$jsonSchema' if necessary.
+ if (translation.isOK()) {
+ if (auto topLevelAnnotation = translation.getValue()->getErrorAnnotation()) {
+ auto oldAnnotation = topLevelAnnotation->annotation;
+ translation.getValue()->setErrorAnnotation(
+ doc_validation_error::createAnnotation(expCtx, "$jsonSchema", oldAnnotation));
+ }
+ }
return translation;
} catch (const DBException& ex) {
return {ex.toStatus()};