diff options
Diffstat (limited to 'src/mongo/db/query/canonical_query_test.cpp')
-rw-r--r-- | src/mongo/db/query/canonical_query_test.cpp | 1003 |
1 files changed, 488 insertions, 515 deletions
diff --git a/src/mongo/db/query/canonical_query_test.cpp b/src/mongo/db/query/canonical_query_test.cpp index d125cb692ab..d3fb9a55fd4 100644 --- a/src/mongo/db/query/canonical_query_test.cpp +++ b/src/mongo/db/query/canonical_query_test.cpp @@ -34,534 +34,507 @@ namespace mongo { namespace { - using std::string; - using std::unique_ptr; - using unittest::assertGet; - - static const char* ns = "somebogusns"; - - /** - * Helper function to parse the given BSON object as a MatchExpression, checks the status, - * and return the MatchExpression*. - */ - MatchExpression* parseMatchExpression(const BSONObj& obj) { - StatusWithMatchExpression status = MatchExpressionParser::parse(obj); - if (!status.isOK()) { - mongoutils::str::stream ss; - ss << "failed to parse query: " << obj.toString() - << ". Reason: " << status.getStatus().toString(); - FAIL(ss); - } - return status.getValue(); - } +using std::string; +using std::unique_ptr; +using unittest::assertGet; - /** - * Helper function which parses and normalizes 'queryStr', and returns whether the given - * (expression tree, lite parsed query) tuple passes CanonicalQuery::isValid(). - * Returns Status::OK() if the tuple is valid, else returns an error Status. - */ - Status isValid(const std::string& queryStr, const LiteParsedQuery& lpqRaw) { - BSONObj queryObj = fromjson(queryStr); - std::unique_ptr<MatchExpression> me( - CanonicalQuery::normalizeTree(parseMatchExpression(queryObj))); - return CanonicalQuery::isValid(me.get(), lpqRaw); - } +static const char* ns = "somebogusns"; - void assertEquivalent(const char* queryStr, - const MatchExpression* expected, - const MatchExpression* actual) { - if (actual->equivalent(expected)) { - return; - } - mongoutils::str::stream ss; - ss << "Match expressions are not equivalent." - << "\nOriginal query: " << queryStr - << "\nExpected: " << expected->toString() - << "\nActual: " << actual->toString(); - FAIL(ss); - } - - void assertNotEquivalent(const char* queryStr, - const MatchExpression* expected, - const MatchExpression* actual) { - if (!actual->equivalent(expected)) { - return; - } +/** + * Helper function to parse the given BSON object as a MatchExpression, checks the status, + * and return the MatchExpression*. + */ +MatchExpression* parseMatchExpression(const BSONObj& obj) { + StatusWithMatchExpression status = MatchExpressionParser::parse(obj); + if (!status.isOK()) { mongoutils::str::stream ss; - ss << "Match expressions are equivalent." - << "\nOriginal query: " << queryStr - << "\nExpected: " << expected->toString() - << "\nActual: " << actual->toString(); + ss << "failed to parse query: " << obj.toString() + << ". Reason: " << status.getStatus().toString(); FAIL(ss); } + return status.getValue(); +} - - TEST(CanonicalQueryTest, IsValidText) { - // Passes in default values for LiteParsedQuery. - // Filter inside LiteParsedQuery is not used. - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Valid: regular TEXT. - ASSERT_OK(isValid("{$text: {$search: 's'}}", *lpq)); - - // Valid: TEXT inside OR. - ASSERT_OK(isValid( - "{$or: [" - " {$text: {$search: 's'}}," - " {a: 1}" - "]}", - *lpq - )); - - // Valid: TEXT outside NOR. - ASSERT_OK(isValid("{$text: {$search: 's'}, $nor: [{a: 1}, {b: 1}]}", *lpq)); - - // Invalid: TEXT inside NOR. - ASSERT_NOT_OK(isValid("{$nor: [{$text: {$search: 's'}}, {a: 1}]}", *lpq)); - - // Invalid: TEXT inside NOR. - ASSERT_NOT_OK(isValid( - "{$nor: [" - " {$or: [" - " {$text: {$search: 's'}}," - " {a: 1}" - " ]}," - " {a: 2}" - "]}", - *lpq - )); - - // Invalid: >1 TEXT. - ASSERT_NOT_OK(isValid( - "{$and: [" - " {$text: {$search: 's'}}," - " {$text: {$search: 't'}}" - "]}", - *lpq - )); - - // Invalid: >1 TEXT. - ASSERT_NOT_OK(isValid( - "{$and: [" - " {$or: [" - " {$text: {$search: 's'}}," - " {a: 1}" - " ]}," - " {$or: [" - " {$text: {$search: 't'}}," - " {b: 1}" - " ]}" - "]}", - *lpq - )); - } - - TEST(CanonicalQueryTest, IsValidGeo) { - // Passes in default values for LiteParsedQuery. - // Filter inside LiteParsedQuery is not used. - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Valid: regular GEO_NEAR. - ASSERT_OK(isValid("{a: {$near: [0, 0]}}", *lpq)); - - // Valid: GEO_NEAR inside nested AND. - ASSERT_OK(isValid( - "{$and: [" - " {$and: [" - " {a: {$near: [0, 0]}}," - " {b: 1}" - " ]}," - " {c: 1}" - "]}", - *lpq - )); - - // Invalid: >1 GEO_NEAR. - ASSERT_NOT_OK(isValid( - "{$and: [" - " {a: {$near: [0, 0]}}," - " {b: {$near: [0, 0]}}" - "]}", - *lpq - )); - - // Invalid: >1 GEO_NEAR. - ASSERT_NOT_OK(isValid( - "{$and: [" - " {a: {$geoNear: [0, 0]}}," - " {b: {$near: [0, 0]}}" - "]}", - *lpq - )); - - // Invalid: >1 GEO_NEAR. - ASSERT_NOT_OK(isValid( - "{$and: [" - " {$and: [" - " {a: {$near: [0, 0]}}," - " {b: 1}" - " ]}," - " {$and: [" - " {c: {$near: [0, 0]}}," - " {d: 1}" - " ]}" - "]}", - *lpq - )); - - // Invalid: GEO_NEAR inside NOR. - ASSERT_NOT_OK(isValid( - "{$nor: [" - " {a: {$near: [0, 0]}}," - " {b: 1}" - "]}", - *lpq - )); - - // Invalid: GEO_NEAR inside OR. - ASSERT_NOT_OK(isValid( - "{$or: [" - " {a: {$near: [0, 0]}}," - " {b: 1}" - "]}", - *lpq - )); - } - - TEST(CanonicalQueryTest, IsValidTextAndGeo) { - // Passes in default values for LiteParsedQuery. - // Filter inside LiteParsedQuery is not used. - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Invalid: TEXT and GEO_NEAR. - ASSERT_NOT_OK(isValid("{$text: {$search: 's'}, a: {$near: [0, 0]}}", *lpq)); - - // Invalid: TEXT and GEO_NEAR. - ASSERT_NOT_OK(isValid("{$text: {$search: 's'}, a: {$geoNear: [0, 0]}}", *lpq)); - - // Invalid: TEXT and GEO_NEAR. - ASSERT_NOT_OK(isValid( - "{$or: [" - " {$text: {$search: 's'}}," - " {a: 1}" - " ]," - " b: {$near: [0, 0]}}", - *lpq - )); - } - - TEST(CanonicalQueryTest, IsValidTextAndNaturalAscending) { - // Passes in default values for LiteParsedQuery except for sort order. - // Filter inside LiteParsedQuery is not used. - BSONObj sort = fromjson("{$natural: 1}"); - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - sort, - BSONObj(), - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Invalid: TEXT and {$natural: 1} sort order. - ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); - } - - TEST(CanonicalQueryTest, IsValidTextAndNaturalDescending) { - // Passes in default values for LiteParsedQuery except for sort order. - // Filter inside LiteParsedQuery is not used. - BSONObj sort = fromjson("{$natural: -1}"); - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - sort, - BSONObj(), - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Invalid: TEXT and {$natural: -1} sort order. - ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); - } - - TEST(CanonicalQueryTest, IsValidTextAndHint) { - // Passes in default values for LiteParsedQuery except for hint. - // Filter inside LiteParsedQuery is not used. - BSONObj hint = fromjson("{a: 1}"); - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - BSONObj(), - hint, - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Invalid: TEXT and {$natural: -1} sort order. - ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); - } - - // SERVER-14366 - TEST(CanonicalQueryTest, IsValidGeoNearNaturalSort) { - // Passes in default values for LiteParsedQuery except for sort order. - // Filter inside LiteParsedQuery is not used. - BSONObj sort = fromjson("{$natural: 1}"); - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - sort, - BSONObj(), - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Invalid: GEO_NEAR and {$natural: 1} sort order. - ASSERT_NOT_OK(isValid("{a: {$near: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}", - *lpq)); - } - - // SERVER-14366 - TEST(CanonicalQueryTest, IsValidGeoNearNaturalHint) { - // Passes in default values for LiteParsedQuery except for the hint. - // Filter inside LiteParsedQuery is not used. - BSONObj hint = fromjson("{$natural: 1}"); - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - BSONObj(), - hint, - BSONObj(), - BSONObj(), - false, // snapshot - false))); // explain - - // Invalid: GEO_NEAR and {$natural: 1} hint. - ASSERT_NOT_OK(isValid("{a: {$near: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}", - *lpq)); - } - - TEST(CanonicalQueryTest, IsValidTextAndSnapshot) { - // Passes in default values for LiteParsedQuery except for snapshot. - // Filter inside LiteParsedQuery is not used. - bool snapshot = true; - unique_ptr<LiteParsedQuery> lpq( - assertGet(LiteParsedQuery::makeAsOpQuery(ns, - 0, - 0, - 0, - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - BSONObj(), - snapshot, - false))); // explain - - // Invalid: TEXT and snapshot. - ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); - } - - // - // Tests for CanonicalQuery::sortTree - // - - /** - * Helper function for testing CanonicalQuery::sortTree(). - * - * Verifies that sorting the expression 'unsortedQueryStr' yields an expression equivalent to - * the expression 'sortedQueryStr'. - */ - void testSortTree(const char* unsortedQueryStr, const char* sortedQueryStr) { - BSONObj unsortedQueryObj = fromjson(unsortedQueryStr); - unique_ptr<MatchExpression> unsortedQueryExpr(parseMatchExpression(unsortedQueryObj)); - - BSONObj sortedQueryObj = fromjson(sortedQueryStr); - unique_ptr<MatchExpression> sortedQueryExpr(parseMatchExpression(sortedQueryObj)); - - // Sanity check that the unsorted expression is not equivalent to the sorted expression. - assertNotEquivalent(unsortedQueryStr, unsortedQueryExpr.get(), sortedQueryExpr.get()); - - // Sanity check that sorting the sorted expression is a no-op. - { - unique_ptr<MatchExpression> sortedQueryExprClone(parseMatchExpression(sortedQueryObj)); - CanonicalQuery::sortTree(sortedQueryExprClone.get()); - assertEquivalent(unsortedQueryStr, sortedQueryExpr.get(), sortedQueryExprClone.get()); - } - - // Test that sorting the unsorted expression yields the sorted expression. - CanonicalQuery::sortTree(unsortedQueryExpr.get()); - assertEquivalent(unsortedQueryStr, unsortedQueryExpr.get(), sortedQueryExpr.get()); - } - - // Test that an EQ expression sorts before a GT expression. - TEST(CanonicalQueryTest, SortTreeMatchTypeComparison) { - testSortTree("{a: {$gt: 1}, a: 1}", "{a: 1, a: {$gt: 1}}"); - } - - // Test that an EQ expression on path "a" sorts before an EQ expression on path "b". - TEST(CanonicalQueryTest, SortTreePathComparison) { - testSortTree("{b: 1, a: 1}", "{a: 1, b: 1}"); - testSortTree("{'a.b': 1, a: 1}", "{a: 1, 'a.b': 1}"); - testSortTree("{'a.c': 1, 'a.b': 1}", "{'a.b': 1, 'a.c': 1}"); - } - - // Test that AND expressions sort according to their first differing child. - TEST(CanonicalQueryTest, SortTreeChildComparison) { - testSortTree("{$or: [{a: 1, c: 1}, {a: 1, b: 1}]}", "{$or: [{a: 1, b: 1}, {a: 1, c: 1}]}"); - } - - // Test that an AND with 2 children sorts before an AND with 3 children, if the first 2 children - // are equivalent in both. - TEST(CanonicalQueryTest, SortTreeNumChildrenComparison) { - testSortTree("{$or: [{a: 1, b: 1, c: 1}, {a: 1, b: 1}]}", - "{$or: [{a: 1, b: 1}, {a: 1, b: 1, c: 1}]}"); +/** + * Helper function which parses and normalizes 'queryStr', and returns whether the given + * (expression tree, lite parsed query) tuple passes CanonicalQuery::isValid(). + * Returns Status::OK() if the tuple is valid, else returns an error Status. + */ +Status isValid(const std::string& queryStr, const LiteParsedQuery& lpqRaw) { + BSONObj queryObj = fromjson(queryStr); + std::unique_ptr<MatchExpression> me( + CanonicalQuery::normalizeTree(parseMatchExpression(queryObj))); + return CanonicalQuery::isValid(me.get(), lpqRaw); +} + +void assertEquivalent(const char* queryStr, + const MatchExpression* expected, + const MatchExpression* actual) { + if (actual->equivalent(expected)) { + return; } - - // - // Tests for CanonicalQuery::logicalRewrite - // - - /** - * Utility function to create a CanonicalQuery - */ - CanonicalQuery* canonicalize(const char* queryStr) { - BSONObj queryObj = fromjson(queryStr); - CanonicalQuery* cq; - Status result = CanonicalQuery::canonicalize(ns, queryObj, &cq); - ASSERT_OK(result); - return cq; + mongoutils::str::stream ss; + ss << "Match expressions are not equivalent." + << "\nOriginal query: " << queryStr << "\nExpected: " << expected->toString() + << "\nActual: " << actual->toString(); + FAIL(ss); +} + +void assertNotEquivalent(const char* queryStr, + const MatchExpression* expected, + const MatchExpression* actual) { + if (!actual->equivalent(expected)) { + return; } + mongoutils::str::stream ss; + ss << "Match expressions are equivalent." + << "\nOriginal query: " << queryStr << "\nExpected: " << expected->toString() + << "\nActual: " << actual->toString(); + FAIL(ss); +} + + +TEST(CanonicalQueryTest, IsValidText) { + // Passes in default values for LiteParsedQuery. + // Filter inside LiteParsedQuery is not used. + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Valid: regular TEXT. + ASSERT_OK(isValid("{$text: {$search: 's'}}", *lpq)); + + // Valid: TEXT inside OR. + ASSERT_OK(isValid( + "{$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + "]}", + *lpq)); + + // Valid: TEXT outside NOR. + ASSERT_OK(isValid("{$text: {$search: 's'}, $nor: [{a: 1}, {b: 1}]}", *lpq)); + + // Invalid: TEXT inside NOR. + ASSERT_NOT_OK(isValid("{$nor: [{$text: {$search: 's'}}, {a: 1}]}", *lpq)); + + // Invalid: TEXT inside NOR. + ASSERT_NOT_OK(isValid( + "{$nor: [" + " {$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + " ]}," + " {a: 2}" + "]}", + *lpq)); + + // Invalid: >1 TEXT. + ASSERT_NOT_OK(isValid( + "{$and: [" + " {$text: {$search: 's'}}," + " {$text: {$search: 't'}}" + "]}", + *lpq)); + + // Invalid: >1 TEXT. + ASSERT_NOT_OK(isValid( + "{$and: [" + " {$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + " ]}," + " {$or: [" + " {$text: {$search: 't'}}," + " {b: 1}" + " ]}" + "]}", + *lpq)); +} + +TEST(CanonicalQueryTest, IsValidGeo) { + // Passes in default values for LiteParsedQuery. + // Filter inside LiteParsedQuery is not used. + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Valid: regular GEO_NEAR. + ASSERT_OK(isValid("{a: {$near: [0, 0]}}", *lpq)); + + // Valid: GEO_NEAR inside nested AND. + ASSERT_OK(isValid( + "{$and: [" + " {$and: [" + " {a: {$near: [0, 0]}}," + " {b: 1}" + " ]}," + " {c: 1}" + "]}", + *lpq)); + + // Invalid: >1 GEO_NEAR. + ASSERT_NOT_OK(isValid( + "{$and: [" + " {a: {$near: [0, 0]}}," + " {b: {$near: [0, 0]}}" + "]}", + *lpq)); + + // Invalid: >1 GEO_NEAR. + ASSERT_NOT_OK(isValid( + "{$and: [" + " {a: {$geoNear: [0, 0]}}," + " {b: {$near: [0, 0]}}" + "]}", + *lpq)); + + // Invalid: >1 GEO_NEAR. + ASSERT_NOT_OK(isValid( + "{$and: [" + " {$and: [" + " {a: {$near: [0, 0]}}," + " {b: 1}" + " ]}," + " {$and: [" + " {c: {$near: [0, 0]}}," + " {d: 1}" + " ]}" + "]}", + *lpq)); + + // Invalid: GEO_NEAR inside NOR. + ASSERT_NOT_OK(isValid( + "{$nor: [" + " {a: {$near: [0, 0]}}," + " {b: 1}" + "]}", + *lpq)); + + // Invalid: GEO_NEAR inside OR. + ASSERT_NOT_OK(isValid( + "{$or: [" + " {a: {$near: [0, 0]}}," + " {b: 1}" + "]}", + *lpq)); +} + +TEST(CanonicalQueryTest, IsValidTextAndGeo) { + // Passes in default values for LiteParsedQuery. + // Filter inside LiteParsedQuery is not used. + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Invalid: TEXT and GEO_NEAR. + ASSERT_NOT_OK(isValid("{$text: {$search: 's'}, a: {$near: [0, 0]}}", *lpq)); + + // Invalid: TEXT and GEO_NEAR. + ASSERT_NOT_OK(isValid("{$text: {$search: 's'}, a: {$geoNear: [0, 0]}}", *lpq)); + + // Invalid: TEXT and GEO_NEAR. + ASSERT_NOT_OK(isValid( + "{$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + " ]," + " b: {$near: [0, 0]}}", + *lpq)); +} + +TEST(CanonicalQueryTest, IsValidTextAndNaturalAscending) { + // Passes in default values for LiteParsedQuery except for sort order. + // Filter inside LiteParsedQuery is not used. + BSONObj sort = fromjson("{$natural: 1}"); + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + sort, + BSONObj(), + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Invalid: TEXT and {$natural: 1} sort order. + ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); +} + +TEST(CanonicalQueryTest, IsValidTextAndNaturalDescending) { + // Passes in default values for LiteParsedQuery except for sort order. + // Filter inside LiteParsedQuery is not used. + BSONObj sort = fromjson("{$natural: -1}"); + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + sort, + BSONObj(), + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Invalid: TEXT and {$natural: -1} sort order. + ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); +} + +TEST(CanonicalQueryTest, IsValidTextAndHint) { + // Passes in default values for LiteParsedQuery except for hint. + // Filter inside LiteParsedQuery is not used. + BSONObj hint = fromjson("{a: 1}"); + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + BSONObj(), + hint, + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Invalid: TEXT and {$natural: -1} sort order. + ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); +} + +// SERVER-14366 +TEST(CanonicalQueryTest, IsValidGeoNearNaturalSort) { + // Passes in default values for LiteParsedQuery except for sort order. + // Filter inside LiteParsedQuery is not used. + BSONObj sort = fromjson("{$natural: 1}"); + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + sort, + BSONObj(), + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Invalid: GEO_NEAR and {$natural: 1} sort order. + ASSERT_NOT_OK(isValid("{a: {$near: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}", *lpq)); +} + +// SERVER-14366 +TEST(CanonicalQueryTest, IsValidGeoNearNaturalHint) { + // Passes in default values for LiteParsedQuery except for the hint. + // Filter inside LiteParsedQuery is not used. + BSONObj hint = fromjson("{$natural: 1}"); + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + BSONObj(), + hint, + BSONObj(), + BSONObj(), + false, // snapshot + false))); // explain + + // Invalid: GEO_NEAR and {$natural: 1} hint. + ASSERT_NOT_OK(isValid("{a: {$near: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}", *lpq)); +} + +TEST(CanonicalQueryTest, IsValidTextAndSnapshot) { + // Passes in default values for LiteParsedQuery except for snapshot. + // Filter inside LiteParsedQuery is not used. + bool snapshot = true; + unique_ptr<LiteParsedQuery> lpq(assertGet(LiteParsedQuery::makeAsOpQuery(ns, + 0, + 0, + 0, + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + BSONObj(), + snapshot, + false))); // explain + + // Invalid: TEXT and snapshot. + ASSERT_NOT_OK(isValid("{$text: {$search: 's'}}", *lpq)); +} + +// +// Tests for CanonicalQuery::sortTree +// - CanonicalQuery* canonicalize(const char* queryStr, const char* sortStr, - const char* projStr) { - BSONObj queryObj = fromjson(queryStr); - BSONObj sortObj = fromjson(sortStr); - BSONObj projObj = fromjson(projStr); - CanonicalQuery* cq; - Status result = CanonicalQuery::canonicalize(ns, queryObj, sortObj, - projObj, - &cq); - ASSERT_OK(result); - return cq; - } +/** + * Helper function for testing CanonicalQuery::sortTree(). + * + * Verifies that sorting the expression 'unsortedQueryStr' yields an expression equivalent to + * the expression 'sortedQueryStr'. + */ +void testSortTree(const char* unsortedQueryStr, const char* sortedQueryStr) { + BSONObj unsortedQueryObj = fromjson(unsortedQueryStr); + unique_ptr<MatchExpression> unsortedQueryExpr(parseMatchExpression(unsortedQueryObj)); - // Don't do anything with a double OR. - TEST(CanonicalQueryTest, RewriteNoDoubleOr) { - string queryStr = "{$or:[{a:1}, {b:1}], $or:[{c:1}, {d:1}], e:1}"; - BSONObj queryObj = fromjson(queryStr); - unique_ptr<MatchExpression> base(parseMatchExpression(queryObj)); - unique_ptr<MatchExpression> rewrite(CanonicalQuery::logicalRewrite(base->shallowClone())); - assertEquivalent(queryStr.c_str(), base.get(), rewrite.get()); - } + BSONObj sortedQueryObj = fromjson(sortedQueryStr); + unique_ptr<MatchExpression> sortedQueryExpr(parseMatchExpression(sortedQueryObj)); - // Do something with a single or. - TEST(CanonicalQueryTest, RewriteSingleOr) { - // Rewrite of this... - string queryStr = "{$or:[{a:1}, {b:1}], e:1}"; - BSONObj queryObj = fromjson(queryStr); - unique_ptr<MatchExpression> rewrite(CanonicalQuery::logicalRewrite(parseMatchExpression(queryObj))); - - // Should look like this. - string rewriteStr = "{$or:[{a:1, e:1}, {b:1, e:1}]}"; - BSONObj rewriteObj = fromjson(rewriteStr); - unique_ptr<MatchExpression> base(parseMatchExpression(rewriteObj)); - assertEquivalent(queryStr.c_str(), base.get(), rewrite.get()); - } + // Sanity check that the unsorted expression is not equivalent to the sorted expression. + assertNotEquivalent(unsortedQueryStr, unsortedQueryExpr.get(), sortedQueryExpr.get()); - /** - * Test function for CanonicalQuery::normalize. - */ - void testNormalizeQuery(const char* queryStr, const char* expectedExprStr) { - unique_ptr<CanonicalQuery> cq(canonicalize(queryStr)); - MatchExpression* me = cq->root(); - BSONObj expectedExprObj = fromjson(expectedExprStr); - unique_ptr<MatchExpression> expectedExpr(parseMatchExpression(expectedExprObj)); - assertEquivalent(queryStr, expectedExpr.get(), me); + // Sanity check that sorting the sorted expression is a no-op. + { + unique_ptr<MatchExpression> sortedQueryExprClone(parseMatchExpression(sortedQueryObj)); + CanonicalQuery::sortTree(sortedQueryExprClone.get()); + assertEquivalent(unsortedQueryStr, sortedQueryExpr.get(), sortedQueryExprClone.get()); } - TEST(CanonicalQueryTest, NormalizeQuerySort) { - // Field names - testNormalizeQuery("{b: 1, a: 1}", "{a: 1, b: 1}"); - // Operator types - testNormalizeQuery("{a: {$gt: 5}, a: {$lt: 10}}}", "{a: {$lt: 10}, a: {$gt: 5}}"); - // Nested queries - testNormalizeQuery("{a: {$elemMatch: {c: 1, b:1}}}", - "{a: {$elemMatch: {b: 1, c:1}}}"); - } + // Test that sorting the unsorted expression yields the sorted expression. + CanonicalQuery::sortTree(unsortedQueryExpr.get()); + assertEquivalent(unsortedQueryStr, unsortedQueryExpr.get(), sortedQueryExpr.get()); +} + +// Test that an EQ expression sorts before a GT expression. +TEST(CanonicalQueryTest, SortTreeMatchTypeComparison) { + testSortTree("{a: {$gt: 1}, a: 1}", "{a: 1, a: {$gt: 1}}"); +} + +// Test that an EQ expression on path "a" sorts before an EQ expression on path "b". +TEST(CanonicalQueryTest, SortTreePathComparison) { + testSortTree("{b: 1, a: 1}", "{a: 1, b: 1}"); + testSortTree("{'a.b': 1, a: 1}", "{a: 1, 'a.b': 1}"); + testSortTree("{'a.c': 1, 'a.b': 1}", "{'a.b': 1, 'a.c': 1}"); +} + +// Test that AND expressions sort according to their first differing child. +TEST(CanonicalQueryTest, SortTreeChildComparison) { + testSortTree("{$or: [{a: 1, c: 1}, {a: 1, b: 1}]}", "{$or: [{a: 1, b: 1}, {a: 1, c: 1}]}"); +} + +// Test that an AND with 2 children sorts before an AND with 3 children, if the first 2 children +// are equivalent in both. +TEST(CanonicalQueryTest, SortTreeNumChildrenComparison) { + testSortTree("{$or: [{a: 1, b: 1, c: 1}, {a: 1, b: 1}]}", + "{$or: [{a: 1, b: 1}, {a: 1, b: 1, c: 1}]}"); +} + +// +// Tests for CanonicalQuery::logicalRewrite +// - TEST(CanonicalQueryTest, NormalizeQueryTree) { - // Single-child $or elimination. - testNormalizeQuery("{$or: [{b: 1}]}", "{b: 1}"); - // Single-child $and elimination. - testNormalizeQuery("{$or: [{$and: [{a: 1}]}, {b: 1}]}", "{$or: [{a: 1}, {b: 1}]}"); - // $or absorbs $or children. - testNormalizeQuery("{$or: [{a: 1}, {$or: [{b: 1}, {$or: [{c: 1}]}]}, {d: 1}]}", - "{$or: [{a: 1}, {b: 1}, {c: 1}, {d: 1}]}"); - // $and absorbs $and children. - testNormalizeQuery("{$and: [{$and: [{a: 1}, {b: 1}]}, {c: 1}]}", - "{$and: [{a: 1}, {b: 1}, {c: 1}]}"); - } +/** + * Utility function to create a CanonicalQuery + */ +CanonicalQuery* canonicalize(const char* queryStr) { + BSONObj queryObj = fromjson(queryStr); + CanonicalQuery* cq; + Status result = CanonicalQuery::canonicalize(ns, queryObj, &cq); + ASSERT_OK(result); + return cq; +} + +CanonicalQuery* canonicalize(const char* queryStr, const char* sortStr, const char* projStr) { + BSONObj queryObj = fromjson(queryStr); + BSONObj sortObj = fromjson(sortStr); + BSONObj projObj = fromjson(projStr); + CanonicalQuery* cq; + Status result = CanonicalQuery::canonicalize(ns, queryObj, sortObj, projObj, &cq); + ASSERT_OK(result); + return cq; +} + +// Don't do anything with a double OR. +TEST(CanonicalQueryTest, RewriteNoDoubleOr) { + string queryStr = "{$or:[{a:1}, {b:1}], $or:[{c:1}, {d:1}], e:1}"; + BSONObj queryObj = fromjson(queryStr); + unique_ptr<MatchExpression> base(parseMatchExpression(queryObj)); + unique_ptr<MatchExpression> rewrite(CanonicalQuery::logicalRewrite(base->shallowClone())); + assertEquivalent(queryStr.c_str(), base.get(), rewrite.get()); +} + +// Do something with a single or. +TEST(CanonicalQueryTest, RewriteSingleOr) { + // Rewrite of this... + string queryStr = "{$or:[{a:1}, {b:1}], e:1}"; + BSONObj queryObj = fromjson(queryStr); + unique_ptr<MatchExpression> rewrite( + CanonicalQuery::logicalRewrite(parseMatchExpression(queryObj))); + + // Should look like this. + string rewriteStr = "{$or:[{a:1, e:1}, {b:1, e:1}]}"; + BSONObj rewriteObj = fromjson(rewriteStr); + unique_ptr<MatchExpression> base(parseMatchExpression(rewriteObj)); + assertEquivalent(queryStr.c_str(), base.get(), rewrite.get()); +} -} // namespace -} // namespace mongo +/** + * Test function for CanonicalQuery::normalize. + */ +void testNormalizeQuery(const char* queryStr, const char* expectedExprStr) { + unique_ptr<CanonicalQuery> cq(canonicalize(queryStr)); + MatchExpression* me = cq->root(); + BSONObj expectedExprObj = fromjson(expectedExprStr); + unique_ptr<MatchExpression> expectedExpr(parseMatchExpression(expectedExprObj)); + assertEquivalent(queryStr, expectedExpr.get(), me); +} + +TEST(CanonicalQueryTest, NormalizeQuerySort) { + // Field names + testNormalizeQuery("{b: 1, a: 1}", "{a: 1, b: 1}"); + // Operator types + testNormalizeQuery("{a: {$gt: 5}, a: {$lt: 10}}}", "{a: {$lt: 10}, a: {$gt: 5}}"); + // Nested queries + testNormalizeQuery("{a: {$elemMatch: {c: 1, b:1}}}", "{a: {$elemMatch: {b: 1, c:1}}}"); +} + +TEST(CanonicalQueryTest, NormalizeQueryTree) { + // Single-child $or elimination. + testNormalizeQuery("{$or: [{b: 1}]}", "{b: 1}"); + // Single-child $and elimination. + testNormalizeQuery("{$or: [{$and: [{a: 1}]}, {b: 1}]}", "{$or: [{a: 1}, {b: 1}]}"); + // $or absorbs $or children. + testNormalizeQuery("{$or: [{a: 1}, {$or: [{b: 1}, {$or: [{c: 1}]}]}, {d: 1}]}", + "{$or: [{a: 1}, {b: 1}, {c: 1}, {d: 1}]}"); + // $and absorbs $and children. + testNormalizeQuery("{$and: [{$and: [{a: 1}, {b: 1}]}, {c: 1}]}", + "{$and: [{a: 1}, {b: 1}, {c: 1}]}"); +} + +} // namespace +} // namespace mongo |