diff options
-rw-r--r-- | jstests/core/json_schema.js | 159 | ||||
-rw-r--r-- | src/mongo/bson/bsontypes.h | 12 | ||||
-rw-r--r-- | src/mongo/db/matcher/SConscript | 8 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf.cpp | 28 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf.h | 42 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf_test.cpp | 51 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.cpp | 91 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.h | 11 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser_leaf_test.cpp | 22 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser_test.cpp | 23 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 243 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.h | 61 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser_test.cpp | 180 | ||||
-rw-r--r-- | src/mongo/db/query/index_bounds_builder.cpp | 4 |
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); |