diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/exec/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/exec/add_fields_projection_executor.h | 6 | ||||
-rw-r--r-- | src/mongo/db/exec/exclusion_projection_executor.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/exec/exclusion_projection_executor.h | 11 | ||||
-rw-r--r-- | src/mongo/db/exec/inclusion_projection_executor.h | 11 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_executor_redaction_test.cpp | 201 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_node.cpp | 19 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_node.h | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_replace_root.h | 4 | ||||
-rw-r--r-- | src/mongo/db/pipeline/group_from_first_document_transformation.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/group_from_first_document_transformation.h | 4 | ||||
-rw-r--r-- | src/mongo/db/pipeline/transformer_interface.h | 4 | ||||
-rw-r--r-- | src/mongo/db/query/projection_ast_test.cpp | 67 | ||||
-rw-r--r-- | src/mongo/db/query/projection_ast_util.cpp | 162 | ||||
-rw-r--r-- | src/mongo/db/query/projection_ast_util.h | 3 | ||||
-rw-r--r-- | src/mongo/db/query/serialization_options.h | 7 |
16 files changed, 442 insertions, 68 deletions
diff --git a/src/mongo/db/exec/SConscript b/src/mongo/db/exec/SConscript index 392582a08f9..60a4c394e03 100644 --- a/src/mongo/db/exec/SConscript +++ b/src/mongo/db/exec/SConscript @@ -132,6 +132,7 @@ env.CppUnitTest( "find_projection_executor_test.cpp", "inclusion_projection_executor_test.cpp", "projection_executor_builder_test.cpp", + "projection_executor_redaction_test.cpp", "projection_executor_test.cpp", "projection_executor_utils_test.cpp", "projection_executor_wildcard_access_test.cpp", diff --git a/src/mongo/db/exec/add_fields_projection_executor.h b/src/mongo/db/exec/add_fields_projection_executor.h index b1dd9130060..48a48a91f74 100644 --- a/src/mongo/db/exec/add_fields_projection_executor.h +++ b/src/mongo/db/exec/add_fields_projection_executor.h @@ -94,9 +94,9 @@ public: */ void parse(const BSONObj& spec); - Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final { - return _root->serialize(explain); + Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options = {}) const final { + return _root->serialize(explain, options); } /** diff --git a/src/mongo/db/exec/exclusion_projection_executor.cpp b/src/mongo/db/exec/exclusion_projection_executor.cpp index 42bf00735b4..c9af71c7adb 100644 --- a/src/mongo/db/exec/exclusion_projection_executor.cpp +++ b/src/mongo/db/exec/exclusion_projection_executor.cpp @@ -55,7 +55,7 @@ std::pair<BSONObj, bool> ExclusionNode::extractProjectOnFieldAndRename(const Str // Check for a projection on subfields of 'oldName'. For example, {oldName: {a: 0, b: 0}}. if (auto it = _children.find(oldName); it != _children.end()) { - extractedExclusion.append(newName, it->second->serialize(boost::none).toBson()); + extractedExclusion.append(newName, it->second->serialize(boost::none, {}).toBson()); _children.erase(it); } diff --git a/src/mongo/db/exec/exclusion_projection_executor.h b/src/mongo/db/exec/exclusion_projection_executor.h index 9be0b831087..475f65d8af1 100644 --- a/src/mongo/db/exec/exclusion_projection_executor.h +++ b/src/mongo/db/exec/exclusion_projection_executor.h @@ -160,16 +160,17 @@ public: return _root.get(); } - Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final { + Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options = {}) const final { MutableDocument output; // The ExclusionNode tree in '_root' will always have a top-level _id node if _id is to be // excluded. If the _id node is not present, then explicitly set {_id: true} to avoid // ambiguity in the expected behavior of the serialized projection. - _root->serialize(explain, &output); - if (output.peek()["_id"].missing()) { - output.addField("_id", Value{true}); + _root->serialize(explain, &output, options); + auto idFieldName = options.serializeFieldName("_id"); + if (output.peek()[idFieldName].missing()) { + output.addField(idFieldName, Value{true}); } return output.freeze(); } diff --git a/src/mongo/db/exec/inclusion_projection_executor.h b/src/mongo/db/exec/inclusion_projection_executor.h index 8bf83753186..6ef0179a732 100644 --- a/src/mongo/db/exec/inclusion_projection_executor.h +++ b/src/mongo/db/exec/inclusion_projection_executor.h @@ -217,16 +217,17 @@ public: /** * Serialize the projection. */ - Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final { + Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options = {}) const final { MutableDocument output; // The InclusionNode tree in '_root' will always have a top-level _id node if _id is to be // included. If the _id node is not present, then explicitly set {_id: false} to avoid // ambiguity in the expected behavior of the serialized projection. - _root->serialize(explain, &output); - if (output.peek()["_id"].missing()) { - output.addField("_id", Value{false}); + _root->serialize(explain, &output, options); + auto idFieldName = options.serializeFieldName("_id"); + if (output.peek()[idFieldName].missing()) { + output.addField(idFieldName, Value{false}); } return output.freeze(); diff --git a/src/mongo/db/exec/projection_executor_redaction_test.cpp b/src/mongo/db/exec/projection_executor_redaction_test.cpp new file mode 100644 index 00000000000..c37f571389a --- /dev/null +++ b/src/mongo/db/exec/projection_executor_redaction_test.cpp @@ -0,0 +1,201 @@ +/** + * 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 + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/db/query/projection_ast_util.h" +#include "mongo/db/query/projection_parser.h" +#include "mongo/db/query/projection_policies.h" +#include "mongo/db/query/serialization_options.h" +#include "mongo/unittest/inline_auto_update.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { +std::unique_ptr<projection_executor::ProjectionExecutor> compileProjection(BSONObj proj) { + auto expCtx = make_intrusive<ExpressionContextForTest>(); + auto policies = ProjectionPolicies::findProjectionPolicies(); + auto ast = projection_ast::parseAndAnalyze(expCtx, proj, policies); + return projection_executor::buildProjectionExecutor( + expCtx, &ast, policies, projection_executor::kDefaultBuilderParams); +} +std::unique_ptr<projection_executor::ProjectionExecutor> compileProjection(BSONObj proj, + BSONObj query) { + auto expCtx = make_intrusive<ExpressionContextForTest>(); + auto match = uassertStatusOK(MatchExpressionParser::parse(query, expCtx)); + auto policies = ProjectionPolicies::findProjectionPolicies(); + auto ast = projection_ast::parseAndAnalyze(expCtx, proj, match.get(), query, policies); + auto exec = projection_executor::buildProjectionExecutor( + expCtx, &ast, policies, projection_executor::kDefaultBuilderParams); + return exec; +} +std::string redactFieldNameForTest(StringData s) { + return str::stream() << "HASH(" << s << ")"; +} + +#define ASSERT_DOCUMENT_EQ_AUTO(expected, actual) \ + ASSERT_STR_EQ_AUTO(expected, actual.toBson().toString()); + +TEST(Redaction, ProjectionTest) { + SerializationOptions options; + options.replacementForLiteralArgs = "?"; + options.redactFieldNames = true; + + options.redactFieldNamesStrategy = redactFieldNameForTest; + auto redactProj = [&](std::string obj) { + return compileProjection(fromjson(obj))->serializeTransformation(boost::none, options); + }; + + /// Inclusion projections + + // Simple single inclusion + auto actual = redactProj("{a: 1}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): true }", // NOLINT (test auto-update) + actual); + + actual = redactProj("{a: true}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): true }", // NOLINT (test auto-update) + actual); + + // Dotted path + actual = redactProj("{\"a.b\": 1}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): { HASH(b): true } }", // NOLINT (test auto-update) + actual); + + // Two fields + actual = redactProj("{a: 1, b: 1}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): true, HASH(b): true }", // NOLINT (test auto-update) + actual); + + // Explicit _id: 1 + actual = redactProj("{b: 1, _id: 1}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(b): true }", // NOLINT (test auto-update) + actual); + + // Two nested fields + actual = redactProj("{\"b.d\": 1, \"b.c\": 1}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(b): { HASH(d): true, HASH(c): true } }", // NOLINT + actual); + + actual = redactProj("{\"b.d\": 1, a: 1, \"b.c\": 1}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): true, HASH(b): { HASH(d): true, HASH(c): true } }", + actual); + + + /// Exclusion projections + + // Simple single exclusion + actual = redactProj("{a: 0}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(a): false, HASH(_id): true }", // NOLINT (test auto-update) + actual); + + // Dotted path + actual = redactProj("{\"a.b\": 0}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(a): { HASH(b): false }, HASH(_id): true }", // NOLINT (test auto-update) + actual); + + // Two fields + actual = redactProj("{a: 0, b: 0}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(a): false, HASH(b): false, HASH(_id): true }", // NOLINT (test auto-update) + actual); + + // Explicit _id: 0 + actual = redactProj("{b: 0, _id: 0}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): false, HASH(b): false }", // NOLINT (test auto-update) + actual); + + // Two nested fields + actual = redactProj("{\"b.d\": 0, \"b.c\": 0}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(b): { HASH(d): false, HASH(c): false }, HASH(_id): true }", // NOLINT (test + // auto-update) + actual); + + actual = redactProj("{\"b.d\": 0, a: 0, \"b.c\": 0}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(a): false, HASH(b): { HASH(d): false, HASH(c): false }, HASH(_id): true }", // NOLINT + actual); + + /// Add fields projection + actual = redactProj("{a: \"hi\"}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): { $const: \"?\" } }", // NOLINT (test auto-update) + actual); + + actual = redactProj("{a: '$field'}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): \"$HASH(field)\" }", // NOLINT (test auto-update) + actual); + + // Dotted path + actual = redactProj("{\"a.b\": \"hi\"}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): { HASH(b): { $const: \"?\" } } }", // NOLINT + actual); + + // Two fields + actual = redactProj("{a: \"hi\", b: \"hello\"}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(a): { $const: \"?\" }, HASH(b): { $const: \"?\" } }", + actual); + + // Explicit _id: 0 + actual = redactProj("{b: \"hi\", _id: \"hey\"}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(b): { $const: \"?\" }, HASH(_id): { $const: \"?\" } }", + actual); + + // Two nested fields + actual = redactProj("{\"b.d\": \"hello\", \"b.c\": \"world\"}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(b): { HASH(d): { $const: \"?\" }, HASH(c): { $const: \"?\" } } }", + actual); + + actual = redactProj("{\"b.d\": \"hello\", a: \"world\", \"b.c\": \"mongodb\"}"); + ASSERT_DOCUMENT_EQ_AUTO( // + "{ HASH(_id): true, HASH(b): { HASH(d): { $const: \"?\" }, HASH(c): { $const: \"?\" } }, " + "HASH(a): { $const: \"?\" } }", + actual); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/exec/projection_node.cpp b/src/mongo/db/exec/projection_node.cpp index 6db417d7b2c..5f412693c99 100644 --- a/src/mongo/db/exec/projection_node.cpp +++ b/src/mongo/db/exec/projection_node.cpp @@ -275,25 +275,27 @@ void ProjectionNode::optimize() { _maxFieldsToProject = maxFieldsToProject(); } -Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { +Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options) const { MutableDocument outputDoc; - serialize(explain, &outputDoc); + serialize(explain, &outputDoc, options); return outputDoc.freeze(); } void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain, - MutableDocument* output) const { + MutableDocument* output, + SerializationOptions options) const { // Determine the boolean value for projected fields in the explain output. const bool projVal = !applyLeafProjectionToValue(Value(true)).missing(); // Always put "_id" first if it was projected (implicitly or explicitly). if (_projectedFieldsSet.find("_id") != _projectedFieldsSet.end()) { - output->addField("_id", Value(projVal)); + output->addField(options.serializeFieldName("_id"), Value(projVal)); } for (auto&& projectedField : _projectedFields) { if (projectedField != "_id") { - output->addField(projectedField, Value(projVal)); + output->addField(options.serializeFieldName(projectedField), Value(projVal)); } } @@ -301,8 +303,8 @@ void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explai auto childIt = _children.find(field); if (childIt != _children.end()) { MutableDocument subDoc; - childIt->second->serialize(explain, &subDoc); - output->addField(field, subDoc.freezeToValue()); + childIt->second->serialize(explain, &subDoc, options); + output->addField(options.serializeFieldName(field), subDoc.freezeToValue()); } else { tassert(7241727, "computed fields must be allowed in inclusion projections.", @@ -311,7 +313,8 @@ void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explai tassert(7241728, "reached end of the expression iterator", expressionIt != _expressions.end()); - output->addField(field, expressionIt->second->serialize(static_cast<bool>(explain))); + output->addField(options.serializeFieldName(field), + expressionIt->second->serialize(options)); } } } diff --git a/src/mongo/db/exec/projection_node.h b/src/mongo/db/exec/projection_node.h index 61e9b98b89d..14ceb554e7a 100644 --- a/src/mongo/db/exec/projection_node.h +++ b/src/mongo/db/exec/projection_node.h @@ -130,10 +130,12 @@ public: void optimize(); - Document serialize(boost::optional<ExplainOptions::Verbosity> explain) const; + Document serialize(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options) const; void serialize(boost::optional<ExplainOptions::Verbosity> explain, - MutableDocument* output) const; + MutableDocument* output, + SerializationOptions options) const; /** * Append the variables referred to by this projection to the set 'refs', without clearing any diff --git a/src/mongo/db/pipeline/document_source_replace_root.h b/src/mongo/db/pipeline/document_source_replace_root.h index 8b4f7f36ea3..16c76713d87 100644 --- a/src/mongo/db/pipeline/document_source_replace_root.h +++ b/src/mongo/db/pipeline/document_source_replace_root.h @@ -61,8 +61,8 @@ public: _newRoot->optimize(); } - Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final { + Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options = {}) const final { return Document{{"newRoot", _newRoot->serialize(static_cast<bool>(explain))}}; } diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.cpp b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp index aa14b50b81a..68ef20cb2e1 100644 --- a/src/mongo/db/pipeline/group_from_first_document_transformation.cpp +++ b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp @@ -48,7 +48,7 @@ void GroupFromFirstDocumentTransformation::optimize() { } Document GroupFromFirstDocumentTransformation::serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const { + boost::optional<ExplainOptions::Verbosity> explain, SerializationOptions options) const { MutableDocument newRoot(_accumulatorExprs.size()); for (auto&& expr : _accumulatorExprs) { diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.h b/src/mongo/db/pipeline/group_from_first_document_transformation.h index 53cb38fb013..c698b95283b 100644 --- a/src/mongo/db/pipeline/group_from_first_document_transformation.h +++ b/src/mongo/db/pipeline/group_from_first_document_transformation.h @@ -78,8 +78,8 @@ public: void optimize() final; - Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final; + Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options = {}) const final; DepsTracker::State addDependencies(DepsTracker* deps) const final; diff --git a/src/mongo/db/pipeline/transformer_interface.h b/src/mongo/db/pipeline/transformer_interface.h index e4a7457adfe..f9fefccf9ab 100644 --- a/src/mongo/db/pipeline/transformer_interface.h +++ b/src/mongo/db/pipeline/transformer_interface.h @@ -66,8 +66,8 @@ public: * Returns a document describing this transformation. For example, this function will return * {_id: 0, x: 1} for the stage parsed from {$project: {_id: 0, x: 1}}. */ - virtual Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const = 0; + virtual Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, + SerializationOptions options = {}) const = 0; /** * Method used by inclusion and add fields projecton executors to extract computed projections diff --git a/src/mongo/db/query/projection_ast_test.cpp b/src/mongo/db/query/projection_ast_test.cpp index 9e0001d5a8b..90b75fe56c5 100644 --- a/src/mongo/db/query/projection_ast_test.cpp +++ b/src/mongo/db/query/projection_ast_test.cpp @@ -40,6 +40,8 @@ #include "mongo/db/query/projection_ast_util.h" #include "mongo/db/query/projection_parser.h" #include "mongo/db/query/query_planner_test_fixture.h" +#include "mongo/db/query/serialization_options.h" +#include "mongo/unittest/inline_auto_update.h" namespace { @@ -771,4 +773,69 @@ TEST_F(ProjectionASTTest, ShouldThrowWithPositionalOnExclusion) { DBException, 31395); } +std::string redactFieldNameForTest(StringData s) { + return str::stream() << "HASH(" << s << ")"; +} + +TEST_F(ProjectionASTTest, TestASTRedaction) { + SerializationOptions options; + options.replacementForLiteralArgs = "?"; + options.redactFieldNames = true; + options.redactFieldNamesStrategy = redactFieldNameForTest; + + + auto proj = fromjson("{'a.b': 1}"); + BSONObj output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(a): { HASH(b): true }, HASH(_id): true }", // NOLINT (test auto-update) + output.toString()); + + proj = fromjson("{'a.b': 0}"); + output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(a): { HASH(b): false } }", // NOLINT (test auto-update) + output.toString()); + + proj = fromjson("{a: 1, b: 1}"); + output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(a): true, HASH(b): true, HASH(_id): true }", // NOLINT (test auto-update) + output.toString()); + + // ElemMatch projection + proj = fromjson("{f: {$elemMatch: {foo: 'bar'}}}"); + output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(f): { $elemMatch: { HASH(foo): { $eq: \"?\" } } }, HASH(_id): true }", // NOLINT + output.toString()); + + // Positional projection + proj = fromjson("{'x.$': 1}"); + output = + projection_ast::serialize(parseWithFindFeaturesEnabled(proj, fromjson("{'x.a': 2}")), {}); + ASSERT_STR_EQ_AUTO( // + "{ x.$: true, _id: true }", // NOLINT (test auto-update) + output.toString()); + + // Slice (first form) + proj = fromjson("{a: {$slice: 1}}"); + output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(a): { $slice: \"?\" } }", // NOLINT (test auto-update) + output.toString()); + + // Slice (second form) + proj = fromjson("{a: {$slice: [1, 3]}}"); + output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(a): { $slice: [ \"?\", \"?\" ] } }", // NOLINT (test auto-update) + output.toString()); + + /// $meta projection + proj = fromjson("{foo: {$meta: 'indexKey'}}"); + output = projection_ast::serialize(parseWithFindFeaturesEnabled(proj), options); + ASSERT_STR_EQ_AUTO( // + "{ HASH(foo): { $meta: \"indexKey\" } }", // NOLINT (test auto-update) + output.toString()); +} } // namespace diff --git a/src/mongo/db/query/projection_ast_util.cpp b/src/mongo/db/query/projection_ast_util.cpp index 50649f8a5ed..7719ca3db96 100644 --- a/src/mongo/db/query/projection_ast_util.cpp +++ b/src/mongo/db/query/projection_ast_util.cpp @@ -29,28 +29,27 @@ #include "mongo/platform/basic.h" -#include "mongo/db/query/projection_ast_util.h" - #include "mongo/db/query/projection_ast_path_tracking_visitor.h" +#include "mongo/db/query/projection_ast_util.h" +#include "mongo/db/query/serialization_options.h" #include "mongo/db/query/tree_walker.h" namespace mongo::projection_ast { namespace { struct BSONVisitorContext { std::stack<BSONObjBuilder> builders; + bool underElemMatch = false; }; class BSONPreVisitor : public ProjectionASTConstVisitor { public: - BSONPreVisitor(PathTrackingVisitorContext<BSONVisitorContext>* context) - : _context(context), _builders(context->data().builders) {} + using ProjectionASTConstVisitor::visit; + BSONPreVisitor(PathTrackingVisitorContext<BSONVisitorContext>* context, + const SerializationOptions& options) + : _context(context), _builders(context->data().builders), _options(options) {} - virtual void visit(const MatchExpressionASTNode* node) { - static_cast<const MatchExpressionASTNode*>(node)->matchExpression()->serialize( - &_builders.top(), {}); - } - virtual void visit(const ProjectionPathASTNode* node) { + void visit(const ProjectionPathASTNode* node) override { if (!node->parent()) { // No root of the tree, thus this node has no field name. _builders.push(BSONObjBuilder()); @@ -59,47 +58,53 @@ public: } } - virtual void visit(const ProjectionPositionalASTNode* node) { - // ProjectionPositional always has the original query's match expression node as its - // child. Serialize as: {"positional.projection.field.$": <original match expression>}. - _context->data().builders.push(_builders.top().subobjStart(getFieldName() + ".$")); - } - - virtual void visit(const ProjectionSliceASTNode* node) { + void visit(const ProjectionSliceASTNode* node) override { BSONObjBuilder sub(_builders.top().subobjStart(getFieldName())); if (node->skip()) { - sub.appendArray("$slice", BSON_ARRAY(*node->skip() << node->limit())); + if (_options.replacementForLiteralArgs) { + const auto rep = _options.replacementForLiteralArgs.value(); + sub.appendArray("$slice", BSON_ARRAY(rep << rep)); + } else { + sub.appendArray("$slice", BSON_ARRAY(*node->skip() << node->limit())); + } } else { - sub.appendNumber("$slice", node->limit()); + if (_options.replacementForLiteralArgs) { + sub.append("$slice", _options.replacementForLiteralArgs.value()); + } else { + sub.appendNumber("$slice", node->limit()); + } } } - virtual void visit(const ProjectionElemMatchASTNode* node) { - // Defer to the child, match expression node. - } - virtual void visit(const ExpressionASTNode* node) { - node->expression()->serialize(false).addToBsonObj(&_builders.top(), getFieldName()); + void visit(const ExpressionASTNode* node) override { + node->expression()->serialize(_options).addToBsonObj(&_builders.top(), getFieldName()); } - virtual void visit(const BooleanConstantASTNode* node) { + void visit(const BooleanConstantASTNode* node) override { _builders.top().append(getFieldName(), node->value()); } -private: + void visit(const ProjectionPositionalASTNode* node) override = 0; + void visit(const ProjectionElemMatchASTNode* node) override = 0; + void visit(const MatchExpressionASTNode* node) override = 0; + +protected: std::string getFieldName() { - return _context->childPath(); + return _options.serializeFieldName(_context->childPath()); } PathTrackingVisitorContext<BSONVisitorContext>* _context; std::stack<BSONObjBuilder>& _builders; + SerializationOptions _options; }; class BSONPostVisitor : public ProjectionASTConstVisitor { public: + using ProjectionASTConstVisitor::visit; BSONPostVisitor(BSONVisitorContext* context) : _context(context) {} - virtual void visit(const ProjectionPathASTNode* node) { + void visit(const ProjectionPathASTNode* node) override { // Don't pop the top builder. if (node->parent()) { // Pop the BSONObjBuilder that was added in the pre visitor. @@ -107,25 +112,96 @@ public: } } - virtual void visit(const ProjectionPositionalASTNode* node) { + void visit(const ProjectionSliceASTNode* node) override {} + void visit(const ExpressionASTNode* node) override {} + void visit(const BooleanConstantASTNode* node) override {} + void visit(const MatchExpressionASTNode* node) override {} + + void visit(const ProjectionPositionalASTNode* node) override = 0; + void visit(const ProjectionElemMatchASTNode* node) override = 0; + +protected: + BSONVisitorContext* _context; +}; + +class DebugPreVisitor : public BSONPreVisitor { +public: + using BSONPreVisitor::visit; + DebugPreVisitor(PathTrackingVisitorContext<BSONVisitorContext>* context) + : BSONPreVisitor(context, SerializationOptions{}) {} + + void visit(const ProjectionPositionalASTNode* node) override { + // ProjectionPositional always has the original query's match expression node as its + // child. Serialize as: {"positional.projection.field.$": <original match expression>}. + _context->data().builders.push(_builders.top().subobjStart(getFieldName() + ".$")); + } + + void visit(const ProjectionElemMatchASTNode* node) override { + // Defer to the child, match expression node. + } + + void visit(const MatchExpressionASTNode* node) override { + static_cast<const MatchExpressionASTNode*>(node)->matchExpression()->serialize( + &_builders.top(), {}); + } +}; + +class DebugPostVisitor : public BSONPostVisitor { +public: + using BSONPostVisitor::visit; + DebugPostVisitor(BSONVisitorContext* context) : BSONPostVisitor(context) {} + + void visit(const ProjectionPositionalASTNode* node) override { _context->builders.pop(); } - virtual void visit(const MatchExpressionASTNode* node) {} - virtual void visit(const ProjectionSliceASTNode* node) {} - virtual void visit(const ProjectionElemMatchASTNode* node) {} - virtual void visit(const ExpressionASTNode* node) {} - virtual void visit(const BooleanConstantASTNode* node) {} + void visit(const ProjectionElemMatchASTNode* node) override {} +}; -private: - BSONVisitorContext* _context; +class SerializationPreVisitor : public BSONPreVisitor { +public: + using BSONPreVisitor::visit; + SerializationPreVisitor(PathTrackingVisitorContext<BSONVisitorContext>* context, + SerializationOptions options) + : BSONPreVisitor(context, options) {} + + void visit(const ProjectionPositionalASTNode* node) override { + tassert(73488, + "Positional projection should not appear below an $elemMatch projection.", + !_context->data().underElemMatch); + _builders.top().append(getFieldName() + ".$", true); + } + + void visit(const ProjectionElemMatchASTNode* node) override { + // The child match expression node should begin with $elemMatch. + _context->data().underElemMatch = true; + } + + void visit(const MatchExpressionASTNode* node) override { + if (_context->data().underElemMatch) { + static_cast<const MatchExpressionASTNode*>(node)->matchExpression()->serialize( + &_builders.top(), _options); + } + } }; + +class SerializationPostVisitor : public BSONPostVisitor { +public: + using BSONPostVisitor::visit; + SerializationPostVisitor(BSONVisitorContext* context) : BSONPostVisitor(context) {} + + void visit(const ProjectionPositionalASTNode* node) override {} + void visit(const ProjectionElemMatchASTNode* node) override { + _context->underElemMatch = false; + } +}; + } // namespace BSONObj astToDebugBSON(const ASTNode* root) { PathTrackingVisitorContext<BSONVisitorContext> context; - BSONPreVisitor preVisitor{&context}; - BSONPostVisitor postVisitor{&context.data()}; + DebugPreVisitor preVisitor{&context}; + DebugPostVisitor postVisitor{&context.data()}; PathTrackingWalker walker{&context, {&preVisitor}, {&postVisitor}}; tree_walker::walk<true, projection_ast::ASTNode>(root, &walker); @@ -133,4 +209,16 @@ BSONObj astToDebugBSON(const ASTNode* root) { invariant(context.data().builders.size() == 1); return context.data().builders.top().obj(); } + +BSONObj serialize(const Projection& ast, SerializationOptions options) { + PathTrackingVisitorContext<BSONVisitorContext> context; + SerializationPreVisitor preVisitor{&context, options}; + SerializationPostVisitor postVisitor{&context.data()}; + PathTrackingWalker walker{&context, {&preVisitor}, {&postVisitor}}; + + tree_walker::walk<true, projection_ast::ASTNode>(ast.root(), &walker); + + invariant(context.data().builders.size() == 1); + return context.data().builders.top().obj(); +} } // namespace mongo::projection_ast diff --git a/src/mongo/db/query/projection_ast_util.h b/src/mongo/db/query/projection_ast_util.h index af89254a9a1..895340104ad 100644 --- a/src/mongo/db/query/projection_ast_util.h +++ b/src/mongo/db/query/projection_ast_util.h @@ -29,6 +29,7 @@ #pragma once +#include "mongo/db/query/projection.h" #include "mongo/db/query/projection_ast.h" namespace mongo { @@ -37,5 +38,7 @@ namespace projection_ast { * This is intended to be used for debug output, not for serialization. */ BSONObj astToDebugBSON(const ASTNode* root); + +BSONObj serialize(const Projection& ast, SerializationOptions options); } // namespace projection_ast } // namespace mongo diff --git a/src/mongo/db/query/serialization_options.h b/src/mongo/db/query/serialization_options.h index 4ac22617ecd..0a69df8d38a 100644 --- a/src/mongo/db/query/serialization_options.h +++ b/src/mongo/db/query/serialization_options.h @@ -55,6 +55,13 @@ struct SerializationOptions { redactFieldNames(redactFieldNamesStrategy_), redactFieldNamesStrategy(redactFieldNamesStrategy_) {} + std::string serializeFieldName(StringData str) const { + if (redactFieldNames) { + return redactFieldNamesStrategy(str); + } + return str.toString(); + } + // 'replacementForLiteralArgs' is an independent option to serialize in a genericized format // with the aim of similar "shaped" queries serializing to the same object. For example, if // set to '?' then the serialization of {a: {$gt: 2}} will result in {a: {$gt: '?'}}, as |