summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDavis Haupt <davis.haupt@mongodb.com>2023-02-27 21:29:37 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2023-02-27 23:34:15 +0000
commitd3eacf8547d811f075ef554dc8363f8e06eea9c4 (patch)
tree4ff82d880b91669488e92ace2eab280b131b9122 /src
parent294fb3396b285d140a7a0522ce3a00a75d694448 (diff)
downloadmongo-d3eacf8547d811f075ef554dc8363f8e06eea9c4.tar.gz
SERVER-73488 Add redaction to projection AST serialization
Diffstat (limited to 'src')
-rw-r--r--src/mongo/db/exec/SConscript1
-rw-r--r--src/mongo/db/exec/add_fields_projection_executor.h6
-rw-r--r--src/mongo/db/exec/exclusion_projection_executor.cpp2
-rw-r--r--src/mongo/db/exec/exclusion_projection_executor.h11
-rw-r--r--src/mongo/db/exec/inclusion_projection_executor.h11
-rw-r--r--src/mongo/db/exec/projection_executor_redaction_test.cpp201
-rw-r--r--src/mongo/db/exec/projection_node.cpp19
-rw-r--r--src/mongo/db/exec/projection_node.h6
-rw-r--r--src/mongo/db/pipeline/document_source_replace_root.h4
-rw-r--r--src/mongo/db/pipeline/group_from_first_document_transformation.cpp2
-rw-r--r--src/mongo/db/pipeline/group_from_first_document_transformation.h4
-rw-r--r--src/mongo/db/pipeline/transformer_interface.h4
-rw-r--r--src/mongo/db/query/projection_ast_test.cpp67
-rw-r--r--src/mongo/db/query/projection_ast_util.cpp162
-rw-r--r--src/mongo/db/query/projection_ast_util.h3
-rw-r--r--src/mongo/db/query/serialization_options.h7
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