summaryrefslogtreecommitdiff
path: root/src/mongo/db
diff options
context:
space:
mode:
authorAnne Lim <anne.lim@mongodb.com>2017-07-26 13:33:22 -0400
committerAnne Lim <anne.lim@mongodb.com>2017-08-04 09:27:40 -0400
commit6ac711a04e3aeff10ae551074812c862aa89e906 (patch)
tree93f53fcb7321ad7c823f4407b5a2cf396b126f78 /src/mongo/db
parent41221453c7bfb54ac35b3390887d9a9ae45013de (diff)
downloadmongo-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/SConscript2
-rw-r--r--src/mongo/db/matcher/expression.h1
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp40
-rw-r--r--src/mongo/db/matcher/expression_parser.h3
-rw-r--r--src/mongo/db/matcher/expression_serialization_test.cpp18
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp101
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_fmod.h74
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp124
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp41
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser_test.cpp33
-rw-r--r--src/mongo/db/pipeline/document_source_match.cpp1
-rw-r--r--src/mongo/db/pipeline/document_source_match_test.cpp2
-rw-r--r--src/mongo/db/query/plan_cache.cpp3
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;