summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/core/json_schema.js159
-rw-r--r--src/mongo/bson/bsontypes.h12
-rw-r--r--src/mongo/db/matcher/SConscript8
-rw-r--r--src/mongo/db/matcher/expression_leaf.cpp28
-rw-r--r--src/mongo/db/matcher/expression_leaf.h42
-rw-r--r--src/mongo/db/matcher/expression_leaf_test.cpp51
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp91
-rw-r--r--src/mongo/db/matcher/expression_parser.h11
-rw-r--r--src/mongo/db/matcher/expression_parser_leaf_test.cpp22
-rw-r--r--src/mongo/db/matcher/expression_parser_test.cpp23
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp243
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.h61
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser_test.cpp180
-rw-r--r--src/mongo/db/query/index_bounds_builder.cpp4
14 files changed, 812 insertions, 123 deletions
diff --git a/jstests/core/json_schema.js b/jstests/core/json_schema.js
new file mode 100644
index 00000000000..8d458734d9a
--- /dev/null
+++ b/jstests/core/json_schema.js
@@ -0,0 +1,159 @@
+(function() {
+ "use strict";
+
+ let coll = db.jstests_json_schema;
+ coll.drop();
+
+ assert.writeOK(coll.insert({_id: 0, num: 3}));
+ assert.writeOK(coll.insert({_id: 1, num: -3}));
+ assert.writeOK(coll.insert({_id: 2, num: NumberInt(2)}));
+ assert.writeOK(coll.insert({_id: 3, num: NumberInt(-2)}));
+ assert.writeOK(coll.insert({_id: 4, num: NumberLong(1)}));
+ assert.writeOK(coll.insert({_id: 5, num: NumberLong(-1)}));
+ assert.writeOK(coll.insert({_id: 6, num: {}}));
+ assert.writeOK(coll.insert({_id: 7, num: "str"}));
+
+ // Test that $jsonSchema fails to parse if its argument is not an object.
+ assert.throws(function() {
+ coll.find({$jsonSchema: "foo"}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: []}).itcount();
+ });
+
+ // Test that $jsonSchema fails to parse if the value for the "type" keyword is not a string.
+ assert.throws(function() {
+ coll.find({$jsonSchema: {type: 3}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {type: {}}}).itcount();
+ });
+
+ // Test that $jsonSchema fails to parse if the value for the properties keyword is not an
+ // object.
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: 3}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: []}}).itcount();
+ });
+
+ // Test that $jsonSchema fails to parse if one of the properties named inside the argument for
+ // the properties keyword is not an object.
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {num: "number"}}}).itcount();
+ });
+
+ // Test that $jsonSchema fails to parse if the value for the maximum keyword is not a number.
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {num: {maximum: "0"}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {num: {maximum: {}}}}}).itcount();
+ });
+
+ // Test that the empty schema matches everything.
+ assert.eq(8, 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());
+
+ // 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}],
+ 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}],
+ 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}],
+ 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}],
+ 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}],
+ 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}],
+ 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}],
+ coll.find({$jsonSchema: {properties: {num: {type: "string", maximum: 0}}}}, {_id: 1})
+ .sort({_id: 1})
+ .toArray());
+
+ coll.drop();
+ assert.writeOK(coll.insert({_id: 0, obj: 3}));
+ assert.writeOK(coll.insert({_id: 1, obj: {f1: {f3: "str"}, f2: "str"}}));
+ assert.writeOK(coll.insert({_id: 2, obj: {f1: "str", f2: "str"}}));
+ assert.writeOK(coll.insert({_id: 3, obj: {f1: 1, f2: "str"}}));
+
+ // Test that properties keyword can be used recursively, and that it does not apply when the
+ // field does not contain on object.
+ assert.eq([{_id: 0}, {_id: 1}],
+ coll.find({
+ $jsonSchema: {
+ properties: {
+ obj: {
+ properties: {
+ f1: {type: "object", properties: {f3: {type: "string"}}},
+ f2: {type: "string"}
+ }
+ }
+ }
+ }
+ },
+ {_id: 1})
+ .sort({_id: 1})
+ .toArray());
+
+ // Test that $jsonSchema can be combined with other operators in the match language.
+ assert.eq(
+ [{_id: 0}, {_id: 1}, {_id: 2}],
+ coll.find({
+ $or: [
+ {"obj.f1": "str"},
+ {
+ $jsonSchema: {
+ properties: {
+ obj: {
+ properties: {
+ f1: {type: "object", properties: {f3: {type: "string"}}},
+ f2: {type: "string"}
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ {_id: 1})
+ .sort({_id: 1})
+ .toArray());
+}());
diff --git a/src/mongo/bson/bsontypes.h b/src/mongo/bson/bsontypes.h
index 0bd9caeff9a..115e2d93aee 100644
--- a/src/mongo/bson/bsontypes.h
+++ b/src/mongo/bson/bsontypes.h
@@ -123,6 +123,18 @@ std::ostream& operator<<(std::ostream& stream, BSONType type);
*/
bool isValidBSONType(int type);
+inline bool isNumericBSONType(BSONType type) {
+ switch (type) {
+ case NumberDouble:
+ case NumberInt:
+ case NumberLong:
+ case NumberDecimal:
+ return true;
+ default:
+ return false;
+ }
+}
+
/* subtypes of BinData.
bdtCustom and above are ones that the JS compiler understands, but are
opaque to the database.
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript
index 54170e44d78..edece7bd350 100644
--- a/src/mongo/db/matcher/SConscript
+++ b/src/mongo/db/matcher/SConscript
@@ -47,9 +47,10 @@ env.Library(
'matcher.cpp',
'schema/expression_internal_schema_num_array_items.cpp',
'schema/expression_internal_schema_object_match.cpp',
+ 'schema/expression_internal_schema_str_length.cpp',
'schema/expression_internal_schema_unique_items.cpp',
'schema/expression_internal_schema_xor.cpp',
- 'schema/expression_internal_schema_str_length.cpp',
+ 'schema/json_schema_parser.cpp',
],
LIBDEPS=[
'$BUILD_DIR/mongo/base',
@@ -70,12 +71,13 @@ env.CppUnitTest(
'expression_test.cpp',
'expression_tree_test.cpp',
'schema/expression_internal_schema_max_items_test.cpp',
+ 'schema/expression_internal_schema_max_length_test.cpp',
'schema/expression_internal_schema_min_items_test.cpp',
+ 'schema/expression_internal_schema_min_length_test.cpp',
'schema/expression_internal_schema_object_match_test.cpp',
'schema/expression_internal_schema_unique_items_test.cpp',
- 'schema/expression_internal_schema_max_length_test.cpp',
- 'schema/expression_internal_schema_min_length_test.cpp',
'schema/expression_internal_schema_xor_test.cpp',
+ 'schema/json_schema_parser_test.cpp',
],
LIBDEPS=[
'$BUILD_DIR/mongo/db/query/collation/collator_interface_mock',
diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp
index b208cd4c00b..482ca061bc8 100644
--- a/src/mongo/db/matcher/expression_leaf.cpp
+++ b/src/mongo/db/matcher/expression_leaf.cpp
@@ -418,27 +418,17 @@ const stdx::unordered_map<std::string, BSONType> TypeMatchExpression::typeAliasM
{typeName(MaxKey), MaxKey},
{typeName(MinKey), MinKey}};
-Status TypeMatchExpression::initWithBSONType(StringData path, int type) {
- if (!isValidBSONType(type)) {
- return Status(ErrorCodes::BadValue,
- str::stream() << "Invalid numerical $type code: " << type);
- }
-
- _type = static_cast<BSONType>(type);
- return setPath(path);
-}
-
-Status TypeMatchExpression::initAsMatchingAllNumbers(StringData path) {
- _matchesAllNumbers = true;
+Status TypeMatchExpression::init(StringData path, Type type) {
+ _type = std::move(type);
return setPath(path);
}
bool TypeMatchExpression::matchesSingleElement(const BSONElement& e) const {
- if (_matchesAllNumbers) {
+ if (_type.allNumbers) {
return e.isNumber();
}
- return e.type() == _type;
+ return e.type() == _type.bsonType;
}
void TypeMatchExpression::debugString(StringBuilder& debug, int level) const {
@@ -447,7 +437,7 @@ void TypeMatchExpression::debugString(StringBuilder& debug, int level) const {
if (matchesAllNumbers()) {
debug << kMatchesAllNumbersAlias;
} else {
- debug << _type;
+ debug << _type.bsonType;
}
MatchExpression::TagData* td = getTag();
@@ -462,7 +452,7 @@ void TypeMatchExpression::serialize(BSONObjBuilder* out) const {
if (matchesAllNumbers()) {
out->append(path(), BSON("$type" << kMatchesAllNumbersAlias));
} else {
- out->append(path(), BSON("$type" << _type));
+ out->append(path(), BSON("$type" << _type.bsonType));
}
}
@@ -476,11 +466,11 @@ bool TypeMatchExpression::equivalent(const MatchExpression* other) const {
return false;
}
- if (_matchesAllNumbers) {
- return realOther->_matchesAllNumbers;
+ if (_type.allNumbers) {
+ return realOther->_type.allNumbers;
}
- return _type == realOther->_type;
+ return _type.bsonType == realOther->_type.bsonType;
}
diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h
index 1caa90014a1..559dcf1f63b 100644
--- a/src/mongo/db/matcher/expression_leaf.h
+++ b/src/mongo/db/matcher/expression_leaf.h
@@ -367,27 +367,25 @@ public:
static const std::string kMatchesAllNumbersAlias;
static const stdx::unordered_map<std::string, BSONType> typeAliasMap;
- TypeMatchExpression() : LeafMatchExpression(TYPE_OPERATOR) {}
-
/**
- * Initialize as matching against a specific BSONType.
- *
- * Returns a non-OK status if 'type' cannot be converted to a valid BSONType.
+ * Represents either a particular BSON type, or the "number" type, which is an alias for all
+ * numeric BSON types.
*/
- Status initWithBSONType(StringData path, int type);
+ struct Type {
+ Type() = default;
+ /* implicit */ Type(BSONType bsonType) : bsonType(bsonType) {}
- /**
- * Initialize as matching against all number types (NumberDouble, NumberLong, and NumberInt).
- */
- Status initAsMatchingAllNumbers(StringData path);
+ bool allNumbers = false;
+ BSONType bsonType = BSONType::EOO;
+ };
+
+ TypeMatchExpression() : LeafMatchExpression(TYPE_OPERATOR) {}
+
+ Status init(StringData path, Type type);
std::unique_ptr<MatchExpression> shallowClone() const override {
std::unique_ptr<TypeMatchExpression> e = stdx::make_unique<TypeMatchExpression>();
- if (_matchesAllNumbers) {
- invariantOK(e->initAsMatchingAllNumbers(path()));
- } else {
- invariantOK(e->initWithBSONType(path(), _type));
- }
+ invariantOK(e->init(path(), _type));
if (getTag()) {
e->setTag(getTag()->clone());
}
@@ -402,10 +400,11 @@ public:
bool equivalent(const MatchExpression* other) const override;
- /**
- * What is the type we're matching against?
- */
- BSONType getType() const {
+ BSONType getBSONType() const {
+ return _type.bsonType;
+ }
+
+ Type getType() const {
return _type;
}
@@ -414,14 +413,13 @@ public:
* Defaults to false. If this is true, _type is EOO.
*/
bool matchesAllNumbers() const {
- return _matchesAllNumbers;
+ return _type.allNumbers;
}
private:
bool _matches(StringData path, const MatchableDocument* doc, MatchDetails* details = 0) const;
- bool _matchesAllNumbers = false;
- BSONType _type = BSONType::EOO;
+ Type _type;
};
/**
diff --git a/src/mongo/db/matcher/expression_leaf_test.cpp b/src/mongo/db/matcher/expression_leaf_test.cpp
index 7ae77f66b2a..67274319ced 100644
--- a/src/mongo/db/matcher/expression_leaf_test.cpp
+++ b/src/mongo/db/matcher/expression_leaf_test.cpp
@@ -1325,7 +1325,7 @@ TEST(TypeMatchExpression, MatchesElementStringType) {
<< "abc");
BSONObj notMatch = BSON("a" << 5);
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("", String).isOK());
+ ASSERT(type.init("", String).isOK());
ASSERT(type.matchesSingleElement(match["a"]));
ASSERT(!type.matchesSingleElement(notMatch["a"]));
}
@@ -1335,7 +1335,7 @@ TEST(TypeMatchExpression, MatchesElementNullType) {
BSONObj notMatch = BSON("a"
<< "abc");
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("", jstNULL).isOK());
+ ASSERT(type.init("", jstNULL).isOK());
ASSERT(type.matchesSingleElement(match["a"]));
ASSERT(!type.matchesSingleElement(notMatch["a"]));
}
@@ -1350,30 +1350,27 @@ TEST(TypeMatchExpression, MatchesElementNumber) {
ASSERT_EQ(BSONType::NumberLong, match2["a"].type());
ASSERT_EQ(BSONType::NumberDouble, match3["a"].type());
- TypeMatchExpression type;
- ASSERT_OK(type.initAsMatchingAllNumbers("a"));
- ASSERT_EQ("a", type.path());
- ASSERT_TRUE(type.matchesSingleElement(match1["a"]));
- ASSERT_TRUE(type.matchesSingleElement(match2["a"]));
- ASSERT_TRUE(type.matchesSingleElement(match3["a"]));
- ASSERT_FALSE(type.matchesSingleElement(notMatch["a"]));
-}
-
-TEST(TypeMatchExpression, InvalidTypeMatchExpressionTypeCode) {
- TypeMatchExpression type;
- ASSERT_NOT_OK(type.initWithBSONType("", JSTypeMax + 1));
+ TypeMatchExpression typeExpr;
+ TypeMatchExpression::Type type;
+ type.allNumbers = true;
+ ASSERT_OK(typeExpr.init("a", type));
+ ASSERT_EQ("a", typeExpr.path());
+ ASSERT_TRUE(typeExpr.matchesSingleElement(match1["a"]));
+ ASSERT_TRUE(typeExpr.matchesSingleElement(match2["a"]));
+ ASSERT_TRUE(typeExpr.matchesSingleElement(match3["a"]));
+ ASSERT_FALSE(typeExpr.matchesSingleElement(notMatch["a"]));
}
TEST(TypeMatchExpression, MatchesScalar) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a", Bool).isOK());
+ ASSERT(type.init("a", Bool).isOK());
ASSERT(type.matchesBSON(BSON("a" << true), NULL));
ASSERT(!type.matchesBSON(BSON("a" << 1), NULL));
}
TEST(TypeMatchExpression, MatchesArray) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a", NumberInt).isOK());
+ ASSERT(type.init("a", NumberInt).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(4)), NULL));
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(4 << "a")), NULL));
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY("a" << 4)), NULL));
@@ -1383,7 +1380,7 @@ TEST(TypeMatchExpression, MatchesArray) {
TEST(TypeMatchExpression, TypeArrayMatchesOuterAndInnerArray) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a", Array).isOK());
+ ASSERT(type.init("a", Array).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSONArray()), nullptr));
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(4 << "a")), nullptr));
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(BSONArray() << 2)), nullptr));
@@ -1394,42 +1391,42 @@ TEST(TypeMatchExpression, TypeArrayMatchesOuterAndInnerArray) {
TEST(TypeMatchExpression, MatchesObject) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a", Object).isOK());
+ ASSERT(type.init("a", Object).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSON("b" << 1)), NULL));
ASSERT(!type.matchesBSON(BSON("a" << 1), NULL));
}
TEST(TypeMatchExpression, MatchesDotNotationFieldObject) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a.b", Object).isOK());
+ ASSERT(type.init("a.b", Object).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSON("b" << BSON("c" << 1))), NULL));
ASSERT(!type.matchesBSON(BSON("a" << BSON("b" << 1)), NULL));
}
TEST(TypeMatchExpression, MatchesDotNotationArrayElementArray) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a.0", Array).isOK());
+ ASSERT(type.init("a.0", Array).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(BSON_ARRAY(1))), NULL));
ASSERT(!type.matchesBSON(BSON("a" << BSON_ARRAY("b")), NULL));
}
TEST(TypeMatchExpression, MatchesDotNotationArrayElementScalar) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a.0", String).isOK());
+ ASSERT(type.init("a.0", String).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY("b")), NULL));
ASSERT(!type.matchesBSON(BSON("a" << BSON_ARRAY(1)), NULL));
}
TEST(TypeMatchExpression, MatchesDotNotationArrayElementObject) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a.0", Object).isOK());
+ ASSERT(type.init("a.0", Object).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(BSON("b" << 1))), NULL));
ASSERT(!type.matchesBSON(BSON("a" << BSON_ARRAY(1)), NULL));
}
TEST(TypeMatchExpression, MatchesNull) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a", jstNULL).isOK());
+ ASSERT(type.init("a", jstNULL).isOK());
ASSERT(type.matchesBSON(BSON("a" << BSONNULL), NULL));
ASSERT(!type.matchesBSON(BSON("a" << 4), NULL));
ASSERT(!type.matchesBSON(BSONObj(), NULL));
@@ -1437,7 +1434,7 @@ TEST(TypeMatchExpression, MatchesNull) {
TEST(TypeMatchExpression, ElemMatchKey) {
TypeMatchExpression type;
- ASSERT(type.initWithBSONType("a.b", String).isOK());
+ ASSERT(type.init("a.b", String).isOK());
MatchDetails details;
details.requestElemMatchKey();
ASSERT(!type.matchesBSON(BSON("a" << 1), &details));
@@ -1459,9 +1456,9 @@ TEST(TypeMatchExpression, Equivalent) {
TypeMatchExpression e1;
TypeMatchExpression e2;
TypeMatchExpression e3;
- e1.initWithBSONType("a", String).transitional_ignore();
- e2.initWithBSONType("a", NumberDouble).transitional_ignore();
- e3.initWithBSONType("b", String).transitional_ignore();
+ ASSERT_OK(e1.init("a", BSONType::String));
+ ASSERT_OK(e2.init("a", BSONType::NumberDouble));
+ ASSERT_OK(e3.init("b", BSONType::String));
ASSERT(e1.equivalent(&e1));
ASSERT(!e1.equivalent(&e2));
diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp
index 14fedfc8018..502305ab995 100644
--- a/src/mongo/db/matcher/expression_parser.cpp
+++ b/src/mongo/db/matcher/expression_parser.cpp
@@ -43,11 +43,11 @@
#include "mongo/db/matcher/schema/expression_internal_schema_object_match.h"
#include "mongo/db/matcher/schema/expression_internal_schema_unique_items.h"
#include "mongo/db/matcher/schema/expression_internal_schema_xor.h"
+#include "mongo/db/matcher/schema/json_schema_parser.h"
#include "mongo/db/namespace_string.h"
#include "mongo/stdx/memory.h"
#include "mongo/util/mongoutils/str.h"
-
namespace {
using namespace mongo;
@@ -423,6 +423,12 @@ StatusWithMatchExpression MatchExpressionParser::_parse(const BSONObj& obj,
if (!s.isOK())
return s;
root->add(xorExpr.release());
+ } else if (mongoutils::str::equals("jsonSchema", rest)) {
+ if (e.type() != BSONType::Object) {
+ return {Status(ErrorCodes::TypeMismatch, "$jsonSchema must be an object")};
+ }
+
+ return JSONSchemaParser::parse(e.Obj());
} else {
return {Status(ErrorCodes::BadValue,
mongoutils::str::stream() << "unknown top level operator: "
@@ -686,53 +692,68 @@ Status MatchExpressionParser::_parseInExpression(InMatchExpression* inExpression
return Status::OK();
}
+StatusWith<std::unique_ptr<TypeMatchExpression>> MatchExpressionParser::parseTypeFromAlias(
+ StringData path, StringData typeAlias) {
+ auto typeExpr = stdx::make_unique<TypeMatchExpression>();
+
+ TypeMatchExpression::Type type;
+
+ if (typeAlias == TypeMatchExpression::kMatchesAllNumbersAlias) {
+ type.allNumbers = true;
+ Status status = typeExpr->init(path, type);
+ if (!status.isOK()) {
+ return status;
+ }
+ return {std::move(typeExpr)};
+ }
+
+ auto it = TypeMatchExpression::typeAliasMap.find(typeAlias.toString());
+ if (it == TypeMatchExpression::typeAliasMap.end()) {
+ return Status(ErrorCodes::BadValue,
+ str::stream() << "Unknown string alias for $type: " << typeAlias);
+ }
+
+ type.bsonType = it->second;
+ Status status = typeExpr->init(path, type);
+ if (!status.isOK()) {
+ return status;
+ }
+ return {std::move(typeExpr)};
+}
+
StatusWithMatchExpression MatchExpressionParser::_parseType(const char* name,
const BSONElement& elt) {
if (!elt.isNumber() && elt.type() != BSONType::String) {
return Status(ErrorCodes::TypeMismatch, "argument to $type is not a number or a string");
}
- std::unique_ptr<TypeMatchExpression> temp = stdx::make_unique<TypeMatchExpression>();
-
- int typeInt;
-
- // The element can be a number (the BSON type number) or a string representing the name
- // of the type.
- if (elt.isNumber()) {
- typeInt = elt.numberInt();
- if (elt.type() != NumberInt && typeInt != elt.number()) {
- typeInt = -1;
+ if (elt.type() == BSONType::String) {
+ auto typeExpr = parseTypeFromAlias(name, elt.valueStringData());
+ if (!typeExpr.isOK()) {
+ return typeExpr.getStatus();
}
- } else {
- invariant(elt.type() == BSONType::String);
- std::string typeAlias = elt.str();
- // If typeAlias is 'number', initialize as matching against all number types.
- if (typeAlias == TypeMatchExpression::kMatchesAllNumbersAlias) {
- Status s = temp->initAsMatchingAllNumbers(name);
- if (!s.isOK()) {
- return s;
- }
- return {std::move(temp)};
- }
+ return {std::move(typeExpr.getValue())};
+ }
- // Search the string-int map for the typeAlias (case-sensitive).
- stdx::unordered_map<std::string, BSONType>::const_iterator it =
- TypeMatchExpression::typeAliasMap.find(typeAlias);
- if (it == TypeMatchExpression::typeAliasMap.end()) {
- std::stringstream ss;
- ss << "unknown string alias for $type: " << typeAlias;
- return Status(ErrorCodes::BadValue, ss.str());
- }
- typeInt = it->second;
+ invariant(elt.isNumber());
+ int typeInt = elt.numberInt();
+ if (elt.type() != BSONType::NumberInt && typeInt != elt.number()) {
+ typeInt = -1;
}
- Status s = temp->initWithBSONType(name, typeInt);
- if (!s.isOK()) {
- return s;
+ if (!isValidBSONType(typeInt)) {
+ return Status(ErrorCodes::BadValue,
+ str::stream() << "Invalid numerical $type code: " << typeInt);
}
- return {std::move(temp)};
+ auto typeExpr = stdx::make_unique<TypeMatchExpression>();
+ auto status = typeExpr->init(name, static_cast<BSONType>(typeInt));
+ if (!status.isOK()) {
+ return status;
+ }
+
+ return {std::move(typeExpr)};
}
StatusWithMatchExpression MatchExpressionParser::_parseElemMatch(const char* name,
diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h
index 630f556e020..b6a7f9c21c6 100644
--- a/src/mongo/db/matcher/expression_parser.h
+++ b/src/mongo/db/matcher/expression_parser.h
@@ -82,6 +82,13 @@ public:
*/
static StatusWith<long long> parseIntegerElementToLong(BSONElement elem);
+ /**
+ * Given a path over which to match, and a type alias (e.g. "long", "number", or "object"),
+ * returns the corresponding $type match expression node.
+ */
+ static StatusWith<std::unique_ptr<TypeMatchExpression>> parseTypeFromAlias(
+ StringData path, StringData typeAlias);
+
private:
MatchExpressionParser(const ExtensionsCallback* extensionsCallback)
: _extensionsCallback(extensionsCallback) {}
@@ -203,10 +210,6 @@ private:
StatusWithMatchExpression _parseInternalSchemaSingleIntegerArgument(
const char* name, const BSONElement& elem) const;
-
- // The maximum allowed depth of a query tree. Just to guard against stack overflow.
- static const int kMaximumTreeDepth;
-
// Performs parsing for the match extensions. We do not own this pointer - it has to live
// as long as the parser is active.
const ExtensionsCallback* _extensionsCallback;
diff --git a/src/mongo/db/matcher/expression_parser_leaf_test.cpp b/src/mongo/db/matcher/expression_parser_leaf_test.cpp
index c23e4294360..c9bae20aac7 100644
--- a/src/mongo/db/matcher/expression_parser_leaf_test.cpp
+++ b/src/mongo/db/matcher/expression_parser_leaf_test.cpp
@@ -1106,7 +1106,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameDouble) {
ASSERT_OK(typeNumberDouble.getStatus());
TypeMatchExpression* tmeNumberDouble =
static_cast<TypeMatchExpression*>(typeNumberDouble.getValue().get());
- ASSERT(tmeNumberDouble->getType() == NumberDouble);
+ ASSERT_EQ(tmeNumberDouble->getBSONType(), BSONType::NumberDouble);
ASSERT_TRUE(tmeNumberDouble->matchesBSON(fromjson("{a: 5.4}")));
ASSERT_FALSE(tmeNumberDouble->matchesBSON(fromjson("{a: NumberInt(5)}")));
}
@@ -1118,7 +1118,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringNameNumberDecimal) {
ASSERT_OK(typeNumberDecimal.getStatus());
TypeMatchExpression* tmeNumberDecimal =
static_cast<TypeMatchExpression*>(typeNumberDecimal.getValue().get());
- ASSERT(tmeNumberDecimal->getType() == NumberDecimal);
+ ASSERT_EQ(tmeNumberDecimal->getBSONType(), BSONType::NumberDecimal);
ASSERT_TRUE(tmeNumberDecimal->matchesBSON(BSON("a" << mongo::Decimal128("1"))));
ASSERT_FALSE(tmeNumberDecimal->matchesBSON(fromjson("{a: true}")));
}
@@ -1130,7 +1130,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameNumberInt) {
ASSERT_OK(typeNumberInt.getStatus());
TypeMatchExpression* tmeNumberInt =
static_cast<TypeMatchExpression*>(typeNumberInt.getValue().get());
- ASSERT(tmeNumberInt->getType() == NumberInt);
+ ASSERT_EQ(tmeNumberInt->getBSONType(), BSONType::NumberInt);
ASSERT_TRUE(tmeNumberInt->matchesBSON(fromjson("{a: NumberInt(5)}")));
ASSERT_FALSE(tmeNumberInt->matchesBSON(fromjson("{a: 5.4}")));
}
@@ -1142,7 +1142,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameNumberLong) {
ASSERT_OK(typeNumberLong.getStatus());
TypeMatchExpression* tmeNumberLong =
static_cast<TypeMatchExpression*>(typeNumberLong.getValue().get());
- ASSERT(tmeNumberLong->getType() == NumberLong);
+ ASSERT_EQ(tmeNumberLong->getBSONType(), BSONType::NumberLong);
ASSERT_TRUE(tmeNumberLong->matchesBSON(BSON("a" << -1LL)));
ASSERT_FALSE(tmeNumberLong->matchesBSON(fromjson("{a: true}")));
}
@@ -1153,7 +1153,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameString) {
fromjson("{a: {$type: 'string'}}"), ExtensionsCallbackDisallowExtensions(), collator);
ASSERT_OK(typeString.getStatus());
TypeMatchExpression* tmeString = static_cast<TypeMatchExpression*>(typeString.getValue().get());
- ASSERT(tmeString->getType() == String);
+ ASSERT_EQ(tmeString->getBSONType(), BSONType::String);
ASSERT_TRUE(tmeString->matchesBSON(fromjson("{a: 'hello world'}")));
ASSERT_FALSE(tmeString->matchesBSON(fromjson("{a: 5.4}")));
}
@@ -1164,7 +1164,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnamejstOID) {
fromjson("{a: {$type: 'objectId'}}"), ExtensionsCallbackDisallowExtensions(), collator);
ASSERT_OK(typejstOID.getStatus());
TypeMatchExpression* tmejstOID = static_cast<TypeMatchExpression*>(typejstOID.getValue().get());
- ASSERT(tmejstOID->getType() == jstOID);
+ ASSERT_EQ(tmejstOID->getBSONType(), BSONType::jstOID);
ASSERT_TRUE(tmejstOID->matchesBSON(fromjson("{a: ObjectId('000000000000000000000000')}")));
ASSERT_FALSE(tmejstOID->matchesBSON(fromjson("{a: 'hello world'}")));
}
@@ -1176,7 +1176,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnamejstNULL) {
ASSERT_OK(typejstNULL.getStatus());
TypeMatchExpression* tmejstNULL =
static_cast<TypeMatchExpression*>(typejstNULL.getValue().get());
- ASSERT(tmejstNULL->getType() == jstNULL);
+ ASSERT_EQ(tmejstNULL->getBSONType(), BSONType::jstNULL);
ASSERT_TRUE(tmejstNULL->matchesBSON(fromjson("{a: null}")));
ASSERT_FALSE(tmejstNULL->matchesBSON(fromjson("{a: true}")));
}
@@ -1187,7 +1187,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameBool) {
fromjson("{a: {$type: 'bool'}}"), ExtensionsCallbackDisallowExtensions(), collator);
ASSERT_OK(typeBool.getStatus());
TypeMatchExpression* tmeBool = static_cast<TypeMatchExpression*>(typeBool.getValue().get());
- ASSERT(tmeBool->getType() == Bool);
+ ASSERT_EQ(tmeBool->getBSONType(), BSONType::Bool);
ASSERT_TRUE(tmeBool->matchesBSON(fromjson("{a: true}")));
ASSERT_FALSE(tmeBool->matchesBSON(fromjson("{a: null}")));
}
@@ -1198,7 +1198,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameObject) {
fromjson("{a: {$type: 'object'}}"), ExtensionsCallbackDisallowExtensions(), collator);
ASSERT_OK(typeObject.getStatus());
TypeMatchExpression* tmeObject = static_cast<TypeMatchExpression*>(typeObject.getValue().get());
- ASSERT(tmeObject->getType() == Object);
+ ASSERT_EQ(tmeObject->getBSONType(), BSONType::Object);
ASSERT_TRUE(tmeObject->matchesBSON(fromjson("{a: {}}")));
ASSERT_FALSE(tmeObject->matchesBSON(fromjson("{a: []}")));
}
@@ -1209,7 +1209,7 @@ TEST(MatchExpressionParserLeafTest, TypeStringnameArray) {
fromjson("{a: {$type: 'array'}}"), ExtensionsCallbackDisallowExtensions(), collator);
ASSERT_OK(typeArray.getStatus());
TypeMatchExpression* tmeArray = static_cast<TypeMatchExpression*>(typeArray.getValue().get());
- ASSERT(tmeArray->getType() == Array);
+ ASSERT_EQ(tmeArray->getBSONType(), BSONType::Array);
ASSERT_TRUE(tmeArray->matchesBSON(fromjson("{a: [[]]}")));
ASSERT_FALSE(tmeArray->matchesBSON(fromjson("{a: {}}")));
}
@@ -1260,7 +1260,7 @@ TEST(MatchExpressionParserLeafTest, ValidTypeCodesParseSuccessfully) {
predicate, ExtensionsCallbackDisallowExtensions(), collator);
ASSERT_OK(expression.getStatus());
auto typeExpression = static_cast<TypeMatchExpression*>(expression.getValue().get());
- ASSERT_EQ(type, typeExpression->getType());
+ ASSERT_EQ(type, typeExpression->getBSONType());
}
}
diff --git a/src/mongo/db/matcher/expression_parser_test.cpp b/src/mongo/db/matcher/expression_parser_test.cpp
index 16b66cce0f7..bc047818622 100644
--- a/src/mongo/db/matcher/expression_parser_test.cpp
+++ b/src/mongo/db/matcher/expression_parser_test.cpp
@@ -192,6 +192,29 @@ TEST(MatchExpressionParserTest, ParseIntegerElementToLongAcceptsThree) {
ASSERT_EQ(result.getValue(), 3LL);
}
+TEST(MatchExpressionParserTest, ParseTypeFromAliasCanParseNumberAlias) {
+ auto result = MatchExpressionParser::parseTypeFromAlias("a", "number");
+ ASSERT_OK(result.getStatus());
+ ASSERT_EQ(result.getValue()->path(), "a");
+ ASSERT_TRUE(result.getValue()->getType().allNumbers);
+ ASSERT_TRUE(result.getValue()->matchesAllNumbers());
+}
+
+TEST(MatchExpressionParserTest, ParseTypeFromAliasCanParseLongAlias) {
+ auto result = MatchExpressionParser::parseTypeFromAlias("a", "long");
+ ASSERT_OK(result.getStatus());
+ ASSERT_EQ(result.getValue()->path(), "a");
+ ASSERT_FALSE(result.getValue()->getType().allNumbers);
+ ASSERT_FALSE(result.getValue()->matchesAllNumbers());
+ ASSERT_EQ(result.getValue()->getType().bsonType, BSONType::NumberLong);
+ ASSERT_EQ(result.getValue()->getBSONType(), BSONType::NumberLong);
+}
+
+TEST(MatchExpressionParserTest, ParseTypeFromAliasFailsToParseUnknownAlias) {
+ auto result = MatchExpressionParser::parseTypeFromAlias("a", "unknown");
+ ASSERT_NOT_OK(result.getStatus());
+}
+
StatusWith<int> fib(int n) {
if (n < 0)
return StatusWith<int>(ErrorCodes::BadValue, "paramter to fib has to be >= 0");
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp
new file mode 100644
index 00000000000..c8683667509
--- /dev/null
+++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp
@@ -0,0 +1,243 @@
+/**
+ * Copyright (C) 2017 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/matcher/schema/json_schema_parser.h"
+
+#include "mongo/bson/bsontypes.h"
+#include "mongo/db/matcher/expression_parser.h"
+#include "mongo/db/matcher/schema/expression_internal_schema_object_match.h"
+#include "mongo/stdx/memory.h"
+#include "mongo/util/string_map.h"
+
+namespace mongo {
+
+namespace {
+// JSON Schema keyword constants.
+constexpr StringData kSchemaMaximumKeyword = "maximum"_sd;
+constexpr StringData kSchemaPropertiesKeyword = "properties"_sd;
+constexpr StringData kSchemaTypeKeyword = "type"_sd;
+
+/**
+ * Constructs and returns a match expression to evaluate a JSON Schema restriction keyword.
+ *
+ * This handles semantic differences between the MongoDB query language and JSON Schema. MongoDB
+ * match expressions which apply to a particular type will reject non-matching types, whereas JSON
+ * Schema restriction keywords allow non-matching types. As an example, consider the maxItems
+ * keyword. This keyword only applies in JSON Schema if the type is an array, whereas the
+ * $_internalSchemaMaxItems match expression node rejects non-arrays.
+ *
+ * The 'restrictionType' expresses the type to which the JSON Schema restriction applies (e.g.
+ * arrays for maxItems). The 'restrictionExpr' is the match expression node which can be used to
+ * enforce this restriction, should the types match (e.g. $_internalSchemaMaxItems). 'statedType' is
+ * a parsed representation of the JSON Schema type keyword which is in effect.
+ */
+std::unique_ptr<MatchExpression> makeRestriction(TypeMatchExpression::Type restrictionType,
+ std::unique_ptr<MatchExpression> restrictionExpr,
+ TypeMatchExpression* statedType) {
+ if (statedType) {
+ const bool bothNumeric = restrictionType.allNumbers &&
+ (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 {
+ // This restriction doesn't take any effect, since the type of the schema is different
+ // from the type to which this retriction applies.
+ //
+ // TODO SERVER-30028: Make this use an explicit "always matches" expression.
+ return stdx::make_unique<AndMatchExpression>();
+ }
+ }
+
+ invariant(!statedType);
+
+ auto typeExprForNot = stdx::make_unique<TypeMatchExpression>();
+ invariantOK(typeExprForNot->init(restrictionExpr->path(), restrictionType));
+
+ auto notExpr = stdx::make_unique<NotMatchExpression>(typeExprForNot.release());
+ auto orExpr = stdx::make_unique<OrMatchExpression>();
+ orExpr->add(notExpr.release());
+ orExpr->add(restrictionExpr.release());
+
+ return std::move(orExpr);
+}
+
+StatusWith<std::unique_ptr<TypeMatchExpression>> parseType(StringData path, BSONElement typeElt) {
+ if (!typeElt) {
+ return {nullptr};
+ }
+
+ if (typeElt.type() != BSONType::String) {
+ return {Status(ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaTypeKeyword
+ << "' must be a string")};
+ }
+
+ return MatchExpressionParser::parseTypeFromAlias(path, typeElt.valueStringData());
+}
+
+StatusWithMatchExpression parseMaximum(StringData path,
+ BSONElement maximum,
+ TypeMatchExpression* typeExpr) {
+ if (!maximum.isNumber()) {
+ return {Status(ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaMaximumKeyword
+ << "' must be a number")};
+ }
+
+ if (path.empty()) {
+ // This restriction has no affect in a top-level schema, since we only store objects.
+ //
+ // TODO SERVER-30028: Make this use an explicit "always matches" expression.
+ return {stdx::make_unique<AndMatchExpression>()};
+ }
+
+ auto lteExpr = stdx::make_unique<LTEMatchExpression>();
+ auto status = lteExpr->init(path, maximum);
+ if (!status.isOK()) {
+ return status;
+ }
+
+ // We use Number as a stand-in for all numeric restrictions.
+ TypeMatchExpression::Type restrictionType;
+ restrictionType.allNumbers = true;
+ return makeRestriction(restrictionType, std::move(lteExpr), typeExpr);
+}
+
+} // namespace
+
+StatusWithMatchExpression JSONSchemaParser::_parseProperties(StringData path,
+ BSONElement propertiesElt,
+ TypeMatchExpression* typeExpr) {
+ if (propertiesElt.type() != BSONType::Object) {
+ return {Status(ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaPropertiesKeyword
+ << "' must be an object")};
+ }
+ auto propertiesObj = propertiesElt.embeddedObject();
+
+ auto andExpr = stdx::make_unique<AndMatchExpression>();
+ for (auto&& property : propertiesObj) {
+ if (property.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "Nested schema for $jsonSchema property '"
+ << property.fieldNameStringData()
+ << "' must be an object"};
+ }
+
+ auto nestedSchemaMatch = _parse(property.fieldNameStringData(), property.embeddedObject());
+ if (!nestedSchemaMatch.isOK()) {
+ return nestedSchemaMatch.getStatus();
+ }
+ andExpr->add(nestedSchemaMatch.getValue().release());
+ }
+
+ // If this is a top-level schema, then we have no path and there is no need for an
+ // explicit object match node.
+ if (path.empty()) {
+ return {std::move(andExpr)};
+ }
+
+ auto objectMatch = stdx::make_unique<InternalSchemaObjectMatchExpression>();
+ auto objectMatchStatus = objectMatch->init(std::move(andExpr), path);
+ if (!objectMatchStatus.isOK()) {
+ return objectMatchStatus;
+ }
+
+ return makeRestriction(BSONType::Object, std::move(objectMatch), typeExpr);
+}
+
+StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj schema) {
+ // Map from JSON Schema keyword to the corresponding element from 'schema', or to an empty
+ // BSONElement if the JSON Schema keyword is not specified.
+ StringMap<BSONElement> keywordMap{
+ {kSchemaTypeKeyword, {}}, {kSchemaPropertiesKeyword, {}}, {kSchemaMaximumKeyword, {}}};
+
+ for (auto&& elt : schema) {
+ auto it = keywordMap.find(elt.fieldNameStringData());
+ if (it == keywordMap.end()) {
+ return Status(ErrorCodes::FailedToParse,
+ str::stream() << "Unknown $jsonSchema keyword: "
+ << elt.fieldNameStringData());
+ }
+
+ if (it->second) {
+ return Status(ErrorCodes::FailedToParse,
+ str::stream() << "Duplicate $jsonSchema keyword: "
+ << elt.fieldNameStringData());
+ }
+
+ keywordMap[elt.fieldNameStringData()] = elt;
+ }
+
+ auto typeExpr = parseType(path, keywordMap[kSchemaTypeKeyword]);
+ if (!typeExpr.isOK()) {
+ return typeExpr.getStatus();
+ }
+
+ auto andExpr = stdx::make_unique<AndMatchExpression>();
+
+ if (auto propertiesElt = keywordMap[kSchemaPropertiesKeyword]) {
+ auto propertiesExpr = _parseProperties(path, propertiesElt, typeExpr.getValue().get());
+ if (!propertiesExpr.isOK()) {
+ return propertiesExpr;
+ }
+ andExpr->add(propertiesExpr.getValue().release());
+ }
+
+ if (auto maximumElt = keywordMap[kSchemaMaximumKeyword]) {
+ auto maxExpr = parseMaximum(path, maximumElt, typeExpr.getValue().get());
+ if (!maxExpr.isOK()) {
+ return maxExpr;
+ }
+ andExpr->add(maxExpr.getValue().release());
+ }
+
+ if (path.empty() && typeExpr.getValue() &&
+ typeExpr.getValue()->getBSONType() != BSONType::Object) {
+ // This is a top-level schema which requires that the type is something other than
+ // "object". Since we only know how to store objects, this schema matches nothing.
+ return {stdx::make_unique<FalseMatchExpression>(StringData{})};
+ }
+
+ if (!path.empty() && typeExpr.getValue()) {
+ andExpr->add(typeExpr.getValue().release());
+ }
+ return {std::move(andExpr)};
+}
+
+StatusWithMatchExpression JSONSchemaParser::parse(BSONObj schema) {
+ return _parse(StringData{}, schema);
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.h b/src/mongo/db/matcher/schema/json_schema_parser.h
new file mode 100644
index 00000000000..aa123efb0e3
--- /dev/null
+++ b/src/mongo/db/matcher/schema/json_schema_parser.h
@@ -0,0 +1,61 @@
+/**
+ * Copyright (C) 2017 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include "mongo/db/matcher/expression.h"
+#include "mongo/db/matcher/expression_leaf.h"
+#include "mongo/db/matcher/expression_tree.h"
+
+namespace mongo {
+
+class JSONSchemaParser {
+public:
+ /**
+ * Converts a JSON schema, represented as BSON, into a semantically equivalent match expression
+ * tree. Returns a non-OK status if the schema is invalid or cannot be parsed.
+ */
+ static StatusWithMatchExpression parse(BSONObj schema);
+
+private:
+ // Parses 'schema' to the semantically equivalent match expression. If the schema has an
+ // associated path, e.g. if we are parsing the nested schema for property "myProp" in
+ //
+ // {properties: {myProp: <nested-schema>}}
+ //
+ // then this is passed in 'path'. In this example, the value of 'path' is "myProp". If there is
+ // no path, e.g. for top-level schemas, then 'path' is empty.
+ static StatusWithMatchExpression _parse(StringData path, BSONObj schema);
+
+ // Parser for the JSON Schema 'properties' keyword.
+ static StatusWithMatchExpression _parseProperties(StringData path,
+ BSONElement propertiesElt,
+ TypeMatchExpression* typeExpr);
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
new file mode 100644
index 00000000000..ba55f40526b
--- /dev/null
+++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
@@ -0,0 +1,180 @@
+/**
+ * Copyright (C) 2017 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/bson/json.h"
+#include "mongo/db/matcher/schema/json_schema_parser.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo {
+namespace {
+
+TEST(JSONSchemaParserTest, FailsToParseIfTypeIsNotAString) {
+ BSONObj schema = fromjson("{type: 1}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseUnknownKeyword) {
+ BSONObj schema = fromjson("{unknown: 1}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfPropertiesIsNotAnObject) {
+ BSONObj schema = fromjson("{properties: 1}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfPropertiesIsNotAnObjectWithType) {
+ BSONObj schema = fromjson("{type: 'string', properties: 1}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfParticularPropertyIsNotAnObject) {
+ BSONObj schema = fromjson("{properties: {foo: 1}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfMaximumIsNotANumber) {
+ BSONObj schema = fromjson("{maximum: 'foo'}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfKeywordIsDuplicated) {
+ BSONObj schema = BSON("type"
+ << "object"
+ << "type"
+ << "object");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse);
+}
+
+TEST(JSONSchemaParserTest, EmptySchemaTranslatesCorrectly) {
+ BSONObj schema = fromjson("{}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ ASSERT_BSONOBJ_EQ(builder.obj(), fromjson("{}"));
+}
+
+TEST(JSONSchemaParserTest, TypeObjectTranslatesCorrectly) {
+ BSONObj schema = fromjson("{type: 'object'}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ ASSERT_BSONOBJ_EQ(builder.obj(), fromjson("{}"));
+}
+
+TEST(JSONSchemaParserTest, NestedTypeObjectTranslatesCorrectly) {
+ BSONObj schema =
+ fromjson("{properties: {a: {type: 'object', properties: {b: {type: 'string'}}}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ ASSERT_BSONOBJ_EQ(
+ builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{a: {$_internalSchemaObjectMatch: {$and: [{$and:[{b: {$type:2}}]}]}}},"
+ "{a: {$type: 3}}]}]}]}"));
+}
+
+TEST(JSONSchemaParserTest, TopLevelNonObjectTypeTranslatesCorrectly) {
+ BSONObj schema = fromjson("{type: 'string'}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ // TODO SERVER-30028: Serialize to a new internal "always false" expression.
+ ASSERT_BSONOBJ_EQ(builder.obj(), fromjson("{'': {$all: []}}"));
+}
+
+TEST(JSONSchemaParserTest, TypeNumberTranslatesCorrectly) {
+ BSONObj schema = fromjson("{properties: {num: {type: 'number'}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: [{num: {$type: 'number'}}]}]}]}"));
+}
+
+TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeNumber) {
+ BSONObj schema = fromjson("{properties: {num: {type: 'number', maximum: 0}}, type: 'object'}");
+ auto result = JSONSchemaParser::parse(schema);
+ 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'}}]}]}]}"));
+}
+
+TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeLong) {
+ BSONObj schema = fromjson("{properties: {num: {type: 'long', maximum: 0}}, type: 'object'}");
+ auto result = JSONSchemaParser::parse(schema);
+ 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}}]}]}]}"));
+}
+
+TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeString) {
+ BSONObj schema = fromjson("{properties: {num: {type: 'string', maximum: 0}}, type: 'object'}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ ASSERT_BSONOBJ_EQ(builder.obj(),
+ fromjson("{$and: [{$and: [{$and: [{}, {num: {$type: 2}}]}]}]}"));
+}
+
+TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithNoType) {
+ BSONObj schema = fromjson("{properties: {num: {maximum: 0}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ BSONObjBuilder builder;
+ result.getValue()->serialize(&builder);
+ ASSERT_BSONOBJ_EQ(
+ builder.obj(),
+ fromjson("{$and: [{$and: [{$and: ["
+ "{$or: [{$nor: [{num: {$type: 'number'}}]}, {num: {$lte: 0}}]}]}]}]}"));
+}
+
+} // namespace
+} // namespace mongo
diff --git a/src/mongo/db/query/index_bounds_builder.cpp b/src/mongo/db/query/index_bounds_builder.cpp
index 6589fd81c0a..c1983a60124 100644
--- a/src/mongo/db/query/index_bounds_builder.cpp
+++ b/src/mongo/db/query/index_bounds_builder.cpp
@@ -520,7 +520,7 @@ void IndexBoundsBuilder::translate(const MatchExpression* expr,
} else if (MatchExpression::TYPE_OPERATOR == expr->matchType()) {
const TypeMatchExpression* tme = static_cast<const TypeMatchExpression*>(expr);
- if (tme->getType() == BSONType::Array) {
+ if (tme->getBSONType() == BSONType::Array) {
// We have $type:"array". Since arrays are indexed by creating a key for each element,
// we have to fetch all indexed documents and check whether the full document contains
// an array.
@@ -531,7 +531,7 @@ void IndexBoundsBuilder::translate(const MatchExpression* expr,
// If we are matching all numbers, we just use the bounds for NumberInt, as these bounds
// also include all NumberDouble and NumberLong values.
- BSONType type = tme->matchesAllNumbers() ? BSONType::NumberInt : tme->getType();
+ BSONType type = tme->matchesAllNumbers() ? BSONType::NumberInt : tme->getBSONType();
BSONObjBuilder bob;
bob.appendMinForType("", type);
bob.appendMaxForType("", type);