/** * Copyright (C) 2023-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/pipeline/expression_context_for_test.h" #include "mongo/db/query/query_shape/find_cmd_shape.h" #include "mongo/db/service_context_test_fixture.h" #include "mongo/unittest/unittest.h" namespace mongo::query_shape { namespace { /** * TODO this was stolen from another test. Time for a library? * Simplistic redaction strategy for testing which appends the field name to the prefix "REDACT_". */ std::string applyHmacForTest(StringData sd) { return "REDACT_" + sd.toString(); } static const NamespaceStringOrUUID kDefaultTestNss = NamespaceStringOrUUID{NamespaceString("testDB.testColl")}; struct RequestOptions { OptionalBool singleBatch = {}; OptionalBool allowDiskUse = {}; OptionalBool returnKey = {}; OptionalBool showRecordId = {}; OptionalBool tailable = {}; OptionalBool awaitData = {}; OptionalBool limit = {}; OptionalBool skip = {}; }; class FindCmdShapeTest : public ServiceContextTest { public: void setUp() final { _expCtx = make_intrusive(); } std::unique_ptr makeShapeFromSort(StringData sortJson) { auto fcr = std::make_unique(kDefaultTestNss); fcr->setSort(fromjson(sortJson.rawData())); auto&& parsedRequest = uassertStatusOK(::mongo::parsed_find_command::parse(_expCtx, std::move(fcr))); return std::make_unique(*parsedRequest, _expCtx); } BSONObj sortShape(StringData sortJson) { auto shape = makeShapeFromSort(sortJson); return shape->components.sort; } /** * Returns the shape of the input sort, or boost::none if the input shape was a natural sort * which got converted into a hint. */ boost::optional maybeRedactedSortShape(StringData sortJson) { auto shape = makeShapeFromSort(sortJson); SerializationOptions opts = SerializationOptions::kDebugQueryShapeSerializeOptions; opts.transformIdentifiers = true; opts.transformIdentifiersCallback = applyHmacForTest; auto shapeBson = shape->toBson(_expCtx->opCtx, opts); if (auto sortElem = shapeBson["sort"]; !sortElem.eoo()) { return sortElem.Obj().getOwned(); } return boost::none; } BSONObj redactedSortShape(StringData sortJson) { return *maybeRedactedSortShape(sortJson); } boost::intrusive_ptr _expCtx; std::unique_ptr makeShapeComponentsFromFilter( BSONObj filter, const RequestOptions& requestOptions = {}) { auto fcr = std::make_unique(kDefaultTestNss); fcr->setFilter(filter.getOwned()); fcr->setSingleBatch(requestOptions.singleBatch); fcr->setAllowDiskUse(requestOptions.allowDiskUse); fcr->setReturnKey(requestOptions.returnKey); fcr->setAllowDiskUse(requestOptions.showRecordId); fcr->setTailable(requestOptions.tailable); fcr->setAwaitData(requestOptions.awaitData); auto parsedFind = uassertStatusOK(parsed_find_command::parse(_expCtx, {std::move(fcr)})); return std::make_unique(*parsedFind, _expCtx); } std::unique_ptr makeShapeFromFilter(const BSONObj& filter) { auto fcr = std::make_unique(kDefaultTestNss); fcr->setFilter(filter.getOwned()); auto parsedFind = uassertStatusOK(parsed_find_command::parse(_expCtx, {std::move(fcr)})); return std::make_unique(*parsedFind, _expCtx); } }; TEST_F(FindCmdShapeTest, NormalSortPattern) { ASSERT_BSONOBJ_EQ_AUTO( // NOLINT R"({"a.b.c":1,"foo":-1})", sortShape(R"({"a.b.c": 1, "foo": -1})")); } TEST_F(FindCmdShapeTest, NaturalSortPattern) { // $natural sorts are interpreted as a hint. Hints are not part of the shape (but should show up // in the query stats key). ASSERT_BSONOBJ_EQ_AUTO( // NOLINT R"({})", sortShape(R"({$natural: 1})")); ASSERT_BSONOBJ_EQ_AUTO( // NOLINT R"({})", sortShape(R"({$natural: -1})")); } TEST_F(FindCmdShapeTest, NaturalSortPatternWithMeta) { ASSERT_THROWS_CODE( sortShape(R"({$natural: 1, x: {$meta: "textScore"}})"), DBException, ErrorCodes::BadValue); } TEST_F(FindCmdShapeTest, MetaPatternWithoutNatural) { ASSERT_BSONOBJ_EQ_AUTO( // NOLINT R"({"normal":1,"$computed1":{"$meta":"textScore"}})", sortShape(R"({normal: 1, x: {$meta: "textScore"}})")); } // Here we have one test to ensure that the redaction policy is accepted and applied in the // query_shape utility, but there are more extensive redaction tests in sort_pattern_test.cpp TEST_F(FindCmdShapeTest, RespectsRedactionPolicy) { ASSERT_BSONOBJ_EQ_AUTO( // NOLINT R"({"REDACT_normal":1,"REDACT_y":1})", redactedSortShape(R"({normal: 1, y: 1})")); // No need to redact $natural. Again, this will be interpreted as a hint, but this test is // interesting to ensure the $-prefix of $natural doesn't confuse us. ASSERT(!maybeRedactedSortShape(R"({$natural: 1})")); } TEST_F(FindCmdShapeTest, SizeOfShapeComponents) { auto query = BSON("query" << 1 << "xEquals" << 42); auto findCmdComponent = makeShapeComponentsFromFilter(query.getOwned()); const auto querySize = findCmdComponent->filter.objsize(); const auto minimumSize = sizeof(FindCmdShapeComponents) + querySize; ASSERT_GT(findCmdComponent->size(), minimumSize); ASSERT_LTE(findCmdComponent->size(), minimumSize + static_cast(4 * BSONObj().objsize())); } TEST_F(FindCmdShapeTest, EquivalentShapeComponentsSizes) { auto query = BSON("query" << 1 << "xEquals" << 42); // Tailable can not be set together with 'singleBatch' option. auto mostlyTrueComponent = makeShapeComponentsFromFilter(query.getOwned(), {/* singleBatch = */ false, /* allowDiskUse = */ true, /* returnKey = */ true, /* showRecordId = */ true, /* tailable = */ true, /* awaitData = */ true, /* limit = */ true, /* skip = */ true}); auto mostlyFalseComponent = makeShapeComponentsFromFilter(query.getOwned(), {/* singleBatch = */ false, /* allowDiskUse = */ false, /* returnKey = */ false, /* showRecordId = */ false, /* tailable = */ true, /* awaitData = */ false, /* limit = */ false, /* skip = */ false}); ASSERT_EQ(mostlyTrueComponent->size(), mostlyFalseComponent->size()); } TEST_F(FindCmdShapeTest, DifferentShapeComponentsSizes) { auto smallQuery = BSON("query" << BSONObj()); auto smallFindCmdComponent = makeShapeComponentsFromFilter(smallQuery.getOwned()); auto largeQuery = BSON("query" << 1 << "xEquals" << 42); auto largeFindCmdComponent = makeShapeComponentsFromFilter(largeQuery.getOwned()); ASSERT_LT(smallQuery.objsize(), largeQuery.objsize()); ASSERT_LT(smallFindCmdComponent->size(), largeFindCmdComponent->size()); } TEST_F(FindCmdShapeTest, SizeOfShapeWithAndWithoutLet) { auto filter = BSON("query" << 1 << "xEquals" << 42); auto shapeWithoutLet = makeShapeFromFilter(filter.getOwned()); auto fcr = std::make_unique(kDefaultTestNss); fcr->setFilter(filter.getOwned()); fcr->setLet(fromjson(R"({x: 4})")); auto parsedFind = uassertStatusOK(parsed_find_command::parse(_expCtx, {std::move(fcr)})); auto shapeWithLet = std::make_unique(*parsedFind, _expCtx); ASSERT_LT(shapeWithoutLet->size(), shapeWithLet->size()); } TEST_F(FindCmdShapeTest, SizeOfShapeWithAndWithoutCollation) { auto filter = BSON("query" << 1 << "xEquals" << 42); auto shapeWithoutCollation = makeShapeFromFilter(filter.getOwned()); auto fcr = std::make_unique(kDefaultTestNss); fcr->setFilter(filter.getOwned()); fcr->setCollation(fromjson(R"({locale: "en_US"})")); auto parsedFind = uassertStatusOK(parsed_find_command::parse(_expCtx, {std::move(fcr)})); auto shapeWithCollation = std::make_unique(*parsedFind, _expCtx); ASSERT_LT(shapeWithoutCollation->size(), shapeWithCollation->size()); } } // namespace } // namespace mongo::query_shape