diff options
author | Anne Lim <anne.lim@mongodb.com> | 2017-07-26 13:33:22 -0400 |
---|---|---|
committer | Anne Lim <anne.lim@mongodb.com> | 2017-08-04 09:27:40 -0400 |
commit | 6ac711a04e3aeff10ae551074812c862aa89e906 (patch) | |
tree | 93f53fcb7321ad7c823f4407b5a2cf396b126f78 /src/mongo/db | |
parent | 41221453c7bfb54ac35b3390887d9a9ae45013de (diff) | |
download | mongo-6ac711a04e3aeff10ae551074812c862aa89e906.tar.gz |
SERVER-30175: Extend the JSON Schema parser to handle multipleOf keyword
Diffstat (limited to 'src/mongo/db')
-rw-r--r-- | src/mongo/db/matcher/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression.h | 1 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.cpp | 40 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.h | 3 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_serialization_test.cpp | 18 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp | 101 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/expression_internal_schema_fmod.h | 74 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp | 124 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 41 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser_test.cpp | 33 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_match.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_match_test.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache.cpp | 3 |
13 files changed, 442 insertions, 1 deletions
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index 70bde1c209e..e60a52c7ff1 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -48,6 +48,7 @@ env.Library( 'matchable.cpp', 'matcher.cpp', 'schema/expression_internal_schema_cond.cpp', + 'schema/expression_internal_schema_fmod.cpp', 'schema/expression_internal_schema_num_array_items.cpp', 'schema/expression_internal_schema_object_match.cpp', 'schema/expression_internal_schema_str_length.cpp', @@ -82,6 +83,7 @@ env.CppUnitTest( 'expression_with_placeholder_test.cpp', 'path_accepting_keyword_test.cpp', 'schema/expression_internal_schema_cond_test.cpp', + 'schema/expression_internal_schema_fmod_test.cpp', 'schema/expression_internal_schema_max_items_test.cpp', 'schema/expression_internal_schema_max_properties_test.cpp', 'schema/expression_internal_schema_max_length_test.cpp', diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h index f0c1d1c4184..1a36b406954 100644 --- a/src/mongo/db/matcher/expression.h +++ b/src/mongo/db/matcher/expression.h @@ -100,6 +100,7 @@ public: // JSON Schema expressions. INTERNAL_SCHEMA_COND, + INTERNAL_SCHEMA_FMOD, INTERNAL_SCHEMA_MAX_ITEMS, INTERNAL_SCHEMA_MIN_ITEMS, INTERNAL_SCHEMA_MAX_PROPERTIES, diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index 320054d2afe..748c080b60c 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -40,6 +40,7 @@ #include "mongo/db/matcher/expression_leaf.h" #include "mongo/db/matcher/expression_tree.h" #include "mongo/db/matcher/schema/expression_internal_schema_cond.h" +#include "mongo/db/matcher/schema/expression_internal_schema_fmod.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_length.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_properties.h" @@ -296,6 +297,9 @@ StatusWithMatchExpression MatchExpressionParser::_parseSubField(const BSONObj& c return _parseBitTest<BitsAnyClearMatchExpression>(name, e); } + case PathAcceptingKeyword::INTERNAL_SCHEMA_FMOD: + return _parseInternalSchemaFmod(name, e); + case PathAcceptingKeyword::INTERNAL_SCHEMA_MIN_ITEMS: { return _parseInternalSchemaSingleIntegerArgument<InternalSchemaMinItemsMatchExpression>( name, e); @@ -1133,6 +1137,41 @@ StatusWith<long long> MatchExpressionParser::parseIntegerElementToLong(BSONEleme return number; } +StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaFmod(const char* name, + const BSONElement& elem) { + StringData path(name); + if (elem.type() != Array) + return {ErrorCodes::BadValue, + str::stream() << path << " must be an array, but got type " << elem.type()}; + + BSONObjIterator i(elem.embeddedObject()); + + if (!i.more()) + return {ErrorCodes::BadValue, str::stream() << path << " does not have enough elements"}; + BSONElement d = i.next(); + if (!d.isNumber()) + return {ErrorCodes::TypeMismatch, + str::stream() << path << " does not have a numeric divisor"}; + + if (!i.more()) + return {ErrorCodes::BadValue, str::stream() << path << " does not have enough elements"}; + BSONElement r = i.next(); + if (!d.isNumber()) + return {ErrorCodes::TypeMismatch, + str::stream() << path << " does not have a numeric remainder"}; + + if (i.more()) + return {ErrorCodes::BadValue, str::stream() << path << " has too many elements"}; + + std::unique_ptr<InternalSchemaFmodMatchExpression> result = + stdx::make_unique<InternalSchemaFmodMatchExpression>(); + Status s = result->init(name, d.numberDecimal(), r.numberDecimal()); + if (!s.isOK()) + return s; + return {std::move(result)}; +} + + template <class T> StatusWithMatchExpression MatchExpressionParser::_parseInternalSchemaFixedArityArgument( StringData name, const BSONElement& input, const CollatorInterface* collator) { @@ -1273,6 +1312,7 @@ MONGO_INITIALIZER(MatchExpressionParser)(InitializerContext* context) { {"bitsAllClear", PathAcceptingKeyword::BITS_ALL_CLEAR}, {"bitsAnySet", PathAcceptingKeyword::BITS_ANY_SET}, {"bitsAnyClear", PathAcceptingKeyword::BITS_ANY_CLEAR}, + {"_internalSchemaFmod", PathAcceptingKeyword::INTERNAL_SCHEMA_FMOD}, {"_internalSchemaMinItems", PathAcceptingKeyword::INTERNAL_SCHEMA_MIN_ITEMS}, {"_internalSchemaMaxItems", PathAcceptingKeyword::INTERNAL_SCHEMA_MAX_ITEMS}, {"_internalSchemaUniqueItems", PathAcceptingKeyword::INTERNAL_SCHEMA_UNIQUE_ITEMS}, diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h index 615066b3e22..475b5f6e7b4 100644 --- a/src/mongo/db/matcher/expression_parser.h +++ b/src/mongo/db/matcher/expression_parser.h @@ -67,6 +67,7 @@ enum class PathAcceptingKeyword { BITS_ALL_CLEAR, BITS_ANY_SET, BITS_ANY_CLEAR, + INTERNAL_SCHEMA_FMOD, INTERNAL_SCHEMA_MIN_ITEMS, INTERNAL_SCHEMA_MAX_ITEMS, INTERNAL_SCHEMA_UNIQUE_ITEMS, @@ -245,6 +246,8 @@ private: */ StatusWith<std::vector<uint32_t>> _parseBitPositionsArray(const BSONObj& theArray); + StatusWithMatchExpression _parseInternalSchemaFmod(const char* name, const BSONElement& e); + /** * Parses a MatchExpression which takes a fixed-size array of MatchExpressions as arguments. */ diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp index 6e6b85bb259..428bf0a291f 100644 --- a/src/mongo/db/matcher/expression_serialization_test.cpp +++ b/src/mongo/db/matcher/expression_serialization_test.cpp @@ -1073,5 +1073,23 @@ TEST(SerializeInternalSchema, ExpressionInternalSchemaMaxPropertiesSerializesCor ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); } +TEST(SerializeInternalSchema, ExpressionInternalSchemaFmodSerializesCorrectly) { + Matcher original( + fromjson("{a: {$_internalSchemaFmod: [NumberDecimal('2.3'), NumberDecimal('1.1')]}}"), + ExtensionsCallbackNoop(), + kSimpleCollator); + Matcher reserialized( + serialize(original.getMatchExpression()), ExtensionsCallbackNoop(), kSimpleCollator); + + ASSERT_BSONOBJ_EQ( + *reserialized.getQuery(), + fromjson("{a: {$_internalSchemaFmod: [NumberDecimal('2.3'), NumberDecimal('1.1')]}}")); + ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); + BSONObj obj = fromjson("{a: NumberDecimal('1.1')}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); + obj = fromjson("{a: NumberDecimal('2.3')}"); + ASSERT_EQ(original.matches(obj), reserialized.matches(obj)); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp new file mode 100644 index 00000000000..dcbe8c2b93b --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2017 10gen 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/expression_internal_schema_fmod.h" + +#include "mongo/bson/bsonmisc.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" + +namespace mongo { + +Status InternalSchemaFmodMatchExpression::init(StringData path, + Decimal128 divisor, + Decimal128 remainder) { + if (divisor.isZero()) { + return Status(ErrorCodes::BadValue, "divisor cannot be 0"); + } + if (divisor.isNaN()) { + return Status(ErrorCodes::BadValue, "divisor cannot be NaN"); + } + if (divisor.isInfinite()) { + return Status(ErrorCodes::BadValue, "divisor cannot be infinite"); + } + _divisor = divisor; + _remainder = remainder; + return setPath(path); +} + +bool InternalSchemaFmodMatchExpression::matchesSingleElement(const BSONElement& e, + MatchDetails* details) const { + if (!e.isNumber()) { + return false; + } + std::uint32_t flags = Decimal128::SignalingFlag::kNoFlag; + Decimal128 result = e.numberDecimal().modulo(_divisor, &flags); + if (flags == Decimal128::SignalingFlag::kNoFlag) { + return result.isEqual(_remainder); + } + return false; +} + +void InternalSchemaFmodMatchExpression::debugString(StringBuilder& debug, int level) const { + _debugAddSpace(debug, level); + debug << path() << " fmod: divisor: " << _divisor.toString() + << " remainder: " << _remainder.toString(); + MatchExpression::TagData* td = getTag(); + if (td) { + debug << " "; + td->debugString(&debug); + } + debug << "\n"; +} + +void InternalSchemaFmodMatchExpression::serialize(BSONObjBuilder* out) const { + BSONObjBuilder objMatchBob(out->subobjStart(path())); + BSONArrayBuilder arrBuilder(objMatchBob.subarrayStart("$_internalSchemaFmod")); + arrBuilder.append(_divisor); + arrBuilder.append(_remainder); + arrBuilder.doneFast(); + objMatchBob.doneFast(); +} + +bool InternalSchemaFmodMatchExpression::equivalent(const MatchExpression* other) const { + if (matchType() != other->matchType()) { + return false; + } + + const InternalSchemaFmodMatchExpression* realOther = + static_cast<const InternalSchemaFmodMatchExpression*>(other); + return path() == realOther->path() && _divisor.isEqual(realOther->_divisor) && + _remainder.isEqual(realOther->_remainder); +} + +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h new file mode 100644 index 00000000000..179a4b06d58 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h @@ -0,0 +1,74 @@ +/** + * 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_leaf.h" + +namespace mongo { + +/** + * MatchExpression for $_internalSchemaFmod keyword. Same as ModMatchExpression but works on + * decimals. + */ +class InternalSchemaFmodMatchExpression final : public LeafMatchExpression { +public: + InternalSchemaFmodMatchExpression() : LeafMatchExpression(MatchType::INTERNAL_SCHEMA_FMOD) {} + + Status init(StringData path, Decimal128 divisor, Decimal128 remainder); + + std::unique_ptr<MatchExpression> shallowClone() const final { + std::unique_ptr<InternalSchemaFmodMatchExpression> m = + stdx::make_unique<InternalSchemaFmodMatchExpression>(); + invariantOK(m->init(path(), _divisor, _remainder)); + if (getTag()) { + m->setTag(getTag()->clone()); + } + return std::move(m); + } + + bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final; + + void debugString(StringBuilder& debug, int level) const final; + + void serialize(BSONObjBuilder* out) const final; + + bool equivalent(const MatchExpression* other) const final; + + Decimal128 getDivisor() const { + return _divisor; + } + Decimal128 getRemainder() const { + return _remainder; + } + +private: + Decimal128 _divisor; + Decimal128 _remainder; +}; +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp new file mode 100644 index 00000000000..2eef5fe0ed5 --- /dev/null +++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2017 10gen Inc. + * + * This program is free software: you can redistribute it and/or fmodify + * 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 fmodify 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/expression_internal_schema_fmod.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +TEST(InternalSchemaFmodMatchExpression, MatchesElement) { + BSONObj match = BSON("a" << 1); + BSONObj largerMatch = BSON("a" << 4.0); + BSONObj longLongMatch = BSON("a" << 68719476736LL); + BSONObj notMatch = BSON("a" << 6); + BSONObj negativeNotMatch = BSON("a" << -2); + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("", Decimal128(3), Decimal128(1))); + ASSERT_TRUE(fmod.matchesSingleElement(match.firstElement())); + ASSERT_TRUE(fmod.matchesSingleElement(largerMatch.firstElement())); + ASSERT_TRUE(fmod.matchesSingleElement(longLongMatch.firstElement())); + ASSERT_FALSE(fmod.matchesSingleElement(notMatch.firstElement())); + ASSERT_FALSE(fmod.matchesSingleElement(negativeNotMatch.firstElement())); +} + +TEST(InternalSchemaFmodMatchExpression, ZeroDivisor) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_NOT_OK(fmod.init("", Decimal128(0), Decimal128(1))); +} + +TEST(InternalSchemaFmodMatchExpression, MatchesScalar) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("a", Decimal128(5), Decimal128(2))); + ASSERT_TRUE(fmod.matchesBSON(BSON("a" << 7.0))); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << 4))); +} + +TEST(InternalSchemaFmodMatchExpression, MatchesNonIntegralValue) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("a", Decimal128(10.5), Decimal128((4.5)))); + ASSERT_TRUE(fmod.matchesBSON(BSON("a" << 15.0))); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << 10.0))); +} + +TEST(InternalSchemaFmodMatchExpression, MatchesArrayValue) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("a", Decimal128(5), Decimal128(2))); + ASSERT_TRUE(fmod.matchesBSON(BSON("a" << BSON_ARRAY(5 << 12LL)))); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << BSON_ARRAY(6 << 8)))); +} + +TEST(InternalSchemaFmodMatchExpression, DoesNotMatchNull) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("a", Decimal128(5), Decimal128(2))); + ASSERT_FALSE(fmod.matchesBSON(BSONObj())); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << BSONNULL))); +} + +TEST(InternalSchemaFmodMatchExpression, NegativeRemainders) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("a", Decimal128(5), Decimal128(-2.4))); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << 7.6))); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << 12.4))); + ASSERT_TRUE(fmod.matchesBSON(BSON("a" << Decimal128(-12.4)))); +} + +TEST(InternalSchemaFmodMatchExpression, ElemMatchKey) { + InternalSchemaFmodMatchExpression fmod; + ASSERT_OK(fmod.init("a", Decimal128(5), Decimal128(2))); + MatchDetails details; + details.requestElemMatchKey(); + ASSERT_FALSE(fmod.matchesBSON(BSON("a" << 4), &details)); + ASSERT_FALSE(details.hasElemMatchKey()); + ASSERT_TRUE(fmod.matchesBSON(BSON("a" << 2), &details)); + ASSERT_FALSE(details.hasElemMatchKey()); + ASSERT_TRUE(fmod.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 5)), &details)); + ASSERT_TRUE(details.hasElemMatchKey()); + ASSERT_EQUALS("1", details.elemMatchKey()); +} + +TEST(InternalSchemaFmodMatchExpression, Equality) { + InternalSchemaFmodMatchExpression m1; + InternalSchemaFmodMatchExpression m2; + InternalSchemaFmodMatchExpression m3; + InternalSchemaFmodMatchExpression m4; + + ASSERT_OK(m1.init("a", Decimal128(1.7), Decimal128(2))); + ASSERT_OK(m2.init("a", Decimal128(2), Decimal128(2))); + ASSERT_OK(m3.init("a", Decimal128(1.7), Decimal128(1))); + ASSERT_OK(m4.init("b", Decimal128(1.7), Decimal128(2))); + + ASSERT_TRUE(m1.equivalent(&m1)); + ASSERT_FALSE(m1.equivalent(&m2)); + ASSERT_FALSE(m1.equivalent(&m3)); + ASSERT_FALSE(m1.equivalent(&m4)); +} +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index 591c1a8be60..85ae25286e5 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -33,6 +33,7 @@ #include "mongo/bson/bsontypes.h" #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/matcher/schema/expression_internal_schema_fmod.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_length.h" #include "mongo/db/matcher/schema/expression_internal_schema_min_length.h" #include "mongo/db/matcher/schema/expression_internal_schema_object_match.h" @@ -50,6 +51,7 @@ constexpr StringData kSchemaMinimumKeyword = "minimum"_sd; constexpr StringData kSchemaMaxLengthKeyword = "maxLength"_sd; constexpr StringData kSchemaMinLengthKeyword = "minLength"_sd; constexpr StringData kSchemaPatternKeyword = "pattern"_sd; +constexpr StringData kSchemaMultipleOfKeyword = "multipleOf"_sd; constexpr StringData kSchemaPropertiesKeyword = "properties"_sd; constexpr StringData kSchemaTypeKeyword = "type"_sd; @@ -264,6 +266,35 @@ StatusWithMatchExpression parsePattern(StringData path, return makeRestriction(BSONType::String, std::move(expr), typeExpr); } +StatusWithMatchExpression parseMultipleOf(StringData path, + BSONElement multipleOf, + TypeMatchExpression* typeExpr) { + if (!multipleOf.isNumber()) { + return {Status(ErrorCodes::TypeMismatch, + str::stream() << "$jsonSchema keyword '" << kSchemaMultipleOfKeyword + << "' must be a number")}; + } + + if (multipleOf.numberDecimal().isNegative() || multipleOf.numberDecimal().isZero()) { + return {Status(ErrorCodes::FailedToParse, + str::stream() << "$jsonSchema keyword '" << kSchemaMultipleOfKeyword + << "' must have a positive value")}; + } + if (path.empty()) { + return {stdx::make_unique<AlwaysTrueMatchExpression>()}; + } + + auto expr = stdx::make_unique<InternalSchemaFmodMatchExpression>(); + auto status = expr->init(path, multipleOf.numberDecimal(), Decimal128(0)); + if (!status.isOK()) { + return status; + } + + TypeMatchExpression::Type restrictionType; + restrictionType.allNumbers = true; + return makeRestriction(restrictionType, std::move(expr), typeExpr); +} + } // namespace StatusWithMatchExpression JSONSchemaParser::_parseProperties(StringData path, @@ -318,7 +349,8 @@ StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj sche {kSchemaExclusiveMinimumKeyword, {}}, {kSchemaMaxLengthKeyword, {}}, {kSchemaMinLengthKeyword, {}}, - {kSchemaPatternKeyword, {}}}; + {kSchemaPatternKeyword, {}}, + {kSchemaMultipleOfKeyword, {}}}; for (auto&& elt : schema) { auto it = keywordMap.find(elt.fieldNameStringData()); @@ -431,6 +463,13 @@ StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj sche } andExpr->add(patternExpr.getValue().release()); } + if (auto multipleOfElt = keywordMap[kSchemaMultipleOfKeyword]) { + auto multipleOfExpr = parseMultipleOf(path, multipleOfElt, typeExpr.getValue().get()); + if (!multipleOfExpr.isOK()) { + return multipleOfExpr; + } + andExpr->add(multipleOfExpr.getValue().release()); + } if (path.empty() && typeExpr.getValue() && typeExpr.getValue()->getBSONType() != BSONType::Object) { diff --git a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp index 81f43b7f6a2..3beaa2fb7cc 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp @@ -443,5 +443,38 @@ TEST(JSONSchemaParserTest, PatternTranslatesCorrectlyWithString) { ASSERT_BSONOBJ_EQ(builder.obj(), expected); } + +TEST(JSONSchemaParserTest, FailsToParseIfMultipleOfIsNotANumber) { + BSONObj schema = fromjson("{multipleOf: 'foo'}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserTest, FailsToParseIfMultipleOfIsLessThanZero) { + BSONObj schema = fromjson("{multipleOf: -1}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserTest, FailsToParseIfMultipleOfIsZero) { + BSONObj schema = fromjson("{multipleOf: 0}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserTest, MultipleOfTranslatesCorrectlyWithTypeNumber) { + BSONObj schema = fromjson( + "{properties: {foo: {type: 'number', multipleOf: NumberDecimal('5.3')}}, 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: [{$or: [{$nor: [{foo: {$type: 'number'}}]}, " + "{foo: {$_internalSchemaFmod: [NumberDecimal('5.3'), 0]}}]}, {$or: [{$nor: [{foo: " + "{$exists: true}}]}, {foo: {$type: 'number'}}]}]}]}]}")); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index 7a58d33f7b4..c67b65c8d23 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -261,6 +261,7 @@ Document redactSafePortionDollarOps(BSONObj expr) { case PathAcceptingKeyword::EXISTS: case PathAcceptingKeyword::WITHIN: case PathAcceptingKeyword::GEO_INTERSECTS: + case PathAcceptingKeyword::INTERNAL_SCHEMA_FMOD: case PathAcceptingKeyword::INTERNAL_SCHEMA_MIN_ITEMS: case PathAcceptingKeyword::INTERNAL_SCHEMA_MAX_ITEMS: case PathAcceptingKeyword::INTERNAL_SCHEMA_UNIQUE_ITEMS: diff --git a/src/mongo/db/pipeline/document_source_match_test.cpp b/src/mongo/db/pipeline/document_source_match_test.cpp index 9f73219cfa2..33e10840ecd 100644 --- a/src/mongo/db/pipeline/document_source_match_test.cpp +++ b/src/mongo/db/pipeline/document_source_match_test.cpp @@ -116,6 +116,8 @@ TEST_F(DocumentSourceMatchTest, RedactSafePortion) { assertExpectedRedactSafePortion("{a: {$_internalSchemaMaxLength: 1}}", "{}"); + assertExpectedRedactSafePortion("{a: {$_internalSchemaFmod: [4.5, 2.3]}}", "{}"); + // Combinations assertExpectedRedactSafePortion("{a:1, b: 'asdf'}", "{a:1, b: 'asdf'}"); diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp index eaf2b38cdce..42bd7f6401b 100644 --- a/src/mongo/db/query/plan_cache.cpp +++ b/src/mongo/db/query/plan_cache.cpp @@ -177,6 +177,9 @@ const char* encodeMatchType(MatchExpression::MatchType mt) { case MatchExpression::INTERNAL_SCHEMA_COND: return "internalSchemaCond"; break; + case MatchExpression::INTERNAL_SCHEMA_FMOD: + return "internalSchemaFmod"; + break; case MatchExpression::INTERNAL_SCHEMA_MIN_ITEMS: return "internalSchemaMinItems"; break; |