diff options
author | James Wahlin <james@mongodb.com> | 2017-09-28 12:21:30 -0400 |
---|---|---|
committer | James Wahlin <james@mongodb.com> | 2017-10-10 16:39:06 -0400 |
commit | 2f3b96e636329b68809bc63b681a862e3d3bccd5 (patch) | |
tree | 93294474137e9607b145c3a9abab01dc84543110 | |
parent | 0ef1d6ec05798f3dae9b838ad8a8fffdd7ec4990 (diff) | |
download | mongo-2f3b96e636329b68809bc63b681a862e3d3bccd5.tar.gz |
SERVER-30989 Add Expression rewrite to ExprMatchExpression
-rw-r--r-- | jstests/aggregation/sources/match/expr_index_use.js | 163 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_expr.cpp | 36 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_expr.h | 7 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_expr_test.cpp | 506 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_serialization_test.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/matcher/rewrite_expr.cpp | 12 | ||||
-rw-r--r-- | src/mongo/db/matcher/rewrite_expr.h | 12 | ||||
-rw-r--r-- | src/mongo/db/matcher/rewrite_expr_test.cpp | 10 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_lookup_test.cpp | 3 |
9 files changed, 668 insertions, 84 deletions
diff --git a/jstests/aggregation/sources/match/expr_index_use.js b/jstests/aggregation/sources/match/expr_index_use.js new file mode 100644 index 00000000000..0d3eecd2a10 --- /dev/null +++ b/jstests/aggregation/sources/match/expr_index_use.js @@ -0,0 +1,163 @@ +// Confirms expected index use when performing a match with a $expr statement. + +(function() { + "use strict"; + + load("jstests/libs/analyze_plan.js"); + + const coll = db.expr_index_use; + coll.drop(); + + assert.writeOK(coll.insert({x: 0})); + assert.writeOK(coll.insert({x: 1, y: 1})); + assert.writeOK(coll.insert({x: 2, y: 2})); + assert.writeOK(coll.insert({x: 3, y: 10})); + assert.writeOK(coll.insert({y: 20})); + assert.writeOK(coll.insert({a: {b: 1}})); + assert.writeOK(coll.insert({a: {b: [1]}})); + assert.writeOK(coll.insert({a: [{b: 1}]})); + assert.writeOK(coll.insert({a: [{b: [1]}]})); + + assert.commandWorked(coll.createIndex({x: 1, y: 1})); + assert.commandWorked(coll.createIndex({y: 1})); + assert.commandWorked(coll.createIndex({"a.b": 1})); + + /** + * Executes an 'executionStats' explain on pipeline and confirms 'metricsToCheck' which is an + * object containing: + * - nReturned: The number of documents the pipeline is expected to return. + * - expectedIndex: Either an index specification object when index use is expected or + * 'null' if a collection scan is expected. + * - indexBounds: The expected index bounds. + */ + function confirmExpectedPipelineExecution(pipeline, metricsToCheck) { + assert(metricsToCheck.hasOwnProperty("nReturned"), + "metricsToCheck must contain an nReturned field"); + + assert.eq(metricsToCheck.nReturned, coll.aggregate(pipeline).itcount()); + + const explain = assert.commandWorked(coll.explain("executionStats").aggregate(pipeline)); + if (metricsToCheck.hasOwnProperty("expectedIndex")) { + const stage = getAggPlanStage(explain, "IXSCAN"); + assert.neq(null, stage, tojson(explain)); + assert(stage.hasOwnProperty("keyPattern"), tojson(explain)); + assert.docEq(stage.keyPattern, metricsToCheck.expectedIndex, tojson(explain)); + } else { + assert.neq(null, aggPlanHasStage(explain, "COLLSCAN"), tojson(explain)); + } + + if (metricsToCheck.hasOwnProperty("indexBounds")) { + const stage = getAggPlanStage(explain, "IXSCAN"); + assert.neq(null, stage, tojson(explain)); + assert(stage.hasOwnProperty("indexBounds"), tojson(explain)); + assert.docEq(stage.indexBounds, metricsToCheck.indexBounds, tojson(explain)); + } + } + + // Comparison of field and constant. + confirmExpectedPipelineExecution([{$match: {$expr: {$eq: ["$x", 1]}}}], { + nReturned: 1, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[1.0, 1.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$eq: [1, "$x"]}}}], { + nReturned: 1, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[1.0, 1.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$lt: ["$x", 1]}}}], { + nReturned: 1, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[-inf.0, 1.0)"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$lt: [1, "$x"]}}}], { + nReturned: 2, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["(1.0, inf.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$lte: ["$x", 1]}}}], { + nReturned: 2, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[-inf.0, 1.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$lte: [1, "$x"]}}}], { + nReturned: 3, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[1.0, inf.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$gt: ["$x", 1]}}}], { + nReturned: 2, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["(1.0, inf.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$gt: [1, "$x"]}}}], { + nReturned: 1, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[-inf.0, 1.0)"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$gte: ["$x", 1]}}}], { + nReturned: 3, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[1.0, inf.0]"], "y": ["[MinKey, MaxKey]"]} + }); + confirmExpectedPipelineExecution([{$match: {$expr: {$gte: [1, "$x"]}}}], { + nReturned: 2, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[-inf.0, 1.0]"], "y": ["[MinKey, MaxKey]"]} + }); + + // $in with field and array of values. + confirmExpectedPipelineExecution([{$match: {$expr: {$in: ["$x", [1, 3]]}}}], { + nReturned: 2, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[1.0, 1.0]", "[3.0, 3.0]"], "y": ["[MinKey, MaxKey]"]} + }); + + // $and with both children eligible for index use. + confirmExpectedPipelineExecution( + [{$match: {$expr: {$and: [{$eq: ["$x", 2]}, {$gt: ["$y", 1]}]}}}], { + nReturned: 1, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["[2.0, 2.0]"], "y": ["(1.0, inf.0]"]} + }); + + // $and with one child eligible for index use and one that is not. + confirmExpectedPipelineExecution( + [{$match: {$expr: {$and: [{$gt: ["$x", 1]}, {$eq: ["$x", "$y"]}]}}}], { + nReturned: 1, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["(1.0, inf.0]"], "y": ["[MinKey, MaxKey]"]} + }); + + // $and with one child elibible for index use and a second child containing a $or where one of + // the two children are eligible. + confirmExpectedPipelineExecution( + [ + { + $match: { + $expr: + {$and: [{$gt: ["$x", 1]}, {$or: [{$eq: ["$x", "$y"]}, {$gt: ["$y", 1]}]}]} + } + } + ], + { + nReturned: 2, + expectedIndex: {x: 1, y: 1}, + indexBounds: {"x": ["(1.0, inf.0]"], "y": ["[MinKey, MaxKey]"]} + }); + + // $cmp is not expected to use an index. + confirmExpectedPipelineExecution([{$match: {$expr: {$cmp: ["$x", 1]}}}], {nReturned: 8}); + + // An constant expression is not expected to use an index. + confirmExpectedPipelineExecution([{$match: {$expr: 1}}], {nReturned: 9}); + + // Comparison of 2 fields is not expected to use an index. + confirmExpectedPipelineExecution([{$match: {$expr: {$eq: ["$x", "$y"]}}}], {nReturned: 6}); + + // Comparison with field path length > 1 is not expected to use an index. + confirmExpectedPipelineExecution([{$match: {$expr: {$eq: ["$a.b", 1]}}}], {nReturned: 1}); + + // $in with field path length > 1 is not expected to use an index. + confirmExpectedPipelineExecution([{$match: {$expr: {$in: ["$a.b", [1, 3]]}}}], {nReturned: 1}); +})(); diff --git a/src/mongo/db/matcher/expression_expr.cpp b/src/mongo/db/matcher/expression_expr.cpp index 77349430e46..459a58307a5 100644 --- a/src/mongo/db/matcher/expression_expr.cpp +++ b/src/mongo/db/matcher/expression_expr.cpp @@ -31,13 +31,21 @@ #include "mongo/db/matcher/expression_expr.h" namespace mongo { +ExprMatchExpression::ExprMatchExpression(boost::intrusive_ptr<Expression> expr, + const boost::intrusive_ptr<ExpressionContext>& expCtx) + : MatchExpression(MatchType::EXPRESSION), _expCtx(expCtx), _expression(expr) {} + ExprMatchExpression::ExprMatchExpression(BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) - : MatchExpression(MatchType::EXPRESSION), - _expCtx(expCtx), - _expression(Expression::parseOperand(expCtx, elem, expCtx->variablesParseState)) {} + : ExprMatchExpression(Expression::parseOperand(expCtx, elem, expCtx->variablesParseState), + expCtx) {} bool ExprMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const { + if (_rewriteResult && _rewriteResult->matchExpression() && + !_rewriteResult->matchExpression()->matches(doc, details)) { + return false; + } + Document document(doc->toBSON()); auto value = _expression->evaluate(document); return value.coerceToBool(); @@ -66,6 +74,10 @@ bool ExprMatchExpression::equivalent(const MatchExpression* other) const { void ExprMatchExpression::_doSetCollator(const CollatorInterface* collator) { _expCtx->setCollator(collator); + + if (_rewriteResult && _rewriteResult->matchExpression()) { + _rewriteResult->matchExpression()->setCollator(collator); + } } @@ -82,7 +94,25 @@ std::unique_ptr<MatchExpression> ExprMatchExpression::shallowClone() const { MatchExpression::ExpressionOptimizerFunc ExprMatchExpression::getOptimizer() const { return [](std::unique_ptr<MatchExpression> expression) { auto& exprMatchExpr = static_cast<ExprMatchExpression&>(*expression); + + // If '_expression' can be rewritten to a MatchExpression, we will return a $and node with + // both the original ExprMatchExpression and the MatchExpression rewrite as children. + // Exiting early prevents additional calls to optimize from performing additional rewrites + // and adding duplicate MatchExpression sub-trees to the tree. + if (exprMatchExpr._rewriteResult) { + return expression; + } + exprMatchExpr._expression = exprMatchExpr._expression->optimize(); + exprMatchExpr._rewriteResult = + RewriteExpr::rewrite(exprMatchExpr._expression, exprMatchExpr._expCtx->getCollator()); + + if (exprMatchExpr._rewriteResult->matchExpression()) { + auto andMatch = stdx::make_unique<AndMatchExpression>(); + andMatch->add(exprMatchExpr._rewriteResult->releaseMatchExpression().release()); + andMatch->add(expression.release()); + expression = std::move(andMatch); + } return expression; }; diff --git a/src/mongo/db/matcher/expression_expr.h b/src/mongo/db/matcher/expression_expr.h index 1a09e2a9334..bd50d2e94f3 100644 --- a/src/mongo/db/matcher/expression_expr.h +++ b/src/mongo/db/matcher/expression_expr.h @@ -32,6 +32,7 @@ #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_tree.h" +#include "mongo/db/matcher/rewrite_expr.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context.h" @@ -46,8 +47,7 @@ public: ExprMatchExpression(BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); ExprMatchExpression(boost::intrusive_ptr<Expression> expr, - const boost::intrusive_ptr<ExpressionContext>& expCtx) - : MatchExpression(MatchType::EXPRESSION), _expCtx(expCtx), _expression(expr) {} + const boost::intrusive_ptr<ExpressionContext>& expCtx); bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final { MONGO_UNREACHABLE; @@ -96,7 +96,8 @@ private: boost::intrusive_ptr<ExpressionContext> _expCtx; boost::intrusive_ptr<Expression> _expression; -}; + boost::optional<RewriteExpr::RewriteResult> _rewriteResult; +}; } // namespace mongo diff --git a/src/mongo/db/matcher/expression_expr_test.cpp b/src/mongo/db/matcher/expression_expr_test.cpp index e89ef9a5c39..4cb8be31e16 100644 --- a/src/mongo/db/matcher/expression_expr_test.cpp +++ b/src/mongo/db/matcher/expression_expr_test.cpp @@ -28,8 +28,10 @@ #include "mongo/platform/basic.h" +#include "mongo/bson/json.h" #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_expr.h" +#include "mongo/db/matcher/expression_parser.h" #include "mongo/db/matcher/matcher.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/collation/collator_interface_mock.h" @@ -41,57 +43,455 @@ namespace { using unittest::assertGet; -TEST(ExprMatchExpression, ComparisonToConstantMatchesCorrectly) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto match = BSON("a" << 5); - auto notMatch = BSON("a" << 6); +class ExprMatchTest : public mongo::unittest::Test { +public: + ExprMatchTest() : _expCtx(new ExpressionContextForTest()) {} + + void createMatcher(const BSONObj& matchExpr) { + _matchExpression = uassertStatusOK( + MatchExpressionParser::parse(matchExpr, + _expCtx, + ExtensionsCallbackNoop(), + MatchExpressionParser::kAllowAllSpecialFeatures)); + _matchExpression = MatchExpression::optimize(std::move(_matchExpression)); + } + + void setCollator(CollatorInterface* collator) { + _expCtx->setCollator(collator); + if (_matchExpression) { + _matchExpression->setCollator(_expCtx->getCollator()); + } + } + + void setVariable(StringData name, Value val) { + auto varId = _expCtx->variablesParseState.defineVariable(name); + _expCtx->variables.setValue(varId, val); + } + + bool matches(const BSONObj& doc) { + invariant(_matchExpression); + return _matchExpression->matchesBSON(doc); + } + +private: + const boost::intrusive_ptr<ExpressionContextForTest> _expCtx; + std::unique_ptr<MatchExpression> _matchExpression; +}; + +TEST_F(ExprMatchTest, ComparisonToConstantMatchesCorrectly) { + createMatcher(BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" << 5)))); + + ASSERT_TRUE(matches(BSON("a" << 5))); + + ASSERT_FALSE(matches(BSON("a" << 4))); + ASSERT_FALSE(matches(BSON("a" << 6))); +} - auto expression1 = BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" << 5))); - Matcher matcher1(expression1, - expCtx, - ExtensionsCallbackNoop(), - MatchExpressionParser::kAllowAllSpecialFeatures); - ASSERT_TRUE(matcher1.matches(match)); - ASSERT_FALSE(matcher1.matches(notMatch)); +TEST_F(ExprMatchTest, ComparisonToConstantVariableMatchesCorrectly) { + setVariable("var", Value(5)); + createMatcher(BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" + << "$$var")))); - auto varId = expCtx->variablesParseState.defineVariable("var"); - expCtx->variables.setValue(varId, Value(5)); - auto expression2 = BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" - << "$$var"))); - Matcher matcher2(expression2, - expCtx, - ExtensionsCallbackNoop(), - MatchExpressionParser::kAllowAllSpecialFeatures); - ASSERT_TRUE(matcher2.matches(match)); - ASSERT_FALSE(matcher2.matches(notMatch)); -} - -TEST(ExprMatchExpression, ComparisonBetweenTwoFieldPathsMatchesCorrectly) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ASSERT_TRUE(matches(BSON("a" << 5))); + + ASSERT_FALSE(matches(BSON("a" << 4))); + ASSERT_FALSE(matches(BSON("a" << 6))); +} - auto expression = BSON("$expr" << BSON("$gt" << BSON_ARRAY("$a" - << "$b"))); - auto match = BSON("a" << 10 << "b" << 2); - auto notMatch = BSON("a" << 2 << "b" << 10); +TEST_F(ExprMatchTest, ComparisonBetweenTwoFieldPathsMatchesCorrectly) { + createMatcher(BSON("$expr" << BSON("$gt" << BSON_ARRAY("$a" + << "$b")))); - Matcher matcher(expression, - std::move(expCtx), - ExtensionsCallbackNoop(), - MatchExpressionParser::kAllowAllSpecialFeatures); + ASSERT_TRUE(matches(BSON("a" << 10 << "b" << 2))); - ASSERT_TRUE(matcher.matches(match)); - ASSERT_FALSE(matcher.matches(notMatch)); + ASSERT_FALSE(matches(BSON("a" << 2 << "b" << 2))); + ASSERT_FALSE(matches(BSON("a" << 2 << "b" << 10))); } -TEST(ExprMatchExpression, ComparisonThrowsWithUnboundVariable) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto expression = BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" - << "$$var"))); - ASSERT_THROWS(ExprMatchExpression pipelineExpr(expression.firstElement(), std::move(expCtx)), +TEST_F(ExprMatchTest, ComparisonThrowsWithUnboundVariable) { + ASSERT_THROWS(createMatcher(BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" + << "$$var")))), DBException); } -TEST(ExprMatchExpression, IdenticalPostOptimizedExpressionsAreEquivalent) { +TEST_F(ExprMatchTest, EqWithLHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$eq: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + + ASSERT_FALSE(matches(BSON("x" << 1))); + ASSERT_FALSE(matches(BSON("x" << 10))); +} + +TEST_F(ExprMatchTest, EqWithRHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$eq: [3, '$x']}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + + ASSERT_FALSE(matches(BSON("x" << 1))); + ASSERT_FALSE(matches(BSON("x" << 10))); +} + +TEST_F(ExprMatchTest, NeWithLHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$ne: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + ASSERT_TRUE(matches(BSON("x" << 10))); + + ASSERT_FALSE(matches(BSON("x" << 3))); +} + +TEST_F(ExprMatchTest, NeWithFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$ne: [3, '$x']}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + ASSERT_TRUE(matches(BSON("x" << 10))); + + ASSERT_FALSE(matches(BSON("x" << 3))); +} + +TEST_F(ExprMatchTest, GtWithLHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$gt: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 10))); + + ASSERT_FALSE(matches(BSON("x" << 1))); + ASSERT_FALSE(matches(BSON("x" << 3))); +} + +TEST_F(ExprMatchTest, GtWithRHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$gt: [3, '$x']}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + + ASSERT_FALSE(matches(BSON("x" << 3))); + ASSERT_FALSE(matches(BSON("x" << 10))); +} + +TEST_F(ExprMatchTest, GteWithLHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$gte: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_TRUE(matches(BSON("x" << 10))); + + ASSERT_FALSE(matches(BSON("x" << 1))); +} + +TEST_F(ExprMatchTest, GteWithRHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$gte: [3, '$x']}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_TRUE(matches(BSON("x" << 1))); + + ASSERT_FALSE(matches(BSON("x" << 10))); +} + +TEST_F(ExprMatchTest, LtWithLHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$lt: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + + ASSERT_FALSE(matches(BSON("x" << 3))); + ASSERT_FALSE(matches(BSON("x" << 10))); +} + +TEST_F(ExprMatchTest, LtWithRHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$lt: [3, '$x']}}")); + + ASSERT_TRUE(matches(BSON("x" << 10))); + + ASSERT_FALSE(matches(BSON("x" << 3))); + ASSERT_FALSE(matches(BSON("x" << 1))); +} + +TEST_F(ExprMatchTest, LteWithLHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$lte: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_FALSE(matches(BSON("x" << 10))); +} + +TEST_F(ExprMatchTest, LteWithRHSFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$lte: [3, '$x']}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_TRUE(matches(BSON("x" << 10))); + + ASSERT_FALSE(matches(BSON("x" << 1))); +} + +TEST_F(ExprMatchTest, AndMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$and: [{$eq: ['$x', 3]}, {$ne: ['$y', 4]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_TRUE(matches(BSON("x" << 3 << "y" << 5))); + + ASSERT_FALSE(matches(BSON("x" << 10 << "y" << 5))); + ASSERT_FALSE(matches(BSON("x" << 3 << "y" << 4))); + ASSERT_FALSE(matches(BSON("x" << 10 << "y" << 5))); +} + +TEST_F(ExprMatchTest, OrMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$or: [{$lte: ['$x', 3]}, {$gte: ['$y', 4]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_TRUE(matches(BSON("y" << 5))); + + ASSERT_FALSE(matches(BSON("x" << 10))); + ASSERT_FALSE(matches(BSON("y" << 1))); +} + +TEST_F(ExprMatchTest, AndNestedWithinOrMatchesCorrectly) { + createMatcher(fromjson( + "{$expr: {$or: [{$and: [{$eq: ['$x', 3]}, {$gt: ['$z', 5]}]}, {$lt: ['$y', 4]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3 << "z" << 7))); + ASSERT_TRUE(matches(BSON("y" << 1))); + + ASSERT_FALSE(matches(BSON("x" << 3 << "z" << 3))); + ASSERT_FALSE(matches(BSON("y" << 5))); +} + +TEST_F(ExprMatchTest, OrNestedWithinAndMatchesCorrectly) { + createMatcher(fromjson( + "{$expr: {$and: [{$or: [{$eq: ['$x', 3]}, {$eq: ['$z', 5]}]}, {$eq: ['$y', 4]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3 << "y" << 4))); + ASSERT_TRUE(matches(BSON("z" << 5 << "y" << 4))); + ASSERT_TRUE(matches(BSON("x" << 3 << "z" << 5 << "y" << 4))); + + ASSERT_FALSE(matches(BSON("x" << 3 << "z" << 5))); + ASSERT_FALSE(matches(BSON("y" << 4))); + ASSERT_FALSE(matches(BSON("x" << 3 << "y" << 10))); +} + +TEST_F(ExprMatchTest, InWithLhsFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$in: ['$x', [1, 2, 3]]}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + ASSERT_TRUE(matches(BSON("x" << 3))); + + ASSERT_FALSE(matches(BSON("x" << 5))); + ASSERT_FALSE(matches(BSON("y" << 2))); + ASSERT_FALSE(matches(BSON("x" << BSON("y" << 2)))); +} + +TEST_F(ExprMatchTest, InWithLhsFieldPathAndArrayAsConstMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$in: ['$x', {$const: [1, 2, 3]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + ASSERT_TRUE(matches(BSON("x" << 3))); + + ASSERT_FALSE(matches(BSON("x" << 5))); + ASSERT_FALSE(matches(BSON("y" << 2))); + ASSERT_FALSE(matches(BSON("x" << BSON("y" << 2)))); +} + +TEST_F(ExprMatchTest, CmpMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$cmp: ['$x', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << 2))); + ASSERT_TRUE(matches(BSON("x" << 4))); + ASSERT_TRUE(matches(BSON("y" << 3))); + + ASSERT_FALSE(matches(BSON("x" << 3))); +} + +TEST_F(ExprMatchTest, ConstantLiteralExpressionMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$literal: {$eq: ['$x', 10]}}}")); + + ASSERT_TRUE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, ConstantPositiveNumberExpressionMatchesCorrectly) { + createMatcher(fromjson("{$expr: 1}")); + + ASSERT_TRUE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, ConstantNegativeNumberExpressionMatchesCorrectly) { + createMatcher(fromjson("{$expr: -1}")); + + ASSERT_TRUE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, ConstantNumberZeroExpressionMatchesCorrectly) { + createMatcher(fromjson("{$expr: 0}")); + + ASSERT_FALSE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, ConstantTrueValueExpressionMatchesCorrectly) { + createMatcher(fromjson("{$expr: true}")); + + ASSERT_TRUE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, ConstantFalseValueExpressionMatchesCorrectly) { + createMatcher(fromjson("{$expr: false}")); + + ASSERT_FALSE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, EqWithTwoFieldPathsMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$eq: ['$x', '$y']}}")); + + ASSERT_TRUE(matches(BSON("x" << 2 << "y" << 2))); + + ASSERT_FALSE(matches(BSON("x" << 2 << "y" << 3))); + ASSERT_FALSE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, EqWithTwoConstantsMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$eq: [3, 4]}}")); + + ASSERT_FALSE(matches(BSON("x" << 3))); +} + +TEST_F(ExprMatchTest, EqWithDottedFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$eq: ['$x.y', 3]}}")); + + ASSERT_TRUE(matches(BSON("x" << BSON("y" << 3)))); + + ASSERT_FALSE(matches(BSON("x" << BSON("y" << BSON_ARRAY(3))))); + ASSERT_FALSE(matches(BSON("x" << BSON_ARRAY(BSON("y" << 3))))); + ASSERT_FALSE(matches(BSON("x" << BSON_ARRAY(BSON("y" << BSON_ARRAY(3)))))); +} + +TEST_F(ExprMatchTest, InWithDottedFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$in: ['$x.y', [1, 2, 3]]}}")); + + ASSERT_TRUE(matches(BSON("x" << BSON("y" << 3)))); + + ASSERT_FALSE(matches(BSON("x" << BSON("y" << BSON_ARRAY(3))))); +} + +TEST_F(ExprMatchTest, AndWithNoMatchRewritableChildrenMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$and: [{$eq: ['$w', '$x']}, {$eq: ['$y', '$z']}]}}")); + + ASSERT_TRUE(matches(BSON("w" << 2 << "x" << 2 << "y" << 5 << "z" << 5))); + + ASSERT_FALSE(matches(BSON("w" << 1 << "x" << 2 << "y" << 5 << "z" << 5))); + ASSERT_FALSE(matches(BSON("w" << 2 << "x" << 2 << "y" << 5 << "z" << 6))); + ASSERT_FALSE(matches(BSON("w" << 2 << "y" << 5))); +} + +TEST_F(ExprMatchTest, OrWithDistinctMatchRewritableAndNonMatchRewritableChildrenMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$or: [{$eq: ['$x', 1]}, {$eq: ['$y', '$z']}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 1))); + ASSERT_TRUE(matches(BSON("y" << 1 << "z" << 1))); + + ASSERT_FALSE(matches(BSON("x" << 2 << "y" << 3))); + ASSERT_FALSE(matches(BSON("y" << 1))); + ASSERT_FALSE(matches(BSON("y" << 1 << "z" << 2))); +} + +TEST_F(ExprMatchTest, InWithoutLhsFieldPathMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$in: [2, [1, 2, 3]]}}")); + ASSERT_TRUE(matches(BSON("x" << 2))); + + createMatcher(fromjson("{$expr: {$in: [2, [5, 6, 7]]}}")); + ASSERT_FALSE(matches(BSON("x" << 2))); +} + +TEST_F(ExprMatchTest, NestedAndWithTwoFieldPathsWithinOrMatchesCorrectly) { + createMatcher(fromjson( + "{$expr: {$or: [{$and: [{$eq: ['$x', '$w']}, {$eq: ['$z', 5]}]}, {$eq: ['$y', 4]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 2 << "w" << 2 << "z" << 5))); + ASSERT_TRUE(matches(BSON("y" << 4))); + + ASSERT_FALSE(matches(BSON("x" << 2 << "w" << 4))); + ASSERT_FALSE(matches(BSON("y" << 5))); +} + +TEST_F(ExprMatchTest, AndWithDistinctMatchAndNonMatchSubTreeMatchesCorrectly) { + createMatcher(fromjson("{$expr: {$and: [{$eq: ['$x', 1]}, {$eq: ['$y', '$z']}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 1 << "y" << 2 << "z" << 2))); + + ASSERT_FALSE(matches(BSON("x" << 2 << "y" << 2 << "z" << 2))); + ASSERT_FALSE(matches(BSON("x" << 1 << "y" << 2 << "z" << 10))); + ASSERT_FALSE(matches(BSON("x" << 1 << "y" << 2))); +} + +TEST_F(ExprMatchTest, ComplexExprMatchesCorrectly) { + createMatcher( + fromjson("{" + " $expr: {" + " $and: [" + " {$eq: ['$a', 1]}," + " {$eq: ['$b', '$c']}," + " {" + " $or: [" + " {$eq: ['$d', 1]}," + " {$eq: ['$e', 3]}," + " {" + " $and: [" + " {$eq: ['$f', 1]}," + " {$eq: ['$g', '$h']}," + " {$or: [{$eq: ['$i', 3]}, {$eq: ['$j', '$k']}]}" + " ]" + " }" + " ]" + " }" + " ]" + " }" + "}")); + + ASSERT_TRUE(matches(BSON("a" << 1 << "b" << 3 << "c" << 3 << "d" << 1))); + ASSERT_TRUE(matches(BSON("a" << 1 << "b" << 3 << "c" << 3 << "e" << 3))); + ASSERT_TRUE(matches(BSON("a" << 1 << "b" << 3 << "c" << 3 << "f" << 1 << "i" << 3))); + ASSERT_TRUE( + matches(BSON("a" << 1 << "b" << 3 << "c" << 3 << "f" << 1 << "j" << 5 << "k" << 5))); + + ASSERT_FALSE(matches(BSON("a" << 1))); + ASSERT_FALSE(matches(BSON("a" << 1 << "b" << 3 << "c" << 3))); + ASSERT_FALSE(matches(BSON("a" << 1 << "b" << 3 << "c" << 3 << "d" << 5))); + ASSERT_FALSE(matches(BSON("a" << 1 << "b" << 3 << "c" << 3 << "j" << 5 << "k" << 10))); +} + +TEST_F(ExprMatchTest, + OrWithAndContainingMatchRewritableAndNonMatchRewritableChildMatchesCorrectly) { + createMatcher(fromjson( + "{$expr: {$or: [{$eq: ['$x', 3]}, {$and: [{$eq: ['$y', 4]}, {$eq: ['$y', '$z']}]}]}}")); + + ASSERT_TRUE(matches(BSON("x" << 3))); + ASSERT_TRUE(matches(BSON("y" << 4 << "z" << 4))); + + ASSERT_FALSE(matches(BSON("x" << 4))); + ASSERT_FALSE(matches(BSON("y" << 4 << "z" << 5))); +} + +TEST_F(ExprMatchTest, InitialCollationUsedForComparisons) { + auto collator = + stdx::make_unique<CollatorInterfaceMock>(CollatorInterfaceMock::MockType::kToLowerString); + setCollator(collator.get()); + createMatcher(fromjson("{$expr: {$eq: ['$x', 'abc']}}")); + + ASSERT_TRUE(matches(BSON("x" + << "AbC"))); + + ASSERT_FALSE(matches(BSON("x" + << "cba"))); +} + +TEST_F(ExprMatchTest, SetCollatorChangesCollationUsedForComparisons) { + createMatcher(fromjson("{$expr: {$eq: ['$x', 'abc']}}")); + + auto collator = + stdx::make_unique<CollatorInterfaceMock>(CollatorInterfaceMock::MockType::kToLowerString); + setCollator(collator.get()); + + ASSERT_TRUE(matches(BSON("x" + << "AbC"))); + + ASSERT_FALSE(matches(BSON("x" + << "cba"))); +} + +TEST(ExprMatchTest, IdenticalPostOptimizedExpressionsAreEquivalent) { BSONObj expression = BSON("$expr" << BSON("$multiply" << BSON_ARRAY(2 << 2))); BSONObj expressionEquiv = BSON("$expr" << BSON("$const" << 4)); BSONObj expressionNotEquiv = BSON("$expr" << BSON("$const" << 10)); @@ -116,7 +516,7 @@ TEST(ExprMatchExpression, IdenticalPostOptimizedExpressionsAreEquivalent) { ASSERT_FALSE(pipelineExpr->equivalent(&pipelineExprNotEquiv)); } -TEST(ExprMatchExpression, ExpressionOptimizeRewritesVariableDereferenceAsConstant) { +TEST(ExprMatchTest, ExpressionOptimizeRewritesVariableDereferenceAsConstant) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto varId = expCtx->variablesParseState.defineVariable("var"); expCtx->variables.setValue(varId, Value(4)); @@ -142,7 +542,7 @@ TEST(ExprMatchExpression, ExpressionOptimizeRewritesVariableDereferenceAsConstan ASSERT_FALSE(pipelineExpr.equivalent(&pipelineExprNotEquiv)); } -TEST(ExprMatchExpression, ShallowClonedExpressionIsEquivalentToOriginal) { +TEST(ExprMatchTest, ShallowClonedExpressionIsEquivalentToOriginal) { BSONObj expression = BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" << 5))); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); @@ -151,25 +551,5 @@ TEST(ExprMatchExpression, ShallowClonedExpressionIsEquivalentToOriginal) { ASSERT_TRUE(pipelineExpr.equivalent(shallowClone.get())); } -TEST(ExprMatchExpression, SetCollatorChangesCollationUsedForComparisons) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto match = BSON("a" - << "abc"); - auto notMatch = BSON("a" - << "ABC"); - - auto expression = BSON("$expr" << BSON("$eq" << BSON_ARRAY("$a" - << "abc"))); - auto matchExpression = assertGet(MatchExpressionParser::parse(expression, expCtx)); - ASSERT_TRUE(matchExpression->matchesBSON(match)); - ASSERT_FALSE(matchExpression->matchesBSON(notMatch)); - - CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual); - matchExpression->setCollator(&collator); - - ASSERT_TRUE(matchExpression->matchesBSON(match)); - ASSERT_TRUE(matchExpression->matchesBSON(notMatch)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp index ca6e1ec0567..fef34675935 100644 --- a/src/mongo/db/matcher/expression_serialization_test.cpp +++ b/src/mongo/db/matcher/expression_serialization_test.cpp @@ -974,8 +974,7 @@ TEST(SerializeBasic, ExpressionNotWithGeoSerializesCorrectly) { ASSERT_BSONOBJ_EQ( *reserialized.getQuery(), fromjson("{$nor: [{$and: [{x: {$geoIntersects: {$geometry: {type: 'Polygon', coordinates: " - "[[[0,0], " - "[5,0], [5, 5], [0, 5], [0, 0]]]}}}}]}]}")); + "[[[ 0, 0 ], [5, 0], [5, 5], [0, 5], [0, 0]]]}}}}]}]}")); ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression())); BSONObj obj = diff --git a/src/mongo/db/matcher/rewrite_expr.cpp b/src/mongo/db/matcher/rewrite_expr.cpp index d5d0e4605ac..304a850f592 100644 --- a/src/mongo/db/matcher/rewrite_expr.cpp +++ b/src/mongo/db/matcher/rewrite_expr.cpp @@ -40,20 +40,20 @@ namespace mongo { using CmpOp = ExpressionCompare::CmpOp; -RewriteExpr::RewriteResult RewriteExpr::rewrite( - const boost::intrusive_ptr<Expression>& expression) { +RewriteExpr::RewriteResult RewriteExpr::rewrite(const boost::intrusive_ptr<Expression>& expression, + const CollatorInterface* collator) { LOG(5) << "Expression prior to rewrite: " << expression->serialize(false); - RewriteExpr rewriteExpr; + RewriteExpr rewriteExpr(collator); std::unique_ptr<MatchExpression> matchExpression; if (auto matchTree = rewriteExpr._rewriteExpression(expression)) { matchExpression = std::move(matchTree); LOG(5) << "Post-rewrite MatchExpression: " << matchExpression->toString(); + matchExpression = MatchExpression::optimize(std::move(matchExpression)); + LOG(5) << "Post-rewrite/post-optimized MatchExpression: " << matchExpression->toString(); } - // TODO SERVER-30989: Optimize via MatchExpression::optimize(). - return {std::move(matchExpression), std::move(rewriteExpr._matchExprStringStorage), std::move(rewriteExpr._matchExprElemStorage)}; @@ -151,6 +151,7 @@ std::unique_ptr<MatchExpression> RewriteExpr::_rewriteInExpression( inValues.elems(elementList); auto matchInExpr = stdx::make_unique<InMatchExpression>(); + matchInExpr->setCollator(_collator); uassertStatusOK(matchInExpr->init(fieldPath)); uassertStatusOK(matchInExpr->setEqualities(elementList)); @@ -254,6 +255,7 @@ std::unique_ptr<MatchExpression> RewriteExpr::_buildComparisonMatchExpression( } uassertStatusOK(compMatchExpr->init(fieldAndValue.fieldName(), fieldAndValue)); + compMatchExpr->setCollator(_collator); if (notMatchExpr) { uassertStatusOK(notMatchExpr->init(compMatchExpr.release())); diff --git a/src/mongo/db/matcher/rewrite_expr.h b/src/mongo/db/matcher/rewrite_expr.h index ec5be2c2641..5bbb8e088aa 100644 --- a/src/mongo/db/matcher/rewrite_expr.h +++ b/src/mongo/db/matcher/rewrite_expr.h @@ -52,10 +52,14 @@ public: _matchExprStringStorage(std::move(matchExprStringStorage)), _matchExprElemStorage(std::move(matchExprElemStorage)) {} - const MatchExpression* matchExpression() const { + MatchExpression* matchExpression() const { return _matchExpression.get(); } + std::unique_ptr<MatchExpression> releaseMatchExpression() { + return std::move(_matchExpression); + } + private: std::unique_ptr<MatchExpression> _matchExpression; @@ -72,9 +76,12 @@ public: * superset of the documents matched by 'expr'. Returns the MatchExpression as a RewriteResult. * If a rewrite is not possible, RewriteResult::matchExpression() will return a nullptr. */ - static RewriteResult rewrite(const boost::intrusive_ptr<Expression>& expr); + static RewriteResult rewrite(const boost::intrusive_ptr<Expression>& expr, + const CollatorInterface* collator); private: + RewriteExpr(const CollatorInterface* collator) : _collator(collator) {} + // Returns rewritten MatchExpression or null unique_ptr if not rewritable. std::unique_ptr<MatchExpression> _rewriteExpression( const boost::intrusive_ptr<Expression>& currExprNode); @@ -109,6 +116,7 @@ private: std::vector<BSONObj> _matchExprElemStorage; std::vector<std::string> _matchExprStringStorage; + const CollatorInterface* _collator; }; } // namespace mongo diff --git a/src/mongo/db/matcher/rewrite_expr_test.cpp b/src/mongo/db/matcher/rewrite_expr_test.cpp index bb86ae761f7..ae9f30d6d5d 100644 --- a/src/mongo/db/matcher/rewrite_expr_test.cpp +++ b/src/mongo/db/matcher/rewrite_expr_test.cpp @@ -50,7 +50,7 @@ void testExprRewrite(BSONObj expr, BSONObj expectedMatch) { auto expression = Expression::parseOperand(expCtx, expr.firstElement(), expCtx->variablesParseState); - auto result = RewriteExpr::rewrite(expression); + auto result = RewriteExpr::rewrite(expression, expCtx->getCollator()); // Confirm expected match. if (!expectedMatch.isEmpty()) { @@ -246,14 +246,14 @@ TEST(RewriteExpr, InWithoutLhsFieldPathDoesNotRewriteToMatch) { TEST(RewriteExpr, NestedAndWithTwoFieldPathsWithinOrPartiallyRewriteToMatch) { const BSONObj expr = fromjson( "{$expr: {$or: [{$and: [{$eq: ['$x', '$w']}, {$eq: ['$z', 5]}]}, {$eq: ['$y', 4]}]}}"); - const BSONObj expectedMatch = fromjson("{$or: [{$and: [{z: {$eq: 5}}]}, {y: {$eq: 4}}]}"); + const BSONObj expectedMatch = fromjson("{$or: [{z: {$eq: 5}}, {y: {$eq: 4}}]}"); testExprRewrite(expr, expectedMatch); } TEST(RewriteExpr, AndWithDistinctMatchAndNonMatchSubTreeSplitsOnRewrite) { const BSONObj expr = fromjson("{$expr: {$and: [{$eq: ['$x', 1]}, {$eq: ['$y', '$z']}]}}"); - const BSONObj expectedMatch = fromjson("{$and: [{x: {$eq: 1}}]}"); + const BSONObj expectedMatch = fromjson("{x: {$eq: 1}}"); testExprRewrite(expr, expectedMatch); } @@ -291,7 +291,7 @@ TEST(RewriteExpr, ComplexSupersetMatchRewritesToMatchSuperset) { " $or: [" " {d: {$eq: 1}}," " {e: {$eq: 3}}," - " {$and: [{f: {$eq: 1}}]}" + " {f: {$eq: 1}}" " ]" " }" " ]" @@ -303,7 +303,7 @@ TEST(RewriteExpr, ComplexSupersetMatchRewritesToMatchSuperset) { TEST(RewriteExpr, OrWithAndContainingMatchAndNonMatchChildPartiallyRewritesToMatch) { const BSONObj expr = fromjson( "{$expr: {$or: [{$eq: ['$x', 3]}, {$and: [{$eq: ['$y', 4]}, {$eq: ['$y', '$z']}]}]}}"); - const BSONObj expectedMatch = fromjson("{$or: [{x: {$eq: 3}}, {$and: [{y: {$eq: 4}}]}]}"); + const BSONObj expectedMatch = fromjson("{$or: [{x: {$eq: 3}}, {y: {$eq: 4}}]}"); testExprRewrite(expr, expectedMatch); } diff --git a/src/mongo/db/pipeline/document_source_lookup_test.cpp b/src/mongo/db/pipeline/document_source_lookup_test.cpp index 434bfabc00f..9e6677f8dcb 100644 --- a/src/mongo/db/pipeline/document_source_lookup_test.cpp +++ b/src/mongo/db/pipeline/document_source_lookup_test.cpp @@ -798,7 +798,8 @@ TEST_F(DocumentSourceLookUpTest, ExprEmbeddedInMatchExpressionShouldBeOptimized) BSONObjBuilder builder; matchSource.getMatchExpression()->serialize(&builder); auto serializedMatch = builder.obj(); - auto expectedMatch = fromjson("{$expr: {$eq: ['$_id', {$const: 5}]}}"); + auto expectedMatch = + fromjson("{$and: [{_id: {$eq: 5}}, {$expr: {$eq: ['$_id', {$const: 5}]}}]}"); ASSERT_VALUE_EQ(Value(serializedMatch), Value(expectedMatch)); } |