2
* Copyright (C) 2014 MongoDB Inc.
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU Affero General Public License, version 3,
6
* as published by the Free Software Foundation.
8
* This program is distributed in the hope that it will be useful,
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
* GNU Affero General Public License for more details.
13
* You should have received a copy of the GNU Affero General Public License
14
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16
* As a special exception, the copyright holders give permission to link the
17
* code of portions of this program with the OpenSSL library under certain
18
* conditions as described in each individual source file and distribute
19
* linked combinations including the program with the OpenSSL library. You
20
* must comply with the GNU Affero General Public License in all respects for
21
* all of the code used other than as permitted herein. If you modify file(s)
22
* with this exception, you may extend this exception to your version of the
23
* file(s), but you are not obligated to do so. If you do not wish to do so,
24
* delete this exception statement from your version. If you delete this
25
* exception statement from all source files in the program, then also delete
26
* it in the license file.
29
#include "mongo/db/query/canonical_query.h"
31
#include "mongo/db/json.h"
32
#include "mongo/unittest/unittest.h"
34
using namespace mongo;
38
static const char* ns = "somebogusns";
41
* Utility function to parse the given JSON as a MatchExpression and normalize the expression
42
* tree. Returns the resulting tree, or an error Status.
44
StatusWithMatchExpression parseNormalize(const std::string& queryStr) {
45
StatusWithMatchExpression swme = MatchExpressionParser::parse(fromjson(queryStr));
46
if (!swme.getStatus().isOK()) {
49
return StatusWithMatchExpression(CanonicalQuery::normalizeTree(swme.getValue()));
52
TEST(CanonicalQueryTest, IsValidText) {
53
// Passes in default values for LiteParsedQuery.
54
// Filter inside LiteParsedQuery is not used.
55
LiteParsedQuery* lpqRaw;
56
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
57
fromjson("{}"), fromjson("{}"), fromjson("{}"),
62
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
64
auto_ptr<MatchExpression> me;
65
StatusWithMatchExpression swme(Status::OK());
67
// Valid: regular TEXT.
68
swme = parseNormalize("{$text: {$search: 's'}}");
69
ASSERT_OK(swme.getStatus());
70
me.reset(swme.getValue());
71
ASSERT_OK(CanonicalQuery::isValid(me.get(), *lpq));
73
// Valid: TEXT inside OR.
74
swme = parseNormalize(
76
" {$text: {$search: 's'}},"
80
ASSERT_OK(swme.getStatus());
81
me.reset(swme.getValue());
82
ASSERT_OK(CanonicalQuery::isValid(me.get(), *lpq));
84
// Valid: TEXT outside NOR.
85
swme = parseNormalize("{$text: {$search: 's'}, $nor: [{a: 1}, {b: 1}]}");
86
ASSERT_OK(swme.getStatus());
87
me.reset(swme.getValue());
88
ASSERT_OK(CanonicalQuery::isValid(me.get(), *lpq));
90
// Invalid: TEXT inside NOR.
91
swme = parseNormalize("{$nor: [{$text: {$search: 's'}}, {a: 1}]}");
92
ASSERT_OK(swme.getStatus());
93
me.reset(swme.getValue());
94
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
96
// Invalid: TEXT inside NOR.
97
swme = parseNormalize(
100
" {$text: {$search: 's'}},"
106
ASSERT_OK(swme.getStatus());
107
me.reset(swme.getValue());
108
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
111
swme = parseNormalize(
113
" {$text: {$search: 's'}},"
114
" {$text: {$search: 't'}}"
117
ASSERT_OK(swme.getStatus());
118
me.reset(swme.getValue());
119
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
122
swme = parseNormalize(
125
" {$text: {$search: 's'}},"
129
" {$text: {$search: 't'}},"
134
ASSERT_OK(swme.getStatus());
135
me.reset(swme.getValue());
136
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
139
TEST(CanonicalQueryTest, IsValidGeo) {
140
// Passes in default values for LiteParsedQuery.
141
// Filter inside LiteParsedQuery is not used.
142
LiteParsedQuery* lpqRaw;
143
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
144
fromjson("{}"), fromjson("{}"), fromjson("{}"),
149
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
151
auto_ptr<MatchExpression> me;
152
StatusWithMatchExpression swme(Status::OK());
154
// Valid: regular GEO_NEAR.
155
swme = parseNormalize("{a: {$near: [0, 0]}}");
156
ASSERT_OK(swme.getStatus());
157
me.reset(swme.getValue());
158
ASSERT_OK(CanonicalQuery::isValid(me.get(), *lpq));
160
// Valid: GEO_NEAR inside nested AND.
161
swme = parseNormalize(
164
" {a: {$near: [0, 0]}},"
170
ASSERT_OK(swme.getStatus());
171
me.reset(swme.getValue());
172
ASSERT_OK(CanonicalQuery::isValid(me.get(), *lpq));
174
// Invalid: >1 GEO_NEAR.
175
swme = parseNormalize(
177
" {a: {$near: [0, 0]}},"
178
" {b: {$near: [0, 0]}}"
181
ASSERT_OK(swme.getStatus());
182
me.reset(swme.getValue());
183
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
185
// Invalid: >1 GEO_NEAR.
186
swme = parseNormalize(
188
" {a: {$geoNear: [0, 0]}},"
189
" {b: {$near: [0, 0]}}"
192
ASSERT_OK(swme.getStatus());
193
me.reset(swme.getValue());
194
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
196
// Invalid: >1 GEO_NEAR.
197
swme = parseNormalize(
200
" {a: {$near: [0, 0]}},"
204
" {c: {$near: [0, 0]}},"
209
ASSERT_OK(swme.getStatus());
210
me.reset(swme.getValue());
211
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
213
// Invalid: GEO_NEAR inside NOR.
214
swme = parseNormalize(
216
" {a: {$near: [0, 0]}},"
220
ASSERT_OK(swme.getStatus());
221
me.reset(swme.getValue());
222
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
224
// Invalid: GEO_NEAR inside OR.
225
swme = parseNormalize(
227
" {a: {$near: [0, 0]}},"
231
ASSERT_OK(swme.getStatus());
232
me.reset(swme.getValue());
233
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
236
TEST(CanonicalQueryTest, IsValidTextAndGeo) {
237
// Passes in default values for LiteParsedQuery.
238
// Filter inside LiteParsedQuery is not used.
239
LiteParsedQuery* lpqRaw;
240
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
241
fromjson("{}"), fromjson("{}"), fromjson("{}"),
246
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
248
auto_ptr<MatchExpression> me;
249
StatusWithMatchExpression swme(Status::OK());
251
// Invalid: TEXT and GEO_NEAR.
252
swme = parseNormalize("{$text: {$search: 's'}, a: {$near: [0, 0]}}");
253
ASSERT_OK(swme.getStatus());
254
me.reset(swme.getValue());
255
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
257
// Invalid: TEXT and GEO_NEAR.
258
swme = parseNormalize("{$text: {$search: 's'}, a: {$geoNear: [0, 0]}}");
259
ASSERT_OK(swme.getStatus());
260
me.reset(swme.getValue());
261
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
263
// Invalid: TEXT and GEO_NEAR.
264
swme = parseNormalize(
266
" {$text: {$search: 's'}},"
269
" b: {$near: [0, 0]}}"
271
ASSERT_OK(swme.getStatus());
272
me.reset(swme.getValue());
273
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
276
TEST(CanonicalQueryTest, IsValidTextAndNaturalAscending) {
277
// Passes in default values for LiteParsedQuery except for sort order.
278
// Filter inside LiteParsedQuery is not used.
279
LiteParsedQuery* lpqRaw;
280
BSONObj sort = fromjson("{$natural: 1}");
281
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
282
sort, fromjson("{}"), fromjson("{}"),
287
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
289
auto_ptr<MatchExpression> me;
290
StatusWithMatchExpression swme(Status::OK());
292
// Invalid: TEXT and {$natural: 1} sort order.
293
swme = parseNormalize("{$text: {$search: 's'}}");
294
ASSERT_OK(swme.getStatus());
295
me.reset(swme.getValue());
296
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
299
TEST(CanonicalQueryTest, IsValidTextAndNaturalDescending) {
300
// Passes in default values for LiteParsedQuery except for sort order.
301
// Filter inside LiteParsedQuery is not used.
302
LiteParsedQuery* lpqRaw;
303
BSONObj sort = fromjson("{$natural: -1}");
304
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
305
sort, fromjson("{}"), fromjson("{}"),
310
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
312
auto_ptr<MatchExpression> me;
313
StatusWithMatchExpression swme(Status::OK());
315
// Invalid: TEXT and {$natural: -1} sort order.
316
swme = parseNormalize("{$text: {$search: 's'}}");
317
ASSERT_OK(swme.getStatus());
318
me.reset(swme.getValue());
319
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
322
TEST(CanonicalQueryTest, IsValidTextAndHint) {
323
// Passes in default values for LiteParsedQuery except for hint.
324
// Filter inside LiteParsedQuery is not used.
325
LiteParsedQuery* lpqRaw;
326
BSONObj hint = fromjson("{a: 1}");
327
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
328
fromjson("{}"), hint, fromjson("{}"),
333
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
335
auto_ptr<MatchExpression> me;
336
StatusWithMatchExpression swme(Status::OK());
338
// Invalid: TEXT and {$natural: -1} sort order.
339
swme = parseNormalize("{$text: {$search: 's'}}");
340
ASSERT_OK(swme.getStatus());
341
me.reset(swme.getValue());
342
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
345
TEST(CanonicalQueryTest, IsValidTextAndSnapshot) {
346
// Passes in default values for LiteParsedQuery except for snapshot.
347
// Filter inside LiteParsedQuery is not used.
348
LiteParsedQuery* lpqRaw;
349
bool snapshot = true;
350
ASSERT_OK(LiteParsedQuery::make(ns, 0, 0, 0, fromjson("{}"), fromjson("{}"),
351
fromjson("{}"), fromjson("{}"), fromjson("{}"),
356
auto_ptr<LiteParsedQuery> lpq(lpqRaw);
358
auto_ptr<MatchExpression> me;
359
StatusWithMatchExpression swme(Status::OK());
361
// Invalid: TEXT and snapshot.
362
swme = parseNormalize("{$text: {$search: 's'}}");
363
ASSERT_OK(swme.getStatus());
364
me.reset(swme.getValue());
365
ASSERT_NOT_OK(CanonicalQuery::isValid(me.get(), *lpq));
369
* Utility function to create a CanonicalQuery
371
CanonicalQuery* canonicalize(const char* queryStr) {
372
BSONObj queryObj = fromjson(queryStr);
374
Status result = CanonicalQuery::canonicalize(ns, queryObj, &cq);
379
CanonicalQuery* canonicalize(const char* queryStr, const char* sortStr,
380
const char* projStr) {
381
BSONObj queryObj = fromjson(queryStr);
382
BSONObj sortObj = fromjson(sortStr);
383
BSONObj projObj = fromjson(projStr);
385
Status result = CanonicalQuery::canonicalize(ns, queryObj, sortObj,
393
* Utility function to create MatchExpression
395
MatchExpression* parseMatchExpression(const BSONObj& obj) {
396
StatusWithMatchExpression status = MatchExpressionParser::parse(obj);
397
if (!status.isOK()) {
398
mongoutils::str::stream ss;
399
ss << "failed to parse query: " << obj.toString()
400
<< ". Reason: " << status.toString();
403
MatchExpression* expr(status.getValue());
407
void assertEquivalent(const char* queryStr,
408
const MatchExpression* expected,
409
const MatchExpression* actual) {
410
if (actual->equivalent(expected)) {
413
mongoutils::str::stream ss;
414
ss << "Match expressions are not equivalent."
415
<< "\nOriginal query: " << queryStr
416
<< "\nExpected: " << expected->toString()
417
<< "\nActual: " << actual->toString();
422
// Tests for CanonicalQuery::logicalRewrite
425
// Don't do anything with a double OR.
426
TEST(CanonicalQueryTest, RewriteNoDoubleOr) {
427
string queryStr = "{$or:[{a:1}, {b:1}], $or:[{c:1}, {d:1}], e:1}";
428
BSONObj queryObj = fromjson(queryStr);
429
auto_ptr<MatchExpression> base(parseMatchExpression(queryObj));
430
auto_ptr<MatchExpression> rewrite(CanonicalQuery::logicalRewrite(base->shallowClone()));
431
assertEquivalent(queryStr.c_str(), base.get(), rewrite.get());
434
// Do something with a single or.
435
TEST(CanonicalQueryTest, RewriteSingleOr) {
436
// Rewrite of this...
437
string queryStr = "{$or:[{a:1}, {b:1}], e:1}";
438
BSONObj queryObj = fromjson(queryStr);
439
auto_ptr<MatchExpression> rewrite(CanonicalQuery::logicalRewrite(parseMatchExpression(queryObj)));
441
// Should look like this.
442
string rewriteStr = "{$or:[{a:1, e:1}, {b:1, e:1}]}";
443
BSONObj rewriteObj = fromjson(rewriteStr);
444
auto_ptr<MatchExpression> base(parseMatchExpression(rewriteObj));
445
assertEquivalent(queryStr.c_str(), base.get(), rewrite.get());
449
* Test function for CanonicalQuery::normalize.
451
void testNormalizeQuery(const char* queryStr, const char* expectedExprStr) {
452
auto_ptr<CanonicalQuery> cq(canonicalize(queryStr));
453
MatchExpression* me = cq->root();
454
BSONObj expectedExprObj = fromjson(expectedExprStr);
455
auto_ptr<MatchExpression> expectedExpr(parseMatchExpression(expectedExprObj));
456
assertEquivalent(queryStr, expectedExpr.get(), me);
459
TEST(CanonicalQueryTest, NormalizeQuerySort) {
461
testNormalizeQuery("{b: 1, a: 1}", "{a: 1, b: 1}");
463
testNormalizeQuery("{a: {$gt: 5}, a: {$lt: 10}}}", "{a: {$lt: 10}, a: {$gt: 5}}");
465
testNormalizeQuery("{a: {$elemMatch: {c: 1, b:1}}}",
466
"{a: {$elemMatch: {b: 1, c:1}}}");
469
TEST(CanonicalQueryTest, NormalizeQueryTree) {
470
// Single-child $or elimination.
471
testNormalizeQuery("{$or: [{b: 1}]}", "{b: 1}");
472
// Single-child $and elimination.
473
testNormalizeQuery("{$or: [{$and: [{a: 1}]}, {b: 1}]}", "{$or: [{a: 1}, {b: 1}]}");
474
// $or absorbs $or children.
475
testNormalizeQuery("{$or: [{a: 1}, {$or: [{b: 1}, {$or: [{c: 1}]}]}, {d: 1}]}",
476
"{$or: [{a: 1}, {b: 1}, {c: 1}, {d: 1}]}");
477
// $and absorbs $and children.
478
testNormalizeQuery("{$and: [{$and: [{a: 1}, {b: 1}]}, {c: 1}]}",
479
"{$and: [{a: 1}, {b: 1}, {c: 1}]}");
483
* Test functions for getPlanCacheKey.
484
* Cache keys are intentionally obfuscated and are meaningful only
485
* within the current lifetime of the server process. Users should treat
486
* plan cache keys as opaque.
488
void testGetPlanCacheKey(const char* queryStr, const char* sortStr,
490
const char *expectedStr) {
491
auto_ptr<CanonicalQuery> cq(canonicalize(queryStr, sortStr, projStr));
492
const PlanCacheKey& key = cq->getPlanCacheKey();
493
PlanCacheKey expectedKey(expectedStr);
494
if (key == expectedKey) {
497
mongoutils::str::stream ss;
498
ss << "Unexpected plan cache key. Expected: " << expectedKey << ". Actual: " << key
499
<< ". Query: " << cq->toString();
503
TEST(PlanCacheTest, GetPlanCacheKey) {
504
// Generated cache keys should be treated as opaque to the user.
507
testGetPlanCacheKey("{}", "{}", "{}", "an");
508
testGetPlanCacheKey("{$or: [{a: 1}, {b: 2}]}", "{}", "{}", "or[eqa,eqb]");
509
testGetPlanCacheKey("{$or: [{a: 1}, {b: 1}, {c: 1}], d: 1}", "{}", "{}",
510
"an[or[eqa,eqb,eqc],eqd]");
511
testGetPlanCacheKey("{$or: [{a: 1}, {b: 1}], c: 1, d: 1}", "{}", "{}",
512
"an[or[eqa,eqb],eqc,eqd]");
513
testGetPlanCacheKey("{a: 1, b: 1, c: 1}", "{}", "{}", "an[eqa,eqb,eqc]");
514
testGetPlanCacheKey("{a: 1, beqc: 1}", "{}", "{}", "an[eqa,eqbeqc]");
515
testGetPlanCacheKey("{ap1a: 1}", "{}", "{}", "eqap1a");
516
testGetPlanCacheKey("{aab: 1}", "{}", "{}", "eqaab");
519
testGetPlanCacheKey("{}", "{a: 1}", "{}", "an~aa");
520
testGetPlanCacheKey("{}", "{a: -1}", "{}", "an~da");
521
testGetPlanCacheKey("{}", "{a: {$meta: 'textScore'}}", "{a: {$meta: 'textScore'}}",
522
"an~ta|{ $meta: \"textScore\" }a");
523
testGetPlanCacheKey("{a: 1}", "{b: 1}", "{}", "eqa~ab");
526
testGetPlanCacheKey("{}", "{}", "{a: 1}", "an|1a");
527
testGetPlanCacheKey("{}", "{}", "{a: 0}", "an|0a");
528
testGetPlanCacheKey("{}", "{}", "{a: 99}", "an|99a");
529
testGetPlanCacheKey("{}", "{}", "{a: 'foo'}", "an|\"foo\"a");
530
testGetPlanCacheKey("{}", "{}", "{a: {$slice: [3, 5]}}", "an|{ $slice: \\[ 3\\, 5 \\] }a");
531
testGetPlanCacheKey("{}", "{}", "{a: {$elemMatch: {x: 2}}}",
532
"an|{ $elemMatch: { x: 2 } }a");
533
testGetPlanCacheKey("{a: 1}", "{}", "{'a.$': 1}", "eqa|1a.$");
534
testGetPlanCacheKey("{a: 1}", "{}", "{a: 1}", "eqa|1a");
536
// Projection should be order-insensitive
537
testGetPlanCacheKey("{}", "{}", "{a: 1, b: 1}", "an|1a1b");
538
testGetPlanCacheKey("{}", "{}", "{b: 1, a: 1}", "an|1a1b");
540
// With or-elimination and projection
541
testGetPlanCacheKey("{$or: [{a: 1}]}", "{}", "{_id: 0, a: 1}", "eqa|0_id1a");
542
testGetPlanCacheKey("{$or: [{a: 1}]}", "{}", "{'a.$': 1}", "eqa|1a.$");
545
// Delimiters found in user field names or non-standard projection field values
547
TEST(PlanCacheTest, GetPlanCacheKeyEscaped) {
548
// Field name in query.
549
testGetPlanCacheKey("{'a,[]~|': 1}", "{}", "{}", "eqa\\,\\[\\]\\~\\|");
551
// Field name in sort.
552
testGetPlanCacheKey("{}", "{'a,[]~|': 1}", "{}", "an~aa\\,\\[\\]\\~\\|");
554
// Field name in projection.
555
testGetPlanCacheKey("{}", "{}", "{'a,[]~|': 1}", "an|1a\\,\\[\\]\\~\\|");
557
// Value in projection.
558
testGetPlanCacheKey("{}", "{}", "{a: 'foo,[]~|'}", "an|\"foo\\,\\[\\]\\~\\|\"a");
561
// Cache keys for $geoWithin queries with legacy and GeoJSON coordinates should
563
TEST(PlanCacheTest, GetPlanCacheKeyGeoWithin) {
564
// Legacy coordinates.
565
auto_ptr<CanonicalQuery> cqLegacy(canonicalize("{a: {$geoWithin: "
566
"{$box: [[-180, -90], [180, 90]]}}}"));
567
// GeoJSON coordinates.
568
auto_ptr<CanonicalQuery> cqNew(canonicalize("{a: {$geoWithin: "
569
"{$geometry: {type: 'Polygon', coordinates: "
570
"[[[0, 0], [0, 90], [90, 0], [0, 0]]]}}}}"));
571
ASSERT_NOT_EQUALS(cqLegacy->getPlanCacheKey(), cqNew->getPlanCacheKey());
574
// GEO_NEAR cache keys should include information on geometry and CRS in addition
575
// to the match type and field name.
576
TEST(PlanCacheTest, GetPlanCacheKeyGeoNear) {
577
testGetPlanCacheKey("{a: {$near: [0,0], $maxDistance:0.3 }}", "{}", "{}",
579
testGetPlanCacheKey("{a: {$nearSphere: [0,0], $maxDistance: 0.31 }}", "{}", "{}",
581
testGetPlanCacheKey("{a: {$geoNear: {$geometry: {type: 'Point', coordinates: [0,0]},"
582
"$maxDistance:100}}}", "{}", "{}",