summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/core/json_schema.js23
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp51
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser_test.cpp89
3 files changed, 110 insertions, 53 deletions
diff --git a/jstests/core/json_schema.js b/jstests/core/json_schema.js
index 8d458734d9a..bfebfb7ab90 100644
--- a/jstests/core/json_schema.js
+++ b/jstests/core/json_schema.js
@@ -12,6 +12,7 @@
assert.writeOK(coll.insert({_id: 5, num: NumberLong(-1)}));
assert.writeOK(coll.insert({_id: 6, num: {}}));
assert.writeOK(coll.insert({_id: 7, num: "str"}));
+ assert.writeOK(coll.insert({_id: 8}));
// Test that $jsonSchema fails to parse if its argument is not an object.
assert.throws(function() {
@@ -53,57 +54,57 @@
});
// Test that the empty schema matches everything.
- assert.eq(8, coll.find({$jsonSchema: {}}).itcount());
+ assert.eq(9, coll.find({$jsonSchema: {}}).itcount());
// Test that a schema just checking that the type of stored documents is "object" is legal and
// matches everything.
- assert.eq(8, coll.find({$jsonSchema: {type: "object"}}).itcount());
+ assert.eq(9, coll.find({$jsonSchema: {type: "object"}}).itcount());
// Test that schemas whose top-level type is not object matches nothing.
assert.eq(0, coll.find({$jsonSchema: {type: "string"}}).itcount());
assert.eq(0, coll.find({$jsonSchema: {type: "long"}}).itcount());
assert.eq(0, coll.find({$jsonSchema: {type: "objectId"}}).itcount());
- // Test that type:"number" only matches numbers.
- assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ // Test that type:"number" only matches numbers, or documents where the field is missing.
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 8}],
coll.find({$jsonSchema: {properties: {num: {type: "number"}}}}, {_id: 1})
.sort({_id: 1})
.toArray());
// Test that maximum restriction is enforced correctly.
- assert.eq([{_id: 1}, {_id: 3}, {_id: 5}],
+ assert.eq([{_id: 1}, {_id: 3}, {_id: 5}, {_id: 8}],
coll.find({$jsonSchema: {properties: {num: {type: "number", maximum: -1}}}}, {_id: 1})
.sort({_id: 1})
.toArray());
// Repeat the test, but include an explicit top-level type:"object".
assert.eq(
- [{_id: 1}, {_id: 3}, {_id: 5}],
+ [{_id: 1}, {_id: 3}, {_id: 5}, {_id: 8}],
coll.find({$jsonSchema: {type: "object", properties: {num: {type: "number", maximum: -1}}}},
{_id: 1})
.sort({_id: 1})
.toArray());
- // Test that type:"long" only matches longs.
- assert.eq([{_id: 4}, {_id: 5}],
+ // Test that type:"long" only matches longs, or documents where the field is missing.
+ assert.eq([{_id: 4}, {_id: 5}, {_id: 8}],
coll.find({$jsonSchema: {properties: {num: {type: "long"}}}}, {_id: 1})
.sort({_id: 1})
.toArray());
// Test that maximum restriction is enforced correctly with type:"long".
- assert.eq([{_id: 5}],
+ assert.eq([{_id: 5}, {_id: 8}],
coll.find({$jsonSchema: {properties: {num: {type: "long", maximum: 0}}}}, {_id: 1})
.sort({_id: 1})
.toArray());
// Test that maximum restriction without a numeric type specified only applies to numbers.
- assert.eq([{_id: 1}, {_id: 3}, {_id: 5}, {_id: 6}, {_id: 7}],
+ assert.eq([{_id: 1}, {_id: 3}, {_id: 5}, {_id: 6}, {_id: 7}, {_id: 8}],
coll.find({$jsonSchema: {properties: {num: {maximum: 0}}}}, {_id: 1})
.sort({_id: 1})
.toArray());
// Test that maximum restriction does nothing if a non-numeric type is also specified.
- assert.eq([{_id: 7}],
+ assert.eq([{_id: 7}, {_id: 8}],
coll.find({$jsonSchema: {properties: {num: {type: "string", maximum: 0}}}}, {_id: 1})
.sort({_id: 1})
.toArray());
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp
index c5fcb89cc1e..4418eb2f699 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp
@@ -70,19 +70,24 @@ std::unique_ptr<MatchExpression> makeRestriction(TypeMatchExpression::Type restr
(statedType->matchesAllNumbers() || isNumericBSONType(statedType->getBSONType()));
const bool bsonTypesMatch = restrictionType.bsonType == statedType->getBSONType();
- if (bothNumeric || bsonTypesMatch) {
- // This restriction applies only to the type that is already being enforced. We return
- // the restriction unmodified.
- return restrictionExpr;
- } else {
+ if (!bothNumeric && !bsonTypesMatch) {
// This restriction doesn't take any effect, since the type of the schema is different
// from the type to which this retriction applies.
return stdx::make_unique<AlwaysTrueMatchExpression>();
}
}
- invariant(!statedType);
-
+ // Generate and return the following expression tree:
+ //
+ // OR
+ // / /
+ // NOT <restrictionExpr>
+ // /
+ // TYPE
+ // <restrictionType>
+ //
+ // We need to do this because restriction keywords do not apply when a field is either not
+ // present or of a different type.
auto typeExprForNot = stdx::make_unique<TypeMatchExpression>();
invariantOK(typeExprForNot->init(restrictionExpr->path(), restrictionType));
@@ -94,6 +99,36 @@ std::unique_ptr<MatchExpression> makeRestriction(TypeMatchExpression::Type restr
return std::move(orExpr);
}
+/**
+ * Constructs and returns the following expression tree:
+ * OR
+ * / \
+ * NOT <typeExpr>
+ * /
+ * EXISTS
+ * <typeExpr field>
+ *
+ * This is needed because the JSON Schema 'type' keyword only applies if the corresponding field is
+ * present.
+ *
+ * 'typeExpr' must be non-null and must have a non-empty path.
+ */
+std::unique_ptr<MatchExpression> makeTypeRestriction(
+ std::unique_ptr<TypeMatchExpression> typeExpr) {
+ invariant(typeExpr);
+ invariant(!typeExpr->path().empty());
+
+ auto existsExpr = stdx::make_unique<ExistsMatchExpression>();
+ invariantOK(existsExpr->init(typeExpr->path()));
+
+ auto notExpr = stdx::make_unique<NotMatchExpression>(existsExpr.release());
+ auto orExpr = stdx::make_unique<OrMatchExpression>();
+ orExpr->add(notExpr.release());
+ orExpr->add(typeExpr.release());
+
+ return std::move(orExpr);
+}
+
StatusWith<std::unique_ptr<TypeMatchExpression>> parseType(StringData path, BSONElement typeElt) {
if (!typeElt) {
return {nullptr};
@@ -318,7 +353,7 @@ StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj sche
}
if (!path.empty() && typeExpr.getValue()) {
- andExpr->add(typeExpr.getValue().release());
+ andExpr->add(makeTypeRestriction(std::move(typeExpr.getValue())).release());
}
return {std::move(andExpr)};
}
diff --git a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
index a39244e12f7..a0de359d855 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
@@ -102,8 +102,9 @@ TEST(JSONSchemaParserTest, NestedTypeObjectTranslatesCorrectly) {
ASSERT_BSONOBJ_EQ(
builder.obj(),
fromjson("{$and: [{$and: [{$and: ["
- "{a: {$_internalSchemaObjectMatch: {$and: [{$and:[{b: {$type:2}}]}]}}},"
- "{a: {$type: 3}}]}]}]}"));
+ "{$or: [{$nor: [{a: {$type: 3}}]}, {a: {$_internalSchemaObjectMatch:"
+ "{$and: [{$and:[{$or: [{$nor: [{b: {$exists: true}}]}, {b: {$type: 2}}]}]}]}}}]},"
+ "{$or: [{$nor: [{a: {$exists: true}}]}, {a: {$type: 3}}]}]}]}]}"));
}
TEST(JSONSchemaParserTest, TopLevelNonObjectTypeTranslatesCorrectly) {
@@ -121,8 +122,10 @@ TEST(JSONSchemaParserTest, TypeNumberTranslatesCorrectly) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$type: 'number'}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(
+ builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 'number'}}]}]}]}]}"));
}
TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeNumber) {
@@ -131,9 +134,11 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeNumber) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$lte: 0}}, {num: {$type: 'number'}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$lte: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 'number'}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeLong) {
@@ -142,9 +147,11 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeLong) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$lte: 0}}, {num: {$type: 18}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$lte: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 18}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeString) {
@@ -153,9 +160,10 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeString) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{$alwaysTrue: 1}, {num: {$type: 2}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: [{$alwaysTrue: 1},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 2}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithNoType) {
@@ -177,9 +185,11 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumTrue) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$lt: 0}}, {num: {$type: 18}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$lt: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 18}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumFalse) {
@@ -189,9 +199,11 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumFalse)
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$lte: 0}}, {num: {$type: 18}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$lte: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 18}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, FailsToParseIfMaximumIsNotANumber) {
@@ -218,9 +230,11 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeNumber) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$gte: 0}}, {num: {$type: 'number'}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$gte: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 'number'}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeLong) {
@@ -229,9 +243,11 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeLong) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$gte: 0}}, {num: {$type: 18}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$gte: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 18}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeString) {
@@ -240,9 +256,10 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeString) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{$alwaysTrue: 1}, {num: {$type: 2}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: [{$alwaysTrue: 1},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 2}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithNoType) {
@@ -264,9 +281,11 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumTrue) {
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$gt: 0}}, {num: {$type: 18}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$gt: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 18}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumFalse) {
@@ -276,9 +295,11 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumFalse)
ASSERT_OK(result.getStatus());
BSONObjBuilder builder;
result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(
- builder.obj(),
- fromjson("{$and: [{$and: [{$and: [{num: {$gte: 0}}, {num: {$type: 18}}]}]}]}"));
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$gte: 0}}]},"
+ "{$or: [{$nor: [{num: {$exists: true}}]}, {num: {$type: 18}}]}"
+ "]}]}]}"));
}
TEST(JSONSchemaParserTest, FailsToParseIfMinimumIsNotANumber) {