/** * Copyright (C) 2018-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, * as published by MongoDB, Inc. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Server Side Public License for more details. * * You should have received a copy of the Server Side Public License * along with this program. If not, see * . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the Server Side Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/db/query/parsed_projection.h" #include "mongo/db/json.h" #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/query/query_test_service_context.h" #include "mongo/unittest/unittest.h" #include namespace { using std::string; using std::unique_ptr; using std::vector; using namespace mongo; // // creation function // unique_ptr createParsedProjection(const BSONObj& query, const BSONObj& projObj) { QueryTestServiceContext serviceCtx; auto opCtx = serviceCtx.makeOperationContext(); const CollatorInterface* collator = nullptr; const boost::intrusive_ptr expCtx( new ExpressionContext(opCtx.get(), collator)); StatusWithMatchExpression statusWithMatcher = MatchExpressionParser::parse(query, std::move(expCtx)); ASSERT(statusWithMatcher.isOK()); std::unique_ptr queryMatchExpr = std::move(statusWithMatcher.getValue()); ParsedProjection* out = nullptr; Status status = ParsedProjection::make(opCtx.get(), projObj, queryMatchExpr.get(), &out); if (!status.isOK()) { FAIL(str::stream() << "failed to parse projection " << projObj << " (query: " << query << "): " << status.toString()); } ASSERT(out); return unique_ptr(out); } unique_ptr createParsedProjection(const char* queryStr, const char* projStr) { BSONObj query = fromjson(queryStr); BSONObj projObj = fromjson(projStr); return createParsedProjection(query, projObj); } // // Failure to create a parsed projection is expected // void assertInvalidProjection(const char* queryStr, const char* projStr) { BSONObj query = fromjson(queryStr); BSONObj projObj = fromjson(projStr); QueryTestServiceContext serviceCtx; auto opCtx = serviceCtx.makeOperationContext(); const CollatorInterface* collator = nullptr; const boost::intrusive_ptr expCtx( new ExpressionContext(opCtx.get(), collator)); StatusWithMatchExpression statusWithMatcher = MatchExpressionParser::parse(query, std::move(expCtx)); ASSERT(statusWithMatcher.isOK()); std::unique_ptr queryMatchExpr = std::move(statusWithMatcher.getValue()); ParsedProjection* out = nullptr; Status status = ParsedProjection::make(opCtx.get(), projObj, queryMatchExpr.get(), &out); std::unique_ptr destroy(out); ASSERT(!status.isOK()); } // canonical_query.cpp will invoke ParsedProjection::make only when // the projection spec is non-empty. This test case is included for // completeness and do not reflect actual usage. TEST(ParsedProjectionTest, MakeId) { unique_ptr parsedProj(createParsedProjection("{}", "{}")); ASSERT(parsedProj->requiresDocument()); } TEST(ParsedProjectionTest, MakeEmpty) { unique_ptr parsedProj(createParsedProjection("{}", "{_id: 0}")); ASSERT(parsedProj->requiresDocument()); } TEST(ParsedProjectionTest, MakeSingleField) { unique_ptr parsedProj(createParsedProjection("{}", "{a: 1}")); ASSERT(!parsedProj->requiresDocument()); const vector& fields = parsedProj->getRequiredFields(); ASSERT_EQUALS(fields.size(), 2U); ASSERT_EQUALS(fields[0], "_id"); ASSERT_EQUALS(fields[1], "a"); } TEST(ParsedProjectionTest, MakeSingleFieldCovered) { unique_ptr parsedProj(createParsedProjection("{}", "{_id: 0, a: 1}")); ASSERT(!parsedProj->requiresDocument()); const vector& fields = parsedProj->getRequiredFields(); ASSERT_EQUALS(fields.size(), 1U); ASSERT_EQUALS(fields[0], "a"); } TEST(ParsedProjectionTest, MakeSingleFieldIDCovered) { unique_ptr parsedProj(createParsedProjection("{}", "{_id: 1}")); ASSERT(!parsedProj->requiresDocument()); const vector& fields = parsedProj->getRequiredFields(); ASSERT_EQUALS(fields.size(), 1U); ASSERT_EQUALS(fields[0], "_id"); } // boolean support is undocumented TEST(ParsedProjectionTest, MakeSingleFieldCoveredBoolean) { unique_ptr parsedProj(createParsedProjection("{}", "{_id: 0, a: true}")); ASSERT(!parsedProj->requiresDocument()); const vector& fields = parsedProj->getRequiredFields(); ASSERT_EQUALS(fields.size(), 1U); ASSERT_EQUALS(fields[0], "a"); } // boolean support is undocumented TEST(ParsedProjectionTest, MakeSingleFieldCoveredIdBoolean) { unique_ptr parsedProj(createParsedProjection("{}", "{_id: false, a: 1}")); ASSERT(!parsedProj->requiresDocument()); const vector& fields = parsedProj->getRequiredFields(); ASSERT_EQUALS(fields.size(), 1U); ASSERT_EQUALS(fields[0], "a"); } // // Positional operator validation // TEST(ParsedProjectionTest, InvalidPositionalOperatorProjections) { assertInvalidProjection("{}", "{'a.$': 1}"); assertInvalidProjection("{a: 1}", "{'b.$': 1}"); assertInvalidProjection("{a: 1}", "{'a.$': 0}"); assertInvalidProjection("{a: 1}", "{'a.$.d.$': 1}"); assertInvalidProjection("{a: 1}", "{'a.$.$': 1}"); assertInvalidProjection("{a: 1}", "{'a.$.$': 1}"); assertInvalidProjection("{a: 1, b: 1, c: 1}", "{'abc.$': 1}"); assertInvalidProjection("{$or: [{a: 1}, {$or: [{b: 1}, {c: 1}]}]}", "{'d.$': 1}"); assertInvalidProjection("{a: [1, 2, 3]}", "{'.$': 1}"); } TEST(ParsedProjectionTest, InvalidElemMatchTextProjection) { assertInvalidProjection("{}", "{a: {$elemMatch: {$text: {$search: 'str'}}}}"); } TEST(ParsedProjectionTest, InvalidElemMatchWhereProjection) { assertInvalidProjection("{}", "{a: {$elemMatch: {$where: 'this.a == this.b'}}}"); } TEST(ParsedProjectionTest, InvalidElemMatchGeoNearProjection) { assertInvalidProjection( "{}", "{a: {$elemMatch: {$nearSphere: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}}"); } TEST(ParsedProjectionTest, InvalidElemMatchExprProjection) { assertInvalidProjection("{}", "{a: {$elemMatch: {$expr: 5}}}"); } TEST(ParsedProjectionTest, ValidPositionalOperatorProjections) { createParsedProjection("{a: 1}", "{'a.$': 1}"); createParsedProjection("{a: 1}", "{'a.foo.bar.$': 1}"); createParsedProjection("{a: 1}", "{'a.foo.bar.$.x.y': 1}"); createParsedProjection("{'a.b.c': 1}", "{'a.b.c.$': 1}"); createParsedProjection("{'a.b.c': 1}", "{'a.e.f.$': 1}"); createParsedProjection("{a: {b: 1}}", "{'a.$': 1}"); createParsedProjection("{a: 1, b: 1}}", "{'a.$': 1}"); createParsedProjection("{a: 1, b: 1}}", "{'b.$': 1}"); createParsedProjection("{$and: [{a: 1}, {b: 1}]}", "{'a.$': 1}"); createParsedProjection("{$and: [{a: 1}, {b: 1}]}", "{'b.$': 1}"); createParsedProjection("{$or: [{a: 1}, {b: 1}]}", "{'a.$': 1}"); createParsedProjection("{$or: [{a: 1}, {b: 1}]}", "{'b.$': 1}"); createParsedProjection("{$and: [{$or: [{a: 1}, {$and: [{b: 1}, {c: 1}]}]}]}", "{'c.d.f.$': 1}"); // Fields with empty name can be projected using the positional $ operator. createParsedProjection("{'': [1, 2, 3]}", "{'.$': 1}"); } // Some match expressions (eg. $where) do not override MatchExpression::path() // In this test case, we use an internal match expression implementation ALWAYS_FALSE // to achieve the same effect. // Projection parser should handle this the same way as an empty path. TEST(ParsedProjectionTest, InvalidPositionalProjectionDefaultPathMatchExpression) { QueryTestServiceContext serviceCtx; auto opCtx = serviceCtx.makeOperationContext(); unique_ptr queryMatchExpr(new AlwaysFalseMatchExpression()); ASSERT(nullptr == queryMatchExpr->path().rawData()); ParsedProjection* out = nullptr; BSONObj projObj = fromjson("{'a.$': 1}"); Status status = ParsedProjection::make(opCtx.get(), projObj, queryMatchExpr.get(), &out); ASSERT(!status.isOK()); std::unique_ptr destroy(out); // Projecting onto empty field should fail. BSONObj emptyFieldProjObj = fromjson("{'.$': 1}"); status = ParsedProjection::make(opCtx.get(), emptyFieldProjObj, queryMatchExpr.get(), &out); ASSERT(!status.isOK()); } TEST(ParsedProjectionTest, ParsedProjectionDefaults) { auto parsedProjection = createParsedProjection("{}", "{}"); ASSERT_FALSE(parsedProjection->wantSortKey()); ASSERT_TRUE(parsedProjection->requiresDocument()); ASSERT_FALSE(parsedProjection->requiresMatchDetails()); ASSERT_FALSE(parsedProjection->wantGeoNearDistance()); ASSERT_FALSE(parsedProjection->wantGeoNearPoint()); } TEST(ParsedProjectionTest, SortKeyMetaProjection) { auto parsedProjection = createParsedProjection("{}", "{foo: {$meta: 'sortKey'}}"); ASSERT_BSONOBJ_EQ(parsedProjection->getProjObj(), fromjson("{foo: {$meta: 'sortKey'}}")); ASSERT_TRUE(parsedProjection->wantSortKey()); ASSERT_TRUE(parsedProjection->requiresDocument()); ASSERT_FALSE(parsedProjection->requiresMatchDetails()); ASSERT_FALSE(parsedProjection->wantGeoNearDistance()); ASSERT_FALSE(parsedProjection->wantGeoNearPoint()); } TEST(ParsedProjectionTest, SortKeyMetaProjectionCovered) { auto parsedProjection = createParsedProjection("{}", "{a: 1, foo: {$meta: 'sortKey'}, _id: 0}"); ASSERT_BSONOBJ_EQ(parsedProjection->getProjObj(), fromjson("{a: 1, foo: {$meta: 'sortKey'}, _id: 0}")); ASSERT_TRUE(parsedProjection->wantSortKey()); ASSERT_FALSE(parsedProjection->requiresDocument()); ASSERT_FALSE(parsedProjection->requiresMatchDetails()); ASSERT_FALSE(parsedProjection->wantGeoNearDistance()); ASSERT_FALSE(parsedProjection->wantGeoNearPoint()); } TEST(ParsedProjectionTest, SortKeyMetaAndSlice) { auto parsedProjection = createParsedProjection("{}", "{a: 1, foo: {$meta: 'sortKey'}, _id: 0, b: {$slice: 1}}"); ASSERT_BSONOBJ_EQ(parsedProjection->getProjObj(), fromjson("{a: 1, foo: {$meta: 'sortKey'}, _id: 0, b: {$slice: 1}}")); ASSERT_TRUE(parsedProjection->wantSortKey()); ASSERT_TRUE(parsedProjection->requiresDocument()); ASSERT_FALSE(parsedProjection->requiresMatchDetails()); ASSERT_FALSE(parsedProjection->wantGeoNearDistance()); ASSERT_FALSE(parsedProjection->wantGeoNearPoint()); } TEST(ParsedProjectionTest, SortKeyMetaAndElemMatch) { auto parsedProjection = createParsedProjection( "{}", "{a: 1, foo: {$meta: 'sortKey'}, _id: 0, b: {$elemMatch: {a: 1}}}"); ASSERT_BSONOBJ_EQ(parsedProjection->getProjObj(), fromjson("{a: 1, foo: {$meta: 'sortKey'}, _id: 0, b: {$elemMatch: {a: 1}}}")); ASSERT_TRUE(parsedProjection->wantSortKey()); ASSERT_TRUE(parsedProjection->requiresDocument()); ASSERT_FALSE(parsedProjection->requiresMatchDetails()); ASSERT_FALSE(parsedProjection->wantGeoNearDistance()); ASSERT_FALSE(parsedProjection->wantGeoNearPoint()); } TEST(ParsedProjectionTest, SortKeyMetaAndExclusion) { auto parsedProjection = createParsedProjection("{}", "{a: 0, foo: {$meta: 'sortKey'}, _id: 0}"); ASSERT_BSONOBJ_EQ(parsedProjection->getProjObj(), fromjson("{a: 0, foo: {$meta: 'sortKey'}, _id: 0}")); ASSERT_TRUE(parsedProjection->wantSortKey()); ASSERT_TRUE(parsedProjection->requiresDocument()); ASSERT_FALSE(parsedProjection->requiresMatchDetails()); ASSERT_FALSE(parsedProjection->wantGeoNearDistance()); ASSERT_FALSE(parsedProjection->wantGeoNearPoint()); } // // Cases for ParsedProjection::isFieldRetainedExactly(). // TEST(ParsedProjectionTest, InclusionProjectionPreservesChild) { auto parsedProjection = createParsedProjection("{}", "{a: 1}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a.b")); } TEST(ParsedProjectionTest, InclusionProjectionDoesNotPreserveParent) { auto parsedProjection = createParsedProjection("{}", "{'a.b': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, InclusionProjectionPreservesField) { auto parsedProjection = createParsedProjection("{}", "{a: 1}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, InclusionProjectionOrderingDeterminesPreservation) { auto parsedProjection = createParsedProjection("{}", "{a: 1, 'a.b': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a.b")); parsedProjection = createParsedProjection("{}", "{'a.b': 1, a: 1}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a.b")); } TEST(ParsedProjectionTest, ExclusionProjectionDoesNotPreserveParent) { auto parsedProjection = createParsedProjection("{}", "{'a.b': 0}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, ExclusionProjectionDoesNotPreserveChild) { auto parsedProjection = createParsedProjection("{}", "{a: 0}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a.b")); } TEST(ParsedProjectionTest, ExclusionProjectionDoesNotPreserveField) { auto parsedProjection = createParsedProjection("{}", "{a: 0}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, InclusionProjectionDoesNotPreserveNonIncludedFields) { auto parsedProjection = createParsedProjection("{}", "{a: 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("c")); } TEST(ParsedProjectionTest, ExclusionProjectionPreservesNonExcludedFields) { auto parsedProjection = createParsedProjection("{}", "{a: 0}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("c")); } TEST(ParsedProjectionTest, PositionalProjectionDoesNotPreserveField) { auto parsedProjection = createParsedProjection("{a: {$elemMatch: {$eq: 0}}}", "{'a.$': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, PositionalProjectionDoesNotPreserveChild) { auto parsedProjection = createParsedProjection("{a: {$elemMatch: {$eq: 0}}}", "{'a.$': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a.b")); } TEST(ParsedProjectionTest, PositionalProjectionDoesNotPreserveParent) { auto parsedProjection = createParsedProjection("{'a.b': {$elemMatch: {$eq: 0}}}", "{'a.b.$': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, MetaProjectionDoesNotPreserveField) { auto parsedProjection = createParsedProjection("{}", "{a: {$meta: 'textScore'}}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, MetaProjectionDoesNotPreserveChild) { auto parsedProjection = createParsedProjection("{}", "{a: {$meta: 'textScore'}}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a.b")); } TEST(ParsedProjectionTest, IdExclusionProjectionPreservesOtherFields) { auto parsedProjection = createParsedProjection("{}", "{_id: 0}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, IdInclusionProjectionDoesNotPreserveOtherFields) { auto parsedProjection = createParsedProjection("{}", "{_id: 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, IdSubfieldExclusionProjectionPreservesId) { auto parsedProjection = createParsedProjection("{}", "{'_id.a': 0}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id.a")); } TEST(ParsedProjectionTest, IdSubfieldInclusionProjectionPreservesId) { auto parsedProjection = createParsedProjection("{}", "{'_id.a': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id.a")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id.b")); } TEST(ParsedProjectionTest, IdExclusionWithExclusionProjectionDoesNotPreserveId) { auto parsedProjection = createParsedProjection("{}", "{_id: 0, a: 0}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("_id")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, IdInclusionWithInclusionProjectionPreservesId) { auto parsedProjection = createParsedProjection("{}", "{_id: 1, a: 1}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, IdExclusionWithInclusionProjectionDoesNotPreserveId) { auto parsedProjection = createParsedProjection("{}", "{_id: 0, a: 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("_id")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("a")); } TEST(ParsedProjectionTest, PositionalProjectionDoesNotPreserveFields) { auto parsedProjection = createParsedProjection("{a: 1}", "{'a.$': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a.b")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id")); } TEST(ParsedProjectionTest, PositionalProjectionWithIdExclusionDoesNotPreserveFields) { auto parsedProjection = createParsedProjection("{a: 1}", "{_id: 0, 'a.$': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a.b")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("_id")); } TEST(ParsedProjectionTest, PositionalProjectionWithIdInclusionPreservesId) { auto parsedProjection = createParsedProjection("{a: 1}", "{_id: 1, 'a.$': 1}"); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("b")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("a.b")); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id")); } TEST(ParsedProjectionTest, ProjectionOfFieldSimilarToIdIsNotSpecial) { auto parsedProjection = createParsedProjection("{}", "{_idimpostor: 0}"); ASSERT_TRUE(parsedProjection->isFieldRetainedExactly("_id")); ASSERT_FALSE(parsedProjection->isFieldRetainedExactly("_idimpostor")); } // // DBRef projections // TEST(ParsedProjectionTest, DBRefProjections) { // non-dotted createParsedProjection(BSONObj(), BSON("$ref" << 1)); createParsedProjection(BSONObj(), BSON("$id" << 1)); createParsedProjection(BSONObj(), BSON("$ref" << 1)); // dotted before createParsedProjection("{}", "{'a.$ref': 1}"); createParsedProjection("{}", "{'a.$id': 1}"); createParsedProjection("{}", "{'a.$db': 1}"); // dotted after createParsedProjection("{}", "{'$id.a': 1}"); // position operator on $id // $ref and $db hold the collection and database names respectively, // so these fields cannot be arrays. createParsedProjection("{'a.$id': {$elemMatch: {x: 1}}}", "{'a.$id.$': 1}"); } } // unnamed namespace