summaryrefslogtreecommitdiff
path: root/src/mongo/db/query/canonical_query_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/query/canonical_query_test.cpp')
-rw-r--r--src/mongo/db/query/canonical_query_test.cpp1003
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