summaryrefslogtreecommitdiff
path: root/src/mongo/db/query/query_shape
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/query/query_shape')
-rw-r--r--src/mongo/db/query/query_shape/README.md69
-rw-r--r--src/mongo/db/query/query_shape/SConscript42
-rw-r--r--src/mongo/db/query/query_shape/agg_cmd_shape.cpp125
-rw-r--r--src/mongo/db/query/query_shape/agg_cmd_shape.h103
-rw-r--r--src/mongo/db/query/query_shape/agg_cmd_shape_test.cpp266
-rw-r--r--src/mongo/db/query/query_shape/cmd_with_let_shape.cpp107
-rw-r--r--src/mongo/db/query/query_shape/cmd_with_let_shape.h109
-rw-r--r--src/mongo/db/query/query_shape/cmd_with_let_shape_test.cpp78
-rw-r--r--src/mongo/db/query/query_shape/find_cmd_shape.cpp227
-rw-r--r--src/mongo/db/query/query_shape/find_cmd_shape.h130
-rw-r--r--src/mongo/db/query/query_shape/find_cmd_shape_test.cpp238
-rw-r--r--src/mongo/db/query/query_shape/query_shape.cpp103
-rw-r--r--src/mongo/db/query/query_shape/query_shape.h165
-rw-r--r--src/mongo/db/query/query_shape/query_shape.idl50
-rw-r--r--src/mongo/db/query/query_shape/query_shape_test.cpp767
-rw-r--r--src/mongo/db/query/query_shape/query_shape_test.idl91
-rw-r--r--src/mongo/db/query/query_shape/serialization_options.cpp515
-rw-r--r--src/mongo/db/query/query_shape/serialization_options.h236
-rw-r--r--src/mongo/db/query/query_shape/shape_helpers.cpp108
-rw-r--r--src/mongo/db/query/query_shape/shape_helpers.h101
20 files changed, 3630 insertions, 0 deletions
diff --git a/src/mongo/db/query/query_shape/README.md b/src/mongo/db/query/query_shape/README.md
new file mode 100644
index 00000000000..b3c02d28e69
--- /dev/null
+++ b/src/mongo/db/query/query_shape/README.md
@@ -0,0 +1,69 @@
+# Query Shape
+A query shape is a transformed version of a command with literal values replaced by a "canonical"
+BSON Type placeholder. Hence, different instances of a command would be considered to have the same
+query shape if they are identical once their literal values are abstracted.
+
+For example, these two queries would have the same shape:
+```js
+db.example.findOne({x: 24});
+db.example.findOne({x: 53});
+```
+While these queries would each have a distinct shape:
+```js
+db.example.findOne({x: 53, y: 1});
+db.example.findOne({x: 53});
+db.example.findOne({x: "string"});
+```
+While different literal _values_ result in the same shape (matching `x` for 23 vs 53), different
+BSON _types_ of the literal are considered distinct shapes (matching `x` for 53 vs "string").
+
+The concept of a query shape exists not just for the find command, but for many of the CRUD commands
+and aggregate. It also includes most (but not all) components of these commands, not just the query
+predicate (MatchExpresssion). In these ways, "query" is meant more generally. While some components
+included in the query shape are shared across the different types of commands (e.g., the "hint"
+field), some are unique. For example, a find command would include a `filter` while an aggregate
+command would have a `pipeline`.
+
+You can see which components are considered part of the query shape or not for each specific shape
+type in their respective "shape component" classes, whose purpose is to determine which components
+are relevant and should be included for determining the shape for specific type of command. The
+structure is as follows:
+- [`CmdSpecificShapeComponents`](query_shape.h#L65)
+ - [`LetShapeComponent`](cmd_with_let_shape.h#L48)
+ - [`AggCmdShapeComponents`](agg_cmd_shape.h#L82)
+ - [`FindCmdShapeComponents`](find_cmd_shape.h#L48)
+
+See more information for the different shapes in their respective classes, structured as follows:
+- [`Shape`](query_shape.h)
+ - [`CmdWithLetShape`](cmd_with_let_shape.h)
+ - [`AggCmdShape`](agg_cmd_shape.h)
+ - [`FindCmdShape`](find_cmd_shape.h)
+
+## Serialization Options
+`SerializationOptions` describes the way we serialize literal values.
+
+There are 3 different serialization options:
+- `kUnchanged`: literals are serialized unmodified
+ - `{x: 5, y: "hello"}` -> `{x: 5, y: "hello"}`
+- `kToDebugTypeString`: human readable format, type string of the literal is serialized
+ - `{x: 5, y: "hello"}` -> `{x: "?number", y: "?string"}`
+- `kToRepresentativeParseableValue`: literal serialized to one canonical value for given type, which
+ must be parseable
+ - `{x: 5, y: "hello"}` -> `{x: 1, y: "?"}`
+ - An example of a query which is serialized differently due to the parseable requirement is `{x:
+ {$regex: "^p.*"}}`. If we serialized the pattern as if it were a normal string we would end up
+ with `{x: {$regex: "?"}}` however `"?"` is not a valid regex pattern, so this would fail
+ parsing. Instead we will serialize it this way to maintain parseability, `{x: {$regex:
+ "\\?"}}`, since `"\\?"` is valid regex.
+
+See [serialization_options.h](serialization_options.h) for more details.
+
+When we compute the [query shape hash](query_shape.cpp#L99-107), we use the
+`kToRepresentativeParseableValue`, since all literals of the same type will become the same value.
+This allows us to group together queries that have the same structure but different literal values
+into the same shape, since they will result in the same hash. The term we use to refer to this is
+"shapify", as we simplify the queries into their query shape.
+
+When shapifying, we try to get as close as possible to the original user input, but there are some
+stages like `$jsonSchema` and `$setWindowFields` that output "internal" stages that are already
+transformed from user input.
diff --git a/src/mongo/db/query/query_shape/SConscript b/src/mongo/db/query/query_shape/SConscript
new file mode 100644
index 00000000000..d4bddba4934
--- /dev/null
+++ b/src/mongo/db/query/query_shape/SConscript
@@ -0,0 +1,42 @@
+# -*- mode: python -*-
+
+Import([
+ "env",
+ "get_option",
+])
+
+env = env.Clone()
+
+env.Library(
+ target='query_shape', source=['query_shape.cpp', 'shape_helpers.cpp'], LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/db/exec/document_value/document_value',
+ '$BUILD_DIR/mongo/db/pipeline/field_path',
+ 'query_shape_common',
+ ], LIBDEPS_PRIVATE=[
+ ])
+
+env.Library(
+ target='query_shape_common', source=[
+ 'query_shape.idl',
+ 'serialization_options.cpp',
+ ], LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/db/exec/document_value/document_value',
+ '$BUILD_DIR/mongo/db/pipeline/field_path',
+ ], LIBDEPS_PRIVATE=[
+ ])
+
+env.CppUnitTest(
+ target="db_query_query_shape_test",
+ source=[
+ "query_shape_test.cpp",
+ "query_shape_test.idl",
+ ],
+ LIBDEPS=[
+ "$BUILD_DIR/mongo/db/auth/authmocks",
+ "$BUILD_DIR/mongo/db/query/query_test_service_context",
+ "$BUILD_DIR/mongo/db/service_context_d_test_fixture",
+ "query_shape",
+ ],
+)
diff --git a/src/mongo/db/query/query_shape/agg_cmd_shape.cpp b/src/mongo/db/query/query_shape/agg_cmd_shape.cpp
new file mode 100644
index 00000000000..e997150ecc6
--- /dev/null
+++ b/src/mongo/db/query/query_shape/agg_cmd_shape.cpp
@@ -0,0 +1,125 @@
+/**
+ * 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/query/query_shape/agg_cmd_shape.h"
+
+#include "mongo/db/query/query_shape/shape_helpers.h"
+
+namespace mongo::query_shape {
+
+AggCmdShapeComponents::AggCmdShapeComponents(
+ const AggregateCommandRequest& aggRequest,
+ stdx::unordered_set<NamespaceString> involvedNamespaces_,
+ std::vector<BSONObj> pipeline)
+ : allowDiskUse(aggRequest.getAllowDiskUse()),
+ involvedNamespaces(std::move(involvedNamespaces_)),
+ representativePipeline(std::move(pipeline)) {}
+
+AggCmdShapeComponents::AggCmdShapeComponents(
+ OptionalBool allowDiskUse,
+ stdx::unordered_set<NamespaceString> involvedNamespaces_,
+ std::vector<BSONObj> pipeline)
+ : allowDiskUse(allowDiskUse),
+ involvedNamespaces(std::move(involvedNamespaces_)),
+ representativePipeline(std::move(pipeline)) {}
+
+void AggCmdShapeComponents::HashValue(absl::HashState state) const {
+ state = absl::HashState::combine(std::move(state), allowDiskUse);
+ for (auto&& shapifiedStage : representativePipeline) {
+ state = absl::HashState::combine(std::move(state), simpleHash(shapifiedStage));
+ }
+}
+
+void AggCmdShape::appendLetCmdSpecificShapeComponents(
+ BSONObjBuilder& bob,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const SerializationOptions& opts) const {
+ tassert(7633000,
+ "We don't support serializing to the unmodified shape here, since we have already "
+ "shapified and stored the representative query - we've lost the original literals",
+ opts.literalPolicy != LiteralSerializationPolicy::kUnchanged);
+
+ if (opts == SerializationOptions::kRepresentativeQueryShapeSerializeOptions) {
+ // We have this copy stored already!
+ return _components.appendTo(bob);
+ } else {
+ // The cached pipeline shape doesn't match the requested options, so we have to
+ // re-parse the pipeline from the initial request.
+ expCtx->inMongos = _inMongos;
+ expCtx->addResolvedNamespaces(_components.involvedNamespaces);
+ auto reparsed = Pipeline::parse(_components.representativePipeline, expCtx);
+ auto serializedPipeline = reparsed->serializeToBson(opts);
+ AggCmdShapeComponents{
+ _components.allowDiskUse, _components.involvedNamespaces, serializedPipeline}
+ .appendTo(bob);
+ }
+}
+
+void AggCmdShapeComponents::appendTo(BSONObjBuilder& bob) const {
+ bob.append("command", "aggregate");
+
+ // pipeline
+ bob.append(AggregateCommandRequest::kPipelineFieldName, representativePipeline);
+
+ // allowDiskUse
+ if (allowDiskUse.has_value()) {
+ bob.append(AggregateCommandRequest::kAllowDiskUseFieldName, bool(allowDiskUse));
+ }
+}
+
+// As part of the size, we must track the allocation of elements in the representative
+// pipeline, as well as the elements in the unordered set of involved namespaces.
+size_t AggCmdShapeComponents::size() const {
+ return sizeof(AggCmdShapeComponents) + shape_helpers::containerSize(representativePipeline) +
+ shape_helpers::containerSize(involvedNamespaces);
+}
+
+AggCmdShape::AggCmdShape(const AggregateCommandRequest& aggregateCommand,
+ NamespaceString origNss,
+ stdx::unordered_set<NamespaceString> involvedNamespaces_,
+ const Pipeline& pipeline,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx)
+ : CmdWithLetShape(aggregateCommand.getLet(),
+ expCtx,
+ _components,
+ std::move(origNss),
+ aggregateCommand.getCollation().value_or(BSONObj())),
+ _components(aggregateCommand,
+ std::move(involvedNamespaces_),
+ pipeline.serializeToBson(
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions)),
+ _inMongos(expCtx->inMongos) {}
+
+size_t AggCmdShape::extraSize() const {
+ // To account for possible padding, we calculate the extra space with the difference instead of
+ // using sizeof(bool);
+ return sizeof(AggCmdShape) - sizeof(CmdWithLetShape) - sizeof(AggCmdShapeComponents);
+}
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/agg_cmd_shape.h b/src/mongo/db/query/query_shape/agg_cmd_shape.h
new file mode 100644
index 00000000000..c0ef5a7b06f
--- /dev/null
+++ b/src/mongo/db/query/query_shape/agg_cmd_shape.h
@@ -0,0 +1,103 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include <boost/intrusive_ptr.hpp>
+
+#include "mongo/db/pipeline/aggregate_command_gen.h"
+#include "mongo/db/pipeline/expression_context.h"
+#include "mongo/db/pipeline/pipeline.h"
+#include "mongo/db/query/query_shape/cmd_with_let_shape.h"
+#include "mongo/db/query/query_shape/query_shape.h"
+
+namespace mongo::query_shape {
+
+/**
+ * A struct representing the aggregate command's specific components that are to be considered part
+ * of the query shape.
+ *
+ * This struct stores the shapified version of the pipeline as a memory optimization. We'll need to
+ * store the BSON version in either case, since often the parsed version needs that BSON to survive
+ * as backing memory, so we store the representative pipeline shape so that we are able to parse the
+ * pipeline again if we need to compute a different shape.
+ */
+struct AggCmdShapeComponents : public query_shape::CmdSpecificShapeComponents {
+ AggCmdShapeComponents(const AggregateCommandRequest&,
+ stdx::unordered_set<NamespaceString> involvedNamespaces,
+ std::vector<BSONObj> shapifiedPipeline);
+
+ AggCmdShapeComponents(OptionalBool allowDiskUse,
+ stdx::unordered_set<NamespaceString> involvedNamespaces,
+ std::vector<BSONObj> shapifiedPipeline);
+
+ size_t size() const final;
+
+ void appendTo(BSONObjBuilder&) const;
+
+ void HashValue(absl::HashState state) const final;
+
+ OptionalBool allowDiskUse;
+
+ stdx::unordered_set<NamespaceString> involvedNamespaces;
+
+ // The representative query shape of the pipeline.
+ std::vector<BSONObj> representativePipeline;
+};
+
+/**
+ * A class representing the query shape of an aggregate command. The components are listed above.
+ * This class knows how to utilize those components to serialize to BSON with any
+ * SerializationOptions. Mostly this involves correctly setting up an ExpressionContext to re-parse
+ * the request if needed.
+ */
+class AggCmdShape : public CmdWithLetShape {
+public:
+ AggCmdShape(const AggregateCommandRequest&,
+ NamespaceString origNss,
+ stdx::unordered_set<NamespaceString> involvedNamespaces,
+ const Pipeline&,
+ const boost::intrusive_ptr<ExpressionContext>&);
+
+ void appendLetCmdSpecificShapeComponents(BSONObjBuilder& bob,
+ const boost::intrusive_ptr<ExpressionContext>&,
+ const SerializationOptions&) const final;
+ size_t extraSize() const final override;
+
+private:
+ AggCmdShapeComponents _components;
+ // Flag to denote if the query was run on mongos. Needed to rebuild the "dummy" expression
+ // context for re-parsing.
+ bool _inMongos;
+};
+static_assert(sizeof(AggCmdShape) <=
+ sizeof(CmdWithLetShape) + sizeof(AggCmdShapeComponents) + 8 /* bool and padding*/,
+ "If the class' members have changed, this assert and the extraSize() calculation may "
+ "need to be updated with a new value.");
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/agg_cmd_shape_test.cpp b/src/mongo/db/query/query_shape/agg_cmd_shape_test.cpp
new file mode 100644
index 00000000000..c617391f4e7
--- /dev/null
+++ b/src/mongo/db/query/query_shape/agg_cmd_shape_test.cpp
@@ -0,0 +1,266 @@
+/**
+ * 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/bson/json.h"
+#include "mongo/db/pipeline/expression_context_for_test.h"
+#include "mongo/db/query/query_shape/agg_cmd_shape.h"
+#include "mongo/db/query/query_shape/cmd_with_let_shape.h"
+#include "mongo/db/query/query_test_service_context.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo::query_shape {
+
+namespace {
+static const NamespaceString kDefaultTestNss = NamespaceString("testDB.testColl");
+
+class AggCmdShapeTest : public unittest::Test {
+public:
+ void setUp() final {
+ _queryTestServiceContext = std::make_unique<QueryTestServiceContext>();
+ _operationContext = _queryTestServiceContext->makeOperationContext();
+ _expCtx = make_intrusive<ExpressionContextForTest>();
+ }
+
+ std::unique_ptr<AggregateCommandRequest> makeAggregateCommandRequest(
+ std::vector<StringData> stagesJson,
+ boost::optional<StringData> letJson = boost::none,
+ boost::optional<StringData> collationJson = boost::none) {
+ std::vector<BSONObj> pipeline;
+ for (auto&& stage : stagesJson) {
+ pipeline.push_back(fromjson(stage.rawData()));
+ }
+
+ auto aggRequest =
+ std::make_unique<AggregateCommandRequest>(kDefaultTestNss, std::move(pipeline));
+ if (letJson) {
+ aggRequest->setLet(fromjson(letJson->rawData()));
+ }
+ if (collationJson) {
+ aggRequest->setCollation(fromjson(collationJson->rawData()));
+ }
+ return aggRequest;
+ }
+
+ std::unique_ptr<AggCmdShape> makeShapeFromPipeline(
+ std::vector<StringData> stagesJson,
+ boost::optional<StringData> letJson = boost::none,
+ boost::optional<StringData> collationJson = boost::none) {
+
+ auto aggRequest = makeAggregateCommandRequest(
+ std::move(stagesJson), std::move(letJson), std::move(collationJson));
+
+ auto parsedPipeline = Pipeline::parse(aggRequest->getPipeline(), _expCtx);
+ return std::make_unique<AggCmdShape>(*aggRequest,
+ kDefaultTestNss,
+ stdx::unordered_set<NamespaceString>{kDefaultTestNss},
+ *parsedPipeline,
+ _expCtx);
+ }
+ std::unique_ptr<AggCmdShapeComponents> makeShapeComponentsFromPipeline(
+ std::vector<StringData> stagesJson, OptionalBool allowDiskUse = {}) {
+ auto aggRequest = makeAggregateCommandRequest(std::move(stagesJson));
+
+ auto parsedPipeline = Pipeline::parse(aggRequest->getPipeline(), _expCtx);
+ return std::make_unique<AggCmdShapeComponents>(
+ *aggRequest,
+ stdx::unordered_set<NamespaceString>{kDefaultTestNss},
+ parsedPipeline->serializeToBson(
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions));
+ }
+
+ std::unique_ptr<QueryTestServiceContext> _queryTestServiceContext;
+
+ ServiceContext::UniqueOperationContext _operationContext;
+ boost::intrusive_ptr<ExpressionContext> _expCtx;
+};
+
+TEST_F(AggCmdShapeTest, BasicPipelineShape) {
+ auto shape =
+ makeShapeFromPipeline({R"({$match: {x: 3, y: {$lte: 3}}})"_sd,
+ R"({$group: {_id: "$y", z: {$max: "$z"}, w: {$avg: "$w"}}})"});
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "cmdNs": {
+ "db": "testDB",
+ "coll": "testColl"
+ },
+ "command": "aggregate",
+ "pipeline": [
+ {
+ "$match": {
+ "$and": [
+ {
+ "x": {
+ "$eq": "?number"
+ }
+ },
+ {
+ "y": {
+ "$lte": "?number"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "$group": {
+ "_id": "$y",
+ "z": {
+ "$max": "$z"
+ },
+ "w": {
+ "$avg": "$w"
+ }
+ }
+ }
+ ]
+ })",
+ shape->toBson(_operationContext.get(),
+ SerializationOptions::kDebugQueryShapeSerializeOptions));
+}
+
+TEST_F(AggCmdShapeTest, IncludesLet) {
+ auto shape = makeShapeFromPipeline({R"({$match: {x: 3}})"_sd, R"({$limit: 2})"_sd},
+ R"({x: 4, y: "str"})"_sd);
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "cmdNs": {
+ "db": "testDB",
+ "coll": "testColl"
+ },
+ "let": {
+ "x": "?number",
+ "y": "?string"
+ },
+ "command": "aggregate",
+ "pipeline": [
+ {
+ "$match": {
+ "x": {
+ "$eq": "?number"
+ }
+ }
+ },
+ {
+ "$limit": "?number"
+ }
+ ]
+ })",
+ shape->toBson(_operationContext.get(),
+ SerializationOptions::kDebugQueryShapeSerializeOptions));
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "cmdNs": {
+ "db": "testDB",
+ "coll": "testColl"
+ },
+ "let": {
+ "x": {
+ "$const": 1
+ },
+ "y": {
+ "$const": "?"
+ }
+ },
+ "command": "aggregate",
+ "pipeline": [
+ {
+ "$match": {
+ "x": {
+ "$eq": 1
+ }
+ }
+ },
+ {
+ "$limit": 1
+ }
+ ]
+ })",
+ shape->toBson(_operationContext.get(),
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions));
+}
+
+TEST_F(AggCmdShapeTest, SizeOfAggCmdShapeComponents) {
+ auto aggComponents = makeShapeComponentsFromPipeline(
+ {R"({$match: {x: 3, y: {$lte: 3}}})"_sd,
+ R"({$group: {_id: "$y", z: {$max: "$z"}, w: {$avg: "$w"}}})"},
+ false /*allowDiskUse*/);
+
+ // The sizes of any members of AggCmdShapeComponents are typically accounted for by
+ // sizeof(AggCmdShapeComponents). The important part of the test here is to ensure that any
+ // additional memory allocations are also included in the size() operation. In our case,
+ // we expect additional memory use from the representative pipeline and the involved
+ // namespaces set.
+ const auto pipelineSize = shape_helpers::containerSize(aggComponents->representativePipeline);
+ const auto involvedNamespacesSize = sizeof(kDefaultTestNss) +
+ kDefaultTestNss.size(); // kDefaultTestNss is the only value in the unordered set.
+
+ ASSERT_EQ(aggComponents->size(),
+ sizeof(AggCmdShapeComponents) + pipelineSize + involvedNamespacesSize);
+}
+
+TEST_F(AggCmdShapeTest, EquivalentAggCmdShapeComponentSizes) {
+ auto aggComponentsDiskUseFalse = makeShapeComponentsFromPipeline(
+ {R"({$match: {x: 3, y: {$lte: 3}}})"_sd,
+ R"({$group: {_id: "$y", z: {$max: "$z"}, w: {$avg: "$w"}}})"},
+ false /*allowDiskUse*/);
+ auto aggComponentsDiskUseTrue = makeShapeComponentsFromPipeline(
+ {R"({$match: {x: 3, y: {$lte: 3}}})"_sd,
+ R"({$group: {_id: "$y", z: {$max: "$z"}, w: {$avg: "$w"}}})"},
+ true /*allowDiskUse*/);
+ ASSERT_EQ(aggComponentsDiskUseFalse->size(), aggComponentsDiskUseTrue->size());
+}
+
+TEST_F(AggCmdShapeTest, DifferentAggCmdShapeComponentSizes) {
+ auto smallAggComponents = makeShapeComponentsFromPipeline({R"({$match: {x: 3, y: {$lte: 3}}})"},
+ false /*allowDiskUse*/);
+ auto largeAggComponents = makeShapeComponentsFromPipeline(
+ {R"({$match: {x: 3, y: {$lte: 3}}})"_sd,
+ R"({$group: {_id: "$y", z: {$max: "$z"}, w: {$avg: "$w"}}})"},
+ false /*allowDiskUse*/);
+ ASSERT_LT(smallAggComponents->size(), largeAggComponents->size());
+}
+
+TEST_F(AggCmdShapeTest, SizeOfAggCmdShapeWithAndWithoutLet) {
+ auto shapeWithoutLet = makeShapeFromPipeline({R"({$match: {x: 3}})"_sd, R"({$limit: 2})"_sd});
+ auto shapeWithLet = makeShapeFromPipeline({R"({$match: {x: 3}})"_sd, R"({$limit: 2})"_sd},
+ R"({x: 4, y: "str"})"_sd);
+ ASSERT_LT(shapeWithoutLet->size(), shapeWithLet->size());
+}
+
+TEST_F(AggCmdShapeTest, SizeOfAggCmdShapeWithAndWithoutCollation) {
+ auto shapeWithoutCollation =
+ makeShapeFromPipeline({R"({$match: {x: 3}})"_sd, R"({$limit: 2})"_sd});
+ auto shapeWithCollation = makeShapeFromPipeline(
+ {R"({$match: {x: 3}})"_sd, R"({$limit: 2})"_sd}, boost::none, R"({locale: "en_US"})"_sd);
+ ASSERT_LT(shapeWithoutCollation->size(), shapeWithCollation->size());
+}
+} // namespace
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/cmd_with_let_shape.cpp b/src/mongo/db/query/query_shape/cmd_with_let_shape.cpp
new file mode 100644
index 00000000000..2bbb6dfeadc
--- /dev/null
+++ b/src/mongo/db/query/query_shape/cmd_with_let_shape.cpp
@@ -0,0 +1,107 @@
+/**
+ * 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/query/query_shape/cmd_with_let_shape.h"
+
+namespace mongo::query_shape {
+
+namespace {
+BSONObj extractLetShape(BSONObj letSpec,
+ const SerializationOptions& opts,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx) {
+ if (letSpec.isEmpty()) {
+ // Fast path for the common case.
+ return letSpec;
+ }
+
+ BSONObjBuilder bob;
+ for (BSONElement elem : letSpec) {
+ auto expr = Expression::parseOperand(expCtx.get(), elem, expCtx->variablesParseState);
+ auto redactedValue = expr->serialize(opts);
+ // Note that this will throw on deeply nested let variables.
+ redactedValue.addToBsonObj(&bob, opts.serializeFieldPathFromString(elem.fieldName()));
+ }
+ return bob.obj();
+}
+
+auto representativeLetShape(boost::optional<BSONObj> let,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx) {
+ return let ? extractLetShape(
+ *let, SerializationOptions::kRepresentativeQueryShapeSerializeOptions, expCtx)
+ : BSONObj();
+}
+} // namespace
+
+LetShapeComponent::LetShapeComponent(boost::optional<BSONObj> let,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const CmdSpecificShapeComponents& unownedInnerComponents_)
+ : shapifiedLet(representativeLetShape(let, expCtx)),
+ hasLet(bool(let)),
+ unownedInnerComponents(unownedInnerComponents_) {}
+
+void LetShapeComponent::HashValue(absl::HashState state) const {
+ state = absl::HashState::combine(
+ std::move(state), hasLet, simpleHash(shapifiedLet), unownedInnerComponents);
+}
+
+size_t LetShapeComponent::size() const {
+ return sizeof(LetShapeComponent) + shapifiedLet.objsize() + unownedInnerComponents.size();
+}
+
+void LetShapeComponent::addLetBson(BSONObjBuilder& bob,
+ const SerializationOptions& opts,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx) const {
+ if (hasLet) {
+ auto shapeToAppend = shapifiedLet;
+ if (opts != SerializationOptions::kRepresentativeQueryShapeSerializeOptions) {
+ // We have the representative query cached/stored here, but the caller is asking for a
+ // different format, so we must re-compute.
+ shapeToAppend = extractLetShape(shapifiedLet, opts, expCtx);
+ }
+ bob.append(FindCommandRequest::kLetFieldName, shapeToAppend);
+ }
+}
+
+void CmdWithLetShape::appendCmdSpecificShapeComponents(BSONObjBuilder& bob,
+ OperationContext* opCtx,
+ const SerializationOptions& opts) const {
+ auto expCtx =
+ ExpressionContext::makeBlankExpressionContext(opCtx, nssOrUUID, _let.shapifiedLet);
+ _let.addLetBson(bob, opts, expCtx);
+ appendLetCmdSpecificShapeComponents(bob, expCtx, opts);
+}
+
+CmdWithLetShape::CmdWithLetShape(boost::optional<BSONObj> let,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const CmdSpecificShapeComponents& unownedInnerComponents,
+ NamespaceStringOrUUID nssOrUUID,
+ BSONObj collation)
+ : Shape(nssOrUUID, collation), _let(let, expCtx, unownedInnerComponents) {}
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/cmd_with_let_shape.h b/src/mongo/db/query/query_shape/cmd_with_let_shape.h
new file mode 100644
index 00000000000..a1c127b9999
--- /dev/null
+++ b/src/mongo/db/query/query_shape/cmd_with_let_shape.h
@@ -0,0 +1,109 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include "mongo/bson/bsonobj.h"
+#include "mongo/db/pipeline/expression_context.h"
+#include "mongo/db/query/parsed_find_command.h"
+#include "mongo/db/query/query_shape/query_shape.h"
+#include "mongo/db/query/query_shape/serialization_options.h"
+
+namespace mongo::query_shape {
+
+/**
+ * This struct is bit of a weird one. We want to use it as the shape's _entire_ "specific
+ * components" (rather than introduce more virtual functions to that interface). So, we track here
+ * the let component (as the name suggests) but we also keep an unowned reference to the specific
+ * components of CmdWithLetShape sub-classes. This class doesn't really do all that much with those
+ * components except track a reference to them and ensure their size is accounted for and their hash
+ * value is incorporated.
+ */
+struct LetShapeComponent : public CmdSpecificShapeComponents {
+ LetShapeComponent(boost::optional<BSONObj> let,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const CmdSpecificShapeComponents& unownedInnerComponents);
+
+ /**
+ * Hashes to include the shapified let parameters and also the hash of 'unownedInnerComponents'.
+ */
+ void HashValue(absl::HashState state) const final;
+
+ /**
+ * Includes the size of the let parameters and the size of 'unownedInnerComponents.'
+ */
+ size_t size() const final;
+
+ /**
+ * Adds _only_ the let params.
+ */
+ void addLetBson(BSONObjBuilder&,
+ const SerializationOptions&,
+ const boost::intrusive_ptr<ExpressionContext>&) const;
+
+ BSONObj shapifiedLet;
+ bool hasLet;
+ // Tracked so that this can be hash combined correctly.
+ const CmdSpecificShapeComponents& unownedInnerComponents;
+};
+
+/**
+ * The 'let' command argument is semi-generic in that it is supported in a couple commands. However
+ * it is treated specially since it supports using expressions as the let constants. Using
+ * expressions induces a library dependency that we don't want in the Shape interface itself. So
+ * this class handles tracking and adding the 'let' component of the shape for sub-classes.
+ */
+class CmdWithLetShape : public Shape {
+public:
+ CmdWithLetShape(boost::optional<BSONObj> let,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const CmdSpecificShapeComponents& unownedInnerComponents,
+ NamespaceStringOrUUID,
+ BSONObj collation_);
+
+ const CmdSpecificShapeComponents& specificComponents() const final {
+ return _let;
+ }
+
+protected:
+ void appendCmdSpecificShapeComponents(BSONObjBuilder&,
+ OperationContext* opCtx,
+ const SerializationOptions& opts) const final;
+ virtual void appendLetCmdSpecificShapeComponents(
+ BSONObjBuilder&,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const SerializationOptions&) const = 0;
+
+ LetShapeComponent _let;
+};
+static_assert(sizeof(CmdWithLetShape) == sizeof(Shape) + sizeof(LetShapeComponent),
+ "If the class' members have changed, this assert and the extraSize() calculation may "
+ "need to be updated with a new value.");
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/cmd_with_let_shape_test.cpp b/src/mongo/db/query/query_shape/cmd_with_let_shape_test.cpp
new file mode 100644
index 00000000000..21812d97fc7
--- /dev/null
+++ b/src/mongo/db/query/query_shape/cmd_with_let_shape_test.cpp
@@ -0,0 +1,78 @@
+/**
+ * Copyright (C) 2024-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/pipeline/expression_context_for_test.h"
+#include "mongo/db/query/query_shape/cmd_with_let_shape.h"
+#include "mongo/db/service_context_test_fixture.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo::query_shape {
+
+namespace {
+static const NamespaceString kDefaultTestNss = NamespaceString("testDB.testColl");
+
+class CmdWithLetShapeTest : public unittest::Test {};
+
+
+struct DummyInnerComponent : public CmdSpecificShapeComponents {
+ DummyInnerComponent(){};
+ void HashValue(absl::HashState state) const {}
+ size_t size() const final {
+ return sizeof(*this);
+ }
+};
+
+TEST_F(CmdWithLetShapeTest, SizeOfLetShapeComponent) {
+ auto expCtx = make_intrusive<ExpressionContextForTest>();
+ auto let = fromjson(R"({x: 4, y: "str"})");
+ auto innerComponents = std::make_unique<DummyInnerComponent>();
+ auto components = std::make_unique<LetShapeComponent>(let, expCtx, *innerComponents);
+
+ const auto minimumSize = sizeof(CmdSpecificShapeComponents) + sizeof(BSONObj) + sizeof(bool) +
+ sizeof(void*) /*CmdSpecificShapeComponents&*/ +
+ static_cast<size_t>(components->shapifiedLet.objsize()) +
+ components->unownedInnerComponents.size();
+
+ ASSERT_GTE(components->size(), minimumSize);
+ ASSERT_LTE(components->size(), minimumSize + 8 /*padding*/);
+}
+
+TEST_F(CmdWithLetShapeTest, SizeOfComponentWithAndWithoutLet) {
+ auto expCtx = make_intrusive<ExpressionContextForTest>();
+ auto let = fromjson(R"({x: 4, y: "str"})");
+ auto innerComponents = std::make_unique<DummyInnerComponent>();
+ auto componentsWithLet = std::make_unique<LetShapeComponent>(let, expCtx, *innerComponents);
+ auto componentsWithNoLet =
+ std::make_unique<LetShapeComponent>(boost::none, expCtx, *innerComponents);
+
+ ASSERT_LT(componentsWithNoLet->size(), componentsWithLet->size());
+}
+
+} // namespace
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/find_cmd_shape.cpp b/src/mongo/db/query/query_shape/find_cmd_shape.cpp
new file mode 100644
index 00000000000..2d018de2619
--- /dev/null
+++ b/src/mongo/db/query/query_shape/find_cmd_shape.cpp
@@ -0,0 +1,227 @@
+/**
+ * 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/query/query_shape/find_cmd_shape.h"
+
+#include "mongo/db/query/projection_ast_util.h"
+#include "mongo/db/query/query_shape/shape_helpers.h"
+
+namespace mongo::query_shape {
+namespace {
+
+BSONObj projectionShape(const boost::optional<projection_ast::Projection>& proj,
+ const SerializationOptions& opts =
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions) {
+ return proj ? projection_ast::serialize(*proj->root(), opts) : BSONObj();
+}
+
+BSONObj sortShape(const boost::optional<SortPattern>& sort,
+ const SerializationOptions& opts =
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions) {
+ return sort
+ ? sort->serialize(SortPattern::SortKeySerialization::kForPipelineSerialization, opts)
+ .toBson()
+ : BSONObj();
+}
+
+void maybeAddWithName(const OptionalBool& optBool, BSONObjBuilder& bob, StringData name) {
+ if (optBool.has_value()) {
+ bob.append(name, bool(optBool));
+ }
+}
+
+void addRemainingFindCommandFields(const FindCmdShapeComponents& components, BSONObjBuilder& bob) {
+ maybeAddWithName(components.singleBatch, bob, FindCommandRequest::kSingleBatchFieldName);
+ maybeAddWithName(components.allowDiskUse, bob, FindCommandRequest::kAllowDiskUseFieldName);
+ maybeAddWithName(components.returnKey, bob, FindCommandRequest::kReturnKeyFieldName);
+ maybeAddWithName(components.showRecordId, bob, FindCommandRequest::kShowRecordIdFieldName);
+ maybeAddWithName(components.tailable, bob, FindCommandRequest::kTailableFieldName);
+ maybeAddWithName(components.awaitData, bob, FindCommandRequest::kAwaitDataFieldName);
+ maybeAddWithName(components.oplogReplay, bob, FindCommandRequest::kOplogReplayFieldName);
+}
+
+} // namespace
+
+FindCmdShapeComponents::FindCmdShapeComponents(
+ const ParsedFindCommand& request,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const SerializationOptions& opts)
+ : filter(request.filter->serialize(opts)),
+ projection(projectionShape(request.proj, opts)),
+ sort(sortShape(request.sort, opts)),
+ min(shape_helpers::extractMinOrMaxShape(request.findCommandRequest->getMin(), opts)),
+ max(shape_helpers::extractMinOrMaxShape(request.findCommandRequest->getMax(), opts)),
+ singleBatch(request.findCommandRequest->getSingleBatch()),
+ allowDiskUse(request.findCommandRequest->getAllowDiskUse().has_value()
+ ? boost::optional<bool>(bool(request.findCommandRequest->getAllowDiskUse()))
+ : boost::none),
+ returnKey(request.findCommandRequest->getReturnKey()),
+ showRecordId(request.findCommandRequest->getShowRecordId()),
+ tailable(request.findCommandRequest->getTailable()),
+ awaitData(request.findCommandRequest->getAwaitData()),
+ oplogReplay(request.findCommandRequest->getOplogReplay()),
+ hasField(),
+ serializationOpts(opts) {
+ hasField.projection = request.proj.has_value();
+ hasField.sort = request.sort.has_value();
+ hasField.limit = request.findCommandRequest->getLimit().has_value();
+ hasField.skip = request.findCommandRequest->getSkip().has_value();
+}
+
+void FindCmdShapeComponents::appendTo(BSONObjBuilder& bob) const {
+
+ bob.append("command", "find");
+
+ std::unique_ptr<MatchExpression> filterExpr;
+ // Filter.
+ bob.append(FindCommandRequest::kFilterFieldName, filter);
+
+ if (hasField.projection) {
+ bob.append(FindCommandRequest::kProjectionFieldName, projection);
+ }
+
+ if (!max.isEmpty()) {
+ bob.append(FindCommandRequest::kMaxFieldName, max);
+ }
+ if (!min.isEmpty()) {
+ bob.append(FindCommandRequest::kMinFieldName, min);
+ }
+
+ // Sort.
+ if (hasField.sort) {
+ bob.append(FindCommandRequest::kSortFieldName, sort);
+ }
+
+ // The values here don't matter (assuming we're not using the 'kUnchanged' policy).
+ tassert(7973601,
+ "Serialization policy not supported - original values have been discarded",
+ serializationOpts.literalPolicy != LiteralSerializationPolicy::kUnchanged);
+ if (hasField.limit) {
+ serializationOpts.appendLiteral(&bob, FindCommandRequest::kLimitFieldName, 1ll);
+ }
+ if (hasField.skip) {
+ serializationOpts.appendLiteral(&bob, FindCommandRequest::kSkipFieldName, 1ll);
+ }
+
+ // Add the fields that require no transformation.
+ addRemainingFindCommandFields(*this, bob);
+}
+
+void FindCmdShapeComponents::HashValue(absl::HashState state) const {
+ absl::HashState::combine(std::move(state),
+ simpleHash(filter),
+ simpleHash(projection),
+ simpleHash(sort),
+ simpleHash(min),
+ simpleHash(max),
+ singleBatch,
+ allowDiskUse,
+ returnKey,
+ showRecordId,
+ tailable,
+ awaitData,
+ oplogReplay,
+ hasField);
+}
+
+std::unique_ptr<FindCommandRequest> FindCmdShape::toFindCommandRequest() const {
+ auto fcr = std::make_unique<FindCommandRequest>(nssOrUUID);
+
+ fcr->setFilter(components.filter);
+ if (components.hasField.projection)
+ fcr->setProjection(components.projection);
+ if (components.hasField.sort)
+ fcr->setSort(components.sort);
+
+ fcr->setMin(components.min);
+ fcr->setMax(components.max);
+
+ // Doesn't matter what value to use for limit and skip in the context of a shape.
+ if (components.hasField.limit)
+ fcr->setLimit(1ll);
+ if (components.hasField.skip)
+ fcr->setSkip(1ll);
+
+ // All the booleans.
+ if (components.singleBatch.has_value())
+ fcr->setSingleBatch(bool(components.singleBatch));
+ if (components.allowDiskUse.has_value())
+ fcr->setAllowDiskUse(bool(components.allowDiskUse));
+ if (components.returnKey.has_value())
+ fcr->setReturnKey(bool(components.returnKey));
+ if (components.showRecordId.has_value())
+ fcr->setShowRecordId(bool(components.showRecordId));
+ if (components.tailable.has_value())
+ fcr->setTailable(bool(components.tailable));
+ if (components.awaitData.has_value())
+ fcr->setAwaitData(bool(components.awaitData));
+ if (components.oplogReplay.has_value())
+ fcr->setOplogReplay(bool(components.oplogReplay));
+
+ // Common shape components.
+ if (_let.hasLet)
+ fcr->setLet(_let.shapifiedLet);
+ if (!collation.isEmpty())
+ fcr->setCollation(collation);
+
+
+ return fcr;
+}
+
+FindCmdShape::FindCmdShape(const ParsedFindCommand& findRequest,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx)
+ : CmdWithLetShape(findRequest.findCommandRequest->getLet(),
+ expCtx,
+ components,
+ findRequest.findCommandRequest->getNamespaceOrUUID(),
+ findRequest.findCommandRequest->getCollation()),
+ components(findRequest, expCtx) {}
+
+void FindCmdShape::appendLetCmdSpecificShapeComponents(
+ BSONObjBuilder& bob,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const SerializationOptions& opts) const {
+ if (opts == SerializationOptions::kRepresentativeQueryShapeSerializeOptions) {
+ // Fast path: we already have this.
+ return components.appendTo(bob);
+ } else {
+ // Slow path: we need to re-parse from our representative shapes.
+ auto request = uassertStatusOKWithContext(
+ parsed_find_command::parse(expCtx,
+ toFindCommandRequest(),
+ ExtensionsCallbackNoop(),
+ MatchExpressionParser::kAllowAllSpecialFeatures),
+ "Could not re-parse a representative query shape");
+
+ // This constructor will shapify according to the options.
+ FindCmdShapeComponents{*request, expCtx, opts}.appendTo(bob);
+ }
+}
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/find_cmd_shape.h b/src/mongo/db/query/query_shape/find_cmd_shape.h
new file mode 100644
index 00000000000..49d70b8ec27
--- /dev/null
+++ b/src/mongo/db/query/query_shape/find_cmd_shape.h
@@ -0,0 +1,130 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include "mongo/db/query/query_shape/cmd_with_let_shape.h"
+#include "mongo/db/query/query_shape/query_shape.h"
+#include "mongo/db/query/query_shape/shape_helpers.h"
+
+namespace mongo::query_shape {
+
+/**
+ * This struct tracks the components of a find command which are important for the find query shape.
+ * It attempts to only track those which are _unique_ to a find command - common elements should go
+ * on some super class.
+ *
+ * Data elements which are shapified like 'filter' are stored in their shapified form. By default
+ * and in most cases this will be the representative query shape form so that it can be re-parsed,
+ * but as a convenience for serializing it is also supported to construct and serialize this with
+ * other options.
+ */
+struct FindCmdShapeComponents : public CmdSpecificShapeComponents {
+
+ FindCmdShapeComponents(const ParsedFindCommand& request,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const SerializationOptions& opts =
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions);
+
+ /**
+ * Appends using the SerializationOptions given in the constructor.
+ */
+ void appendTo(BSONObjBuilder&) const;
+
+ size_t size() const final {
+ return sizeof(FindCmdShapeComponents) + filter.objsize() + projection.objsize() +
+ sort.objsize() + min.objsize() + max.objsize();
+ }
+
+ BSONObj filter;
+ BSONObj projection;
+ BSONObj sort;
+ BSONObj min;
+ BSONObj max;
+
+ OptionalBool singleBatch;
+ OptionalBool allowDiskUse;
+ OptionalBool returnKey;
+ OptionalBool showRecordId;
+ OptionalBool tailable;
+ OptionalBool awaitData;
+ OptionalBool oplogReplay;
+
+ // This anonymous struct represents the presence of the member variables as C++ bit fields.
+ // In doing so, each of these boolean values takes up 1 bit instead of 1 byte.
+ struct HasField {
+ HasField() : projection(false), sort(false), limit(false), skip(false) {}
+ bool projection : 1;
+ bool sort : 1;
+ bool limit : 1;
+ bool skip : 1;
+ } hasField;
+
+ // We save a copy of the options used when constructed so we know how to properly append things
+ // like limit and skip - either a 1 or "?number". We could have the caller pass the options
+ // again during 'appendTo()', but this introduces a risk that the options provided are different
+ // than the ones we used to compute 'filter' and the other components.
+ SerializationOptions serializationOpts;
+
+ void HashValue(absl::HashState state) const final;
+};
+
+class FindCmdShape : public CmdWithLetShape {
+public:
+ FindCmdShape(const ParsedFindCommand& findRequest,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx);
+
+ /**
+ * Assembles a parseable FindCommandRequest representing this shape - some of the pieces are
+ * stored right here in the shape, others are in parent classes.
+ */
+ std::unique_ptr<FindCommandRequest> toFindCommandRequest() const;
+
+ FindCmdShapeComponents components;
+
+protected:
+ void appendLetCmdSpecificShapeComponents(BSONObjBuilder& bob,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const SerializationOptions& opts) const final;
+};
+
+template <typename H>
+H AbslHashValue(H h, const FindCmdShapeComponents::HasField& hasField) {
+ return H::combine(
+ std::move(h), hasField.projection, hasField.sort, hasField.limit, hasField.skip);
+}
+
+// This assertion is still active on the maintained master branch. On the v6.0 branch, we disable it
+// since it is not passing on all toolchains/platforms - notably x86 macOS. The intent of the
+// assertion is to prevent accidental additions of data members, which should not happen on this
+// branch without first happening on the master branch and passing that assertion.
+// static_assert(sizeof(FindCmdShape) == sizeof(CmdWithLetShape) + sizeof(FindCmdShapeComponents),
+// "If the class' members have changed, this assert and the extraSize() calculation
+// may " "need to be updated with a new value.");
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/find_cmd_shape_test.cpp b/src/mongo/db/query/query_shape/find_cmd_shape_test.cpp
new file mode 100644
index 00000000000..0d839a5d3d2
--- /dev/null
+++ b/src/mongo/db/query/query_shape/find_cmd_shape_test.cpp
@@ -0,0 +1,238 @@
+/**
+ * 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/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<ExpressionContextForTest>();
+ }
+
+ std::unique_ptr<FindCmdShape> makeShapeFromSort(StringData sortJson) {
+ auto fcr = std::make_unique<FindCommandRequest>(kDefaultTestNss);
+ fcr->setSort(fromjson(sortJson.rawData()));
+ auto&& parsedRequest =
+ uassertStatusOK(::mongo::parsed_find_command::parse(_expCtx, std::move(fcr)));
+ return std::make_unique<FindCmdShape>(*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<BSONObj> 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<ExpressionContext> _expCtx;
+
+ std::unique_ptr<FindCmdShapeComponents> makeShapeComponentsFromFilter(
+ BSONObj filter, const RequestOptions& requestOptions = {}) {
+ auto fcr = std::make_unique<FindCommandRequest>(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<FindCmdShapeComponents>(*parsedFind, _expCtx);
+ }
+
+ std::unique_ptr<FindCmdShape> makeShapeFromFilter(const BSONObj& filter) {
+ auto fcr = std::make_unique<FindCommandRequest>(kDefaultTestNss);
+ fcr->setFilter(filter.getOwned());
+ auto parsedFind = uassertStatusOK(parsed_find_command::parse(_expCtx, {std::move(fcr)}));
+ return std::make_unique<FindCmdShape>(*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<size_t>(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<FindCommandRequest>(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<FindCmdShape>(*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<FindCommandRequest>(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<FindCmdShape>(*parsedFind, _expCtx);
+
+ ASSERT_LT(shapeWithoutCollation->size(), shapeWithCollation->size());
+}
+
+} // namespace
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/query_shape.cpp b/src/mongo/db/query/query_shape/query_shape.cpp
new file mode 100644
index 00000000000..2fa0520120e
--- /dev/null
+++ b/src/mongo/db/query/query_shape/query_shape.cpp
@@ -0,0 +1,103 @@
+/**
+ * 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/query/query_shape/query_shape.h"
+
+#include "mongo/base/status.h"
+#include "mongo/crypto/sha256_block.h"
+#include "mongo/db/query/find_command_gen.h"
+#include "mongo/db/query/projection_ast_util.h"
+#include "mongo/db/query/query_request_helper.h"
+#include "mongo/db/query/query_shape/agg_cmd_shape.h"
+#include "mongo/db/query/query_shape/find_cmd_shape.h"
+#include "mongo/db/query/query_shape/query_shape_gen.h"
+#include "mongo/db/query/query_shape/shape_helpers.h"
+#include "mongo/db/query/sort_pattern.h"
+
+namespace mongo::query_shape {
+
+namespace {
+void appendCmdNs(BSONObjBuilder& bob,
+ const NamespaceString& nss,
+ const SerializationOptions& opts) {
+ BSONObjBuilder nsObj = bob.subobjStart("cmdNs");
+ shape_helpers::appendNamespaceShape(nsObj, nss, opts);
+ nsObj.doneFast();
+}
+} // namespace
+
+Shape::Shape(NamespaceStringOrUUID nssOrUUID_, BSONObj collation_)
+ : nssOrUUID(nssOrUUID_), collation(std::move(collation_)) {}
+
+
+BSONObj Shape::toBson(OperationContext* opCtx, const SerializationOptions& opts) const {
+ BSONObjBuilder bob;
+ appendCmdNsOrUUID(bob, opts);
+ if (!collation.isEmpty()) {
+ // Collation is never shapified. We use find command's collation name definition, but it
+ // should be the same for all requests.
+ bob.append(FindCommandRequest::kCollationFieldName, collation);
+ }
+ appendCmdSpecificShapeComponents(bob, opCtx, opts);
+ return bob.obj();
+}
+
+size_t Shape::size() const {
+ return sizeof(Shape) + shape_helpers::optionalObjSize(collation) + specificComponents().size() +
+ extraSize();
+}
+
+QueryShapeHash Shape::sha256Hash(OperationContext* opCtx) const {
+ // The Query Shape Hash should use the representative query shape.
+ auto serialized =
+ toBson(opCtx, SerializationOptions::kRepresentativeQueryShapeSerializeOptions);
+ return SHA256Block::computeHash((const uint8_t*)serialized.sharedBuffer().get(),
+ serialized.objsize());
+}
+
+void Shape::appendCmdNsOrUUID(BSONObjBuilder& bob, const SerializationOptions& opts) const {
+ if (nssOrUUID.nss()) {
+ appendCmdNs(bob, *nssOrUUID.nss(), opts);
+ } else {
+ BSONObjBuilder cmdNs = bob.subobjStart("cmdNs");
+ cmdNs.append("uuid", opts.serializeIdentifier(nssOrUUID.uuid()->toString()));
+ cmdNs.append("db", opts.serializeIdentifier(nssOrUUID.db()));
+ cmdNs.doneFast();
+ }
+}
+
+void Shape::appendCmdNs(BSONObjBuilder& bob,
+ const NamespaceString& nss,
+ const SerializationOptions& opts) const {
+ BSONObjBuilder nsObj = bob.subobjStart("cmdNs");
+ shape_helpers::appendNamespaceShape(nsObj, nss, opts);
+ nsObj.doneFast();
+}
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/query_shape.h b/src/mongo/db/query/query_shape/query_shape.h
new file mode 100644
index 00000000000..dc83cbab127
--- /dev/null
+++ b/src/mongo/db/query/query_shape/query_shape.h
@@ -0,0 +1,165 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include "mongo/db/matcher/expression.h"
+#include "mongo/db/pipeline/aggregate_command_gen.h"
+#include "mongo/db/query/find_command_gen.h"
+#include "mongo/db/query/query_request_helper.h"
+#include "mongo/db/query/query_shape/shape_helpers.h"
+
+namespace mongo::query_shape {
+
+/**
+ * Each type of "query" command likely has different fields/options that are considered important
+ * for the shape. For example, a find command has a skip and a limit, and an aggregate command has a
+ * pipeline. This interface is used to allow different sub-commands to diverge in this way but still
+ * ensure we can appropriately hash them to compare their shapes, and properly account for their
+ * size.
+ *
+ * This struct is split out as a separate inheritence hierarchy from 'Shape' to make it easier to
+ * ensure each piece is hashed without sub-classes needing to enumerate the parent class's member
+ * variables.
+ */
+struct CmdSpecificShapeComponents {
+ virtual ~CmdSpecificShapeComponents() {}
+
+ /**
+ * Sub-classes should implement this in a way which includes all shape-relevant state. If two
+ * shapes should compare equal, they should result in the same hash value. For example for the
+ * find command - we would include the _shapified_ filter and projection here, but we will not
+ * include the comment - which is not part of the shape.
+ */
+ virtual void HashValue(absl::HashState state) const = 0;
+
+ /**
+ * It is important for shape components to accurately report their size, and to make a
+ * reasonable effort to maintain a minimal size. We use the query shape in memory-constrained
+ * data structures, so a bigger shape means we can have fewer different shapes stored (for
+ * example in the query stats store).
+ *
+ * We cannot just use sizeof() because there are some variable size data members (like BSON
+ * objects) which depend on the particular instance.
+ */
+ virtual size_t size() const = 0;
+
+ // Some template boilerplate to allow sub-classes to overload the hash implementation.
+ template <typename H>
+ friend H AbslHashValue(H state, const CmdSpecificShapeComponents& value) {
+ value.HashValue(absl::HashState::Create(&state));
+ return std::move(state);
+ }
+};
+
+using QueryShapeHash = SHA256Block;
+
+/**
+ * A query "shape" is a version of a command with literal values abstracted so that two instances of
+ * the command may compare/hash equal even if they use slightly different literal values. This
+ * concept exists not just the find command, but planned for many of the CRUD commands + aggregate.
+ * It also includes most (but not all) components of these commands, not just the query predicate
+ * (MatchExpresssion). In these ways, "query" is meant more generally.
+ *
+ * A "Query Shape" can vary depending on the command (e.g. find, aggregate, or distinct). This
+ * abstract struct is the API we must implement for each command which we want to have a "shape"
+ * concept.
+ *
+ * In order to properly account for the size of a query shape, the CmdSpecificShapeComponents should
+ * include all meaningful memory consumption, and be sure to report it in 'size()'. Subclasses of
+ * 'Shape' are not expected to have any meaningful memory usage outside of that struct.
+ */
+class Shape {
+public:
+ virtual ~Shape() {}
+
+ /**
+ * Sub-classes are expected to implement this as a mechanism for plugging in their command
+ * specific shape components.
+ */
+ virtual const CmdSpecificShapeComponents& specificComponents() const = 0;
+
+ /**
+ * Note this may involve re-parsing command BSON and so is not necessarily cheap.
+ */
+ BSONObj toBson(OperationContext*, const SerializationOptions&) const;
+
+ /**
+ * The Query Shape Hash is defined to be the SHA256 Hash of the representatice query shape. This
+ * helper computes that.
+ */
+ QueryShapeHash sha256Hash(OperationContext*) const;
+
+ /**
+ * The size of a query shape is important, since we store these in space-constrained
+ * environments like the query stats store.
+ */
+ size_t size() const;
+
+ /**
+ * This should be overriden by a child class if it has members whose sizes are not included in
+ * specificComponents().size().
+ */
+ virtual size_t extraSize() const {
+ return 0;
+ }
+ template <typename H>
+ friend H AbslHashValue(H h, const Shape& shape) {
+ h = H::combine(std::move(h), shape.nssOrUUID, shape.specificComponents());
+ if (!shape.collation.isEmpty())
+ h = H::combine(std::move(h), simpleHash(shape.collation));
+ return h;
+ }
+
+
+ // Not shapified but it is an identifier so it may be transformed.
+ NamespaceStringOrUUID nssOrUUID;
+
+ // Never shapified. If it's empty, leave it off.
+ BSONObj collation;
+
+protected:
+ Shape(NamespaceStringOrUUID, BSONObj collation_);
+
+ /**
+ * Along with the hash implementation, this is the main way that shapes are 'shapified' -
+ * sub-classes should implement this to add the shapified versions of their literals to an
+ * object. Depending on 'opts', this may be eligible to be used for output in $queryStats or as
+ * the object to compute the QueryShapeHash.
+ */
+ virtual void appendCmdSpecificShapeComponents(BSONObjBuilder&,
+ OperationContext*,
+ const SerializationOptions& opts) const = 0;
+
+private:
+ void appendCmdNsOrUUID(BSONObjBuilder&, const SerializationOptions&) const;
+ void appendCmdNs(BSONObjBuilder&, const NamespaceString&, const SerializationOptions&) const;
+};
+
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/query_shape.idl b/src/mongo/db/query/query_shape/query_shape.idl
new file mode 100644
index 00000000000..77e71756467
--- /dev/null
+++ b/src/mongo/db/query/query_shape/query_shape.idl
@@ -0,0 +1,50 @@
+# 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.
+
+global:
+ cpp_namespace: "mongo::query_shape"
+
+imports:
+ - "mongo/idl/basic_types.idl"
+
+
+structs:
+ CommandNamespace:
+ description: "Representation of the cmdNs sub-object of the query shape."
+ fields:
+ db:
+ type: string
+ coll:
+ type: string
+ optional: true
+ uuid:
+ type: string
+ optional: true
+ tenantId:
+ type: string
+ optional: true
+ \ No newline at end of file
diff --git a/src/mongo/db/query/query_shape/query_shape_test.cpp b/src/mongo/db/query/query_shape/query_shape_test.cpp
new file mode 100644
index 00000000000..d6185b5c5cb
--- /dev/null
+++ b/src/mongo/db/query/query_shape/query_shape_test.cpp
@@ -0,0 +1,767 @@
+/**
+ * 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/bson/bsonmisc.h"
+#include "mongo/bson/bsonobj.h"
+#include "mongo/db/matcher/expression_geo.h"
+#include "mongo/db/matcher/extensions_callback_real.h"
+#include "mongo/db/matcher/parsed_match_expression_for_test.h"
+#include "mongo/db/namespace_string.h"
+#include "mongo/db/pipeline/expression_context_for_test.h"
+#include "mongo/db/query/query_shape/query_shape.h"
+#include "mongo/db/query/query_shape/query_shape_test_gen.h"
+#include "mongo/db/query/query_shape/serialization_options.h"
+#include "mongo/db/query/query_shape/shape_helpers.h"
+#include "mongo/db/service_context_test_fixture.h"
+#include "mongo/unittest/bson_test_util.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo::query_shape {
+
+namespace {
+BSONObj predicateShape(const MatchExpression* expr) {
+ return expr->serialize(SerializationOptions::kDebugQueryShapeSerializeOptions);
+}
+BSONObj predicateShape(std::string filterJson) {
+ return predicateShape(ParsedMatchExpressionForTest(filterJson).get());
+}
+
+BSONObj predicateShapeRedacted(const MatchExpression* expr) {
+ return expr->serialize(SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST);
+}
+BSONObj predicateShapeRedacted(std::string filterJson) {
+ return predicateShapeRedacted(ParsedMatchExpressionForTest(filterJson).get());
+}
+
+// TODO SERVER-87736 There is no 'auto' here, make that more clear.
+#define ASSERT_SHAPE_EQ_AUTO(expected, actual) \
+ ASSERT_BSONOBJ_EQ_AUTO(expected, predicateShape(actual))
+
+#define ASSERT_REDACTED_SHAPE_EQ_AUTO(expected, actual) \
+ ASSERT_BSONOBJ_EQ_AUTO(expected, predicateShapeRedacted(actual))
+
+
+TEST(QueryPredicateShape, Equals) {
+ ASSERT_SHAPE_EQ_AUTO( // Implicit equals
+ R"({"a":{"$eq":"?number"}})",
+ "{a: 5}");
+ ASSERT_SHAPE_EQ_AUTO( // Explicit equals
+ R"({"a":{"$eq":"?number"}})",
+ "{a: {$eq: 5}}");
+ ASSERT_SHAPE_EQ_AUTO( // implicit $and
+ R"({"$and":[{"a":{"$eq":"?number"}},{"b":{"$eq":"?number"}}]})",
+ "{a: 5, b: 6}");
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // Implicit equals
+ R"({"HASH<a>":{"$eq":"?number"}})",
+ "{a: 5}");
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // Explicit equals
+ R"({"HASH<a>":{"$eq":"?number"}})",
+ "{a: {$eq: 5}}");
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({"$and":[{"HASH<a>":{"$eq":"?number"}},{"HASH<b>":{"$eq":"?number"}}]})",
+ "{a: 5, b: 6}");
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({"HASH<foo>.HASH<$bar>":{"$eq":"?number"}})",
+ R"({"foo.$bar":0})");
+}
+
+TEST(QueryPredicateShape, ArraySubTypes) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ "{a: {$eq: '[]'}}",
+ "{a: []}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ "{a: {$eq: '?array<?number>'}}",
+ "{a: [2]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<?number>"}})",
+ "{a: [2, 3]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<?object>"}})",
+ "{a: [{}]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<?object>"}})",
+ "{a: [{}, {}]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<?array>"}})",
+ "{a: [[], [], []]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<?array>"}})",
+ "{a: [[2, 3], ['string'], []]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<>"}})",
+ "{a: [{}, 2]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<>"}})",
+ "{a: [[], 2]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<>"}})",
+ "{a: [[{}, 'string'], 2]}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$eq":"?array<>"}})",
+ "{a: [[{}, 'string'], 2]}");
+}
+
+TEST(QueryPredicateShape, Comparisons) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$lt": "?number"
+ }
+ },
+ {
+ "b": {
+ "$gt": "?number"
+ }
+ },
+ {
+ "c": {
+ "$gte": "?number"
+ }
+ },
+ {
+ "c": {
+ "$lte": "?number"
+ }
+ }
+ ]
+ })",
+ "{a: {$lt: 5}, b: {$gt: 6}, c: {$gte: 3, $lte: 10}}");
+}
+
+namespace {
+void assertShapeIs(std::string filterJson, BSONObj expectedShape) {
+ ASSERT_BSONOBJ_EQ(expectedShape, predicateShape(filterJson));
+}
+
+void assertRedactedShapeIs(std::string filterJson, BSONObj expectedShape) {
+ ASSERT_BSONOBJ_EQ(expectedShape, predicateShapeRedacted(filterJson));
+}
+} // namespace
+
+TEST(QueryPredicateShape, Regex) {
+ // Note/warning: 'fromjson' will parse $regex into a /regex/, so these tests can't use
+ // auto-updating BSON assertions.
+ assertShapeIs("{a: /a+/}",
+ BSON("a" << BSON("$regex"
+ << "?string")));
+ assertShapeIs("{a: /a+/i}",
+ BSON("a" << BSON("$regex"
+ << "?string"
+ << "$options"
+ << "?string")));
+ assertRedactedShapeIs("{a: /a+/}",
+ BSON("HASH<a>" << BSON("$regex"
+ << "?string")));
+ assertRedactedShapeIs("{a: /a+/}",
+ BSON("HASH<a>" << BSON("$regex"
+ << "?string")));
+}
+
+TEST(QueryPredicateShape, Mod) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$mod":["?number","?number"]}})",
+ "{a: {$mod: [2, 0]}}");
+}
+
+TEST(QueryPredicateShape, Exists) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$exists":"?bool"}})",
+ "{a: {$exists: true}}");
+}
+
+TEST(QueryPredicateShape, In) {
+ // Any number of children in any order is always the same shape
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$in":"?array<?number>"}})",
+ "{a: {$in: [1]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$in":"?array<>"}})",
+ "{a: {$in: [1, 4, 'str', /regex/]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$in":"?array<>"}})",
+ "{a: {$in: ['str', /regex/, 1, 4]}}");
+}
+
+TEST(QueryPredicateShape, BitTestOperators) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAllSet":"?array<?number>"}})",
+ "{a: {$bitsAllSet: [1, 5]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAllSet":"?array<?number>"}})",
+ "{a: {$bitsAllSet: 50}}");
+
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAnySet":"?array<?number>"}})",
+ "{a: {$bitsAnySet: [1, 5]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAnySet":"?array<?number>"}})",
+ "{a: {$bitsAnySet: 50}}");
+
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAllClear":"?array<?number>"}})",
+ "{a: {$bitsAllClear: [1, 5]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAllClear":"?array<?number>"}})",
+ "{a: {$bitsAllClear: 50}}");
+
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAnyClear":"?array<?number>"}})",
+ "{a: {$bitsAnyClear: [1, 5]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$bitsAnyClear":"?array<?number>"}})",
+ "{a: {$bitsAnyClear: 50}}");
+}
+
+TEST(QueryPredicateShape, AlwaysBoolean) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"$alwaysTrue":"?number"})",
+ "{$alwaysTrue: 1}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"$alwaysFalse":"?number"})",
+ "{$alwaysFalse: 1}");
+}
+
+TEST(QueryPredicateShape, And) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$lt": "?number"
+ }
+ },
+ {
+ "b": {
+ "$gte": "?number"
+ }
+ },
+ {
+ "c": {
+ "$lte": "?number"
+ }
+ }
+ ]
+ })",
+ "{$and: [{a: {$lt: 5}}, {b: {$gte: 3}}, {c: {$lte: 10}}]}");
+}
+
+TEST(QueryPredicateShape, Or) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({
+ "$or": [
+ {
+ "a": {
+ "$eq": "?number"
+ }
+ },
+ {
+ "b": {
+ "$in": "?array<?number>"
+ }
+ },
+ {
+ "c": {
+ "$gt": "?number"
+ }
+ }
+ ]
+ })",
+ "{$or: [{a: 5}, {b: {$in: [1,2,3]}}, {c: {$gt: 10}}]}");
+}
+
+TEST(QueryPredicateShape, ElemMatch) {
+ // ElemMatchObjectMatchExpression
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({
+ "a": {
+ "$elemMatch": {
+ "$and": [
+ {
+ "b": {
+ "$eq": "?number"
+ }
+ },
+ {
+ "c": {
+ "$exists": "?bool"
+ }
+ }
+ ]
+ }
+ }
+ })",
+ "{a: {$elemMatch: {b: 5, c: {$exists: true}}}}");
+
+ // ElemMatchValueMatchExpression
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$elemMatch":{"$gt":"?number","$lt":"?number"}}})",
+ "{a: {$elemMatch: {$gt: 5, $lt: 10}}}");
+
+ // Nested
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({
+ "HASH<a>": {
+ "$elemMatch": {
+ "$elemMatch": {
+ "$gt": "?number",
+ "$lt": "?number"
+ }
+ }
+ }
+ })",
+ "{a: {$elemMatch: {$elemMatch: {$gt: 5, $lt: 10}}}}");
+}
+
+TEST(QueryPredicateShape, InternalBucketGeoWithinMatchExpression) {
+ auto query =
+ "{ $_internalBucketGeoWithin: {withinRegion: {$centerSphere: [[0, 0], 10]}, field: "
+ "\"a\"} "
+ "}";
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({
+ "$_internalBucketGeoWithin": {
+ "withinRegion": {
+ "$centerSphere": "?array<>"
+ },
+ "field": "HASH<a>"
+ }
+ })",
+ query);
+}
+
+TEST(QueryPredicateShape, NorMatchExpression) {
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({"$nor":[{"HASH<a>":{"$lt":"?number"}},{"HASH<b>":{"$gt":"?number"}}]})",
+ "{ $nor: [ { a: {$lt: 5} }, { b: {$gt: 4} } ] }");
+}
+
+TEST(QueryPredicateShape, NotMatchExpression) {
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({"HASH<price>":{"$not":{"$gt":"?number"}}})",
+ "{ price: { $not: { $gt: 1.99 } } }");
+ // Test the special case where NotMatchExpression::serialize() reduces to $alwaysFalse.
+ auto emptyAnd = std::make_unique<AndMatchExpression>();
+ const MatchExpression& notExpr = NotMatchExpression(std::move(emptyAnd));
+ auto serialized =
+ notExpr.serialize(SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST);
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({"$alwaysFalse":"?number"})",
+ serialized);
+}
+
+TEST(QueryPredicateShape, SizeMatchExpression) {
+ ASSERT_REDACTED_SHAPE_EQ_AUTO( // NOLINT
+ R"({"HASH<price>":{"$size":"?number"}})",
+ "{ price: { $size: 2 } }");
+}
+
+TEST(QueryPredicateShape, TextMatchExpression) {
+ TextMatchExpressionBase::TextParams params = {"coffee"};
+ auto expr = ExtensionsCallbackNoop().createText(params);
+ auto literalAndFieldRedactOpts = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST;
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "$text": {
+ "$search": "?string",
+ "$language": "?string",
+ "$caseSensitive": "?bool",
+ "$diacriticSensitive": "?bool"
+ }
+ })",
+ expr->serialize(SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST));
+}
+
+TEST(QueryPredicateShape, TwoDPtInAnnulusExpression) {
+ const MatchExpression& expr = TwoDPtInAnnulusExpression({}, {});
+ auto literalAndFieldRedactOpts = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST;
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({"$TwoDPtInAnnulusExpression":true})",
+ expr.serialize(SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST));
+}
+
+TEST(QueryPredicateShape, WhereMatchExpression) {
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"$where":"?javascript"})",
+ "{$where: \"some_code()\"}");
+}
+
+BSONObj queryShapeForOptimizedExprExpression(std::string exprPredicateJson) {
+ ParsedMatchExpressionForTest expr(exprPredicateJson);
+ // We need to optimize an $expr expression in order to generate an $_internalExprEq. It's
+ // not clear we'd want to do optimization before computing the query shape, but we should
+ // support the computation on any MatchExpression, and this is the easiest way we can create
+ // this type of MatchExpression node.
+ auto optimized = MatchExpression::optimize(expr.release());
+ return predicateShape(optimized.get());
+}
+
+TEST(QueryPredicateShape, OptimizedExprPredicates) {
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$_internalExprEq": "?number"
+ }
+ },
+ {
+ "$expr": {
+ "$eq": [
+ "$a",
+ "?number"
+ ]
+ }
+ }
+ ]
+ })",
+ queryShapeForOptimizedExprExpression("{$expr: {$eq: ['$a', 2]}}"));
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$_internalExprLt": "?number"
+ }
+ },
+ {
+ "$expr": {
+ "$lt": [
+ "$a",
+ "?number"
+ ]
+ }
+ }
+ ]
+ })",
+ queryShapeForOptimizedExprExpression("{$expr: {$lt: ['$a', 2]}}"));
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$_internalExprLte": "?number"
+ }
+ },
+ {
+ "$expr": {
+ "$lte": [
+ "$a",
+ "?number"
+ ]
+ }
+ }
+ ]
+ })",
+ queryShapeForOptimizedExprExpression("{$expr: {$lte: ['$a', 2]}}"));
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$_internalExprGt": "?number"
+ }
+ },
+ {
+ "$expr": {
+ "$gt": [
+ "$a",
+ "?number"
+ ]
+ }
+ }
+ ]
+ })",
+ queryShapeForOptimizedExprExpression("{$expr: {$gt: ['$a', 2]}}"));
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "$and": [
+ {
+ "a": {
+ "$_internalExprGte": "?number"
+ }
+ },
+ {
+ "$expr": {
+ "$gte": [
+ "$a",
+ "?number"
+ ]
+ }
+ }
+ ]
+ })",
+ queryShapeForOptimizedExprExpression("{$expr: {$gte: ['$a', 2]}}"));
+}
+
+TEST(QueryShapeIDL, ShapifyIDLStruct) {
+ SerializationOptions options;
+ options.transformIdentifiers = true;
+ options.transformIdentifiersCallback = [](StringData s) -> std::string {
+ return str::stream() << "HASH<" << s << ">";
+ };
+ options.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString;
+
+ auto nested = NestedStruct("value",
+ ExampleEnumEnum::Value1,
+ "hello",
+ {1, 2, 3, 4},
+ "field.path",
+ {"field.path.1", "fieldpath2"},
+ NamespaceString{"db", "coll"},
+ NamespaceString{"db", "coll"},
+ 177,
+ true);
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "stringField": "value",
+ "enumField": "EnumValue1",
+ "stringIntVariantEnum": "hello",
+ "arrayOfInts": [
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "fieldpath": "field.path",
+ "fieldpathList": [
+ "field.path.1",
+ "fieldpath2"
+ ],
+ "nss": "db.coll",
+ "plainNss": "db.coll",
+ "safeInt64Field": 177,
+ "boolField": true
+ })",
+ nested.toBSON());
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "stringField": "?string",
+ "enumField": "EnumValue1",
+ "stringIntVariantEnum": "hello",
+ "arrayOfInts": "?array<?number>",
+ "fieldpath": "HASH<field>.HASH<path>",
+ "fieldpathList": [
+ "HASH<field>.HASH<path>.HASH<1>",
+ "HASH<fieldpath2>"
+ ],
+ "nss": "HASH<db.coll>",
+ "plainNss": "db.coll",
+ "safeInt64Field": "?number",
+ "boolField": "?bool"
+ })",
+ nested.toBSON(options));
+
+
+ auto parent = ParentStruct(nested, nested);
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "nested_shape": {
+ "stringField": "value",
+ "enumField": "EnumValue1",
+ "stringIntVariantEnum": "hello",
+ "arrayOfInts": [
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "fieldpath": "field.path",
+ "fieldpathList": [
+ "field.path.1",
+ "fieldpath2"
+ ],
+ "nss": "db.coll",
+ "plainNss": "db.coll",
+ "safeInt64Field": 177,
+ "boolField": true
+ },
+ "nested_no_shape": {
+ "stringField": "value",
+ "enumField": "EnumValue1",
+ "stringIntVariantEnum": "hello",
+ "arrayOfInts": [
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "fieldpath": "field.path",
+ "fieldpathList": [
+ "field.path.1",
+ "fieldpath2"
+ ],
+ "nss": "db.coll",
+ "plainNss": "db.coll",
+ "safeInt64Field": 177,
+ "boolField": true
+ }
+ })",
+ parent.toBSON());
+
+ ASSERT_BSONOBJ_EQ_AUTO( // NOLINT
+ R"({
+ "nested_shape": {
+ "stringField": "?string",
+ "enumField": "EnumValue1",
+ "stringIntVariantEnum": "hello",
+ "arrayOfInts": "?array<?number>",
+ "fieldpath": "HASH<field>.HASH<path>",
+ "fieldpathList": [
+ "HASH<field>.HASH<path>.HASH<1>",
+ "HASH<fieldpath2>"
+ ],
+ "nss": "HASH<db.coll>",
+ "plainNss": "db.coll",
+ "safeInt64Field": "?number",
+ "boolField": "?bool"
+ },
+ "nested_no_shape": {
+ "stringField": "value",
+ "enumField": "EnumValue1",
+ "stringIntVariantEnum": "hello",
+ "arrayOfInts": [
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "fieldpath": "field.path",
+ "fieldpathList": [
+ "field.path.1",
+ "fieldpath2"
+ ],
+ "nss": "db.coll",
+ "plainNss": "db.coll",
+ "safeInt64Field": 177,
+ "boolField": true
+ }
+ })",
+ parent.toBSON(options));
+}
+
+} // namespace
+
+namespace {
+
+static const NamespaceString kDefaultTestNss = NamespaceString("testDB.testColl");
+
+struct DummyShapeSpecificComponents : public query_shape::CmdSpecificShapeComponents {
+ DummyShapeSpecificComponents(){};
+ void HashValue(absl::HashState state) const {}
+ size_t size() const final {
+ return sizeof(DummyShapeSpecificComponents);
+ }
+};
+
+class DummyShape : public Shape {
+public:
+ DummyShape(NamespaceStringOrUUID nssOrUUID,
+ BSONObj collation,
+ DummyShapeSpecificComponents dummyComponents)
+ : Shape(nssOrUUID, collation) {
+ components = dummyComponents;
+ }
+
+ const CmdSpecificShapeComponents& specificComponents() const final {
+ return components;
+ }
+
+ void appendCmdSpecificShapeComponents(BSONObjBuilder&,
+ OperationContext*,
+ const SerializationOptions& opts) const final {}
+ DummyShapeSpecificComponents components;
+};
+
+class DummyShapeWithExtraSize : public Shape {
+public:
+ DummyShapeWithExtraSize(NamespaceStringOrUUID nssOrUUID,
+ BSONObj collation,
+ DummyShapeSpecificComponents dummyComponents)
+ : Shape(nssOrUUID, collation) {
+ components = dummyComponents;
+ }
+
+ const CmdSpecificShapeComponents& specificComponents() const final {
+ return components;
+ }
+
+ // Random number for testing purposes.
+ size_t extraSize() const final override {
+ return 125;
+ }
+ void appendCmdSpecificShapeComponents(BSONObjBuilder&,
+ OperationContext*,
+ const SerializationOptions& opts) const final {}
+
+ DummyShapeSpecificComponents components;
+};
+
+class UniversalShapeTest : public ServiceContextTest {};
+
+TEST_F(UniversalShapeTest, SizeOfSpecificComponents) {
+ auto innerComponents = std::make_unique<DummyShapeSpecificComponents>();
+ ASSERT_EQ(innerComponents->size(), sizeof(CmdSpecificShapeComponents));
+ ASSERT_EQ(innerComponents->size(), sizeof(void*) /*vtable ptr*/);
+}
+
+TEST_F(UniversalShapeTest, SizeOfShape) {
+ auto expCtx = make_intrusive<ExpressionContextForTest>();
+
+ // Make shape for testing.
+ auto collation = BSONObj{};
+ auto innerComponents = std::make_unique<DummyShapeSpecificComponents>();
+ auto shape = std::make_unique<DummyShape>(kDefaultTestNss, collation, *innerComponents);
+
+ ASSERT_EQ(innerComponents->size(), shape->specificComponents().size());
+ ASSERT_EQ(shape->size(),
+ sizeof(NamespaceStringOrUUID) + sizeof(BSONObj) + sizeof(void*) /*vtable ptr*/ +
+ shape->specificComponents().size() + static_cast<size_t>(collation.objsize()));
+}
+
+TEST_F(UniversalShapeTest, SizeOfShapeWithExtraSize) {
+ auto expCtx = make_intrusive<ExpressionContextForTest>();
+
+ // Make shape for testing.
+ auto collation = BSONObj{};
+ auto innerComponents = std::make_unique<DummyShapeSpecificComponents>();
+ auto shape = std::make_unique<DummyShape>(kDefaultTestNss, collation, *innerComponents);
+ auto shapeWithExtraSize =
+ std::make_unique<DummyShapeWithExtraSize>(kDefaultTestNss, collation, *innerComponents);
+
+ ASSERT_EQ(shapeWithExtraSize->size(), shape->size() + shapeWithExtraSize->extraSize());
+}
+} // namespace
+} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape/query_shape_test.idl b/src/mongo/db/query/query_shape/query_shape_test.idl
new file mode 100644
index 00000000000..06efb7ed1ef
--- /dev/null
+++ b/src/mongo/db/query/query_shape/query_shape_test.idl
@@ -0,0 +1,91 @@
+# 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.
+#
+
+global:
+ cpp_namespace: "mongo"
+
+imports:
+ - "mongo/idl/basic_types.idl"
+
+enums:
+ ExampleEnum:
+ description: ""
+ type: string
+ values:
+ Value1: "EnumValue1"
+ Value2: "EnumValue2"
+
+structs:
+ NestedStruct:
+ query_shape_component: true
+ strict: true
+ description: ""
+ fields:
+ stringField:
+ query_shape: literal
+ type: string
+ enumField:
+ query_shape: parameter
+ type: ExampleEnum
+ stringIntVariantEnum:
+ query_shape: parameter
+ type:
+ variant: [string, int]
+ arrayOfInts:
+ query_shape: literal
+ type: array<int>
+ fieldpath:
+ query_shape: anonymize
+ type: string
+ fieldpathList:
+ query_shape: anonymize
+ type: array<string>
+ nss:
+ query_shape: custom
+ type: namespacestring
+ plainNss:
+ query_shape: parameter
+ type: namespacestring
+ safeInt64Field:
+ query_shape: literal
+ type: safeInt64
+ boolField:
+ query_shape: literal
+ type: bool
+
+ ParentStruct:
+ query_shape_component: true
+ strict: true
+ description: ""
+ fields:
+ nested_shape:
+ query_shape: literal
+ type: NestedStruct
+ nested_no_shape:
+ query_shape: parameter
+ type: NestedStruct
diff --git a/src/mongo/db/query/query_shape/serialization_options.cpp b/src/mongo/db/query/query_shape/serialization_options.cpp
new file mode 100644
index 00000000000..e6008f8579b
--- /dev/null
+++ b/src/mongo/db/query/query_shape/serialization_options.cpp
@@ -0,0 +1,515 @@
+/**
+ * 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 "serialization_options.h"
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
+
+#include "mongo/db/query/query_shape/serialization_options.h"
+
+#include <boost/optional.hpp>
+#include <string>
+
+#include "mongo/base/string_data.h"
+#include "mongo/bson/timestamp.h"
+#include "mongo/db/exec/document_value/document.h"
+#include "mongo/db/exec/document_value/value.h"
+#include "mongo/db/query/explain_options.h"
+#include "mongo/logv2/log.h"
+#include "mongo/util/assert_util.h"
+
+namespace mongo {
+
+namespace {
+
+// We'll pre-declare all of these strings so that we can avoid the allocations when we reference
+// them later.
+static constexpr StringData kUndefinedTypeString = "?undefined"_sd;
+static constexpr StringData kStringTypeString = "?string"_sd;
+static constexpr StringData kNumberTypeString = "?number"_sd;
+static constexpr StringData kMinKeyTypeString = "?minKey"_sd;
+static constexpr StringData kObjectTypeString = "?object"_sd;
+static constexpr StringData kArrayTypeString = "?array"_sd;
+static constexpr StringData kBinDataTypeString = "?binData"_sd;
+static constexpr StringData kObjectIdTypeString = "?objectId"_sd;
+static constexpr StringData kBoolTypeString = "?bool"_sd;
+static constexpr StringData kDateTypeString = "?date"_sd;
+static constexpr StringData kNullTypeString = "?null"_sd;
+static constexpr StringData kRegexTypeString = "?regex"_sd;
+static constexpr StringData kDbPointerTypeString = "?dbPointer"_sd;
+static constexpr StringData kJavascriptTypeString = "?javascript"_sd;
+static constexpr StringData kJavascriptWithScopeTypeString = "?javascriptWithScope"_sd;
+static constexpr StringData kTimestampTypeString = "?timestamp"_sd;
+static constexpr StringData kMaxKeyTypeString = "?maxKey"_sd;
+
+static const StringMap<StringData> kArrayTypeStringConstants{
+ {kUndefinedTypeString.rawData(), "?array<?undefined>"_sd},
+ {kStringTypeString.rawData(), "?array<?string>"_sd},
+ {kNumberTypeString.rawData(), "?array<?number>"_sd},
+ {kMinKeyTypeString.rawData(), "?array<?minKey>"_sd},
+ {kObjectTypeString.rawData(), "?array<?object>"_sd},
+ {kArrayTypeString.rawData(), "?array<?array>"_sd},
+ {kBinDataTypeString.rawData(), "?array<?binData>"_sd},
+ {kObjectIdTypeString.rawData(), "?array<?objectId>"_sd},
+ {kBoolTypeString.rawData(), "?array<?bool>"_sd},
+ {kDateTypeString.rawData(), "?array<?date>"_sd},
+ {kNullTypeString.rawData(), "?array<?null>"_sd},
+ {kRegexTypeString.rawData(), "?array<?regex>"_sd},
+ {kDbPointerTypeString.rawData(), "?array<?dbPointer>"_sd},
+ {kJavascriptTypeString.rawData(), "?array<?javascript>"_sd},
+ {kJavascriptWithScopeTypeString.rawData(), "?array<?javascriptWithScope>"_sd},
+ {kTimestampTypeString.rawData(), "?array<?timestamp>"_sd},
+ {kMaxKeyTypeString.rawData(), "?array<?maxKey>"_sd},
+};
+
+static constexpr auto kRepresentativeString = "?"_sd;
+static constexpr auto kRepresentativeNumber = 1;
+static const auto kRepresentativeObject = BSON("?"
+ << "?");
+static const auto kRepresentativeArray = BSONArray();
+static constexpr auto kRepresentativeBinData = BSONBinData();
+static const auto kRepresentativeObjectId = OID::max();
+static constexpr auto kRepresentativeBool = true;
+static const auto kRepresentativeDate = Date_t::fromMillisSinceEpoch(0);
+static const auto kRepresentativeRegex = BSONRegEx("/\?/");
+static const auto kRepresentativeDbPointer = BSONDBRef("?.?", OID::max());
+static const auto kRepresentativeJavascript = BSONCode("return ?;");
+static const auto kRepresentativeJavascriptWithScope = BSONCodeWScope("return ?;", BSONObj());
+static const auto kRepresentativeTimestamp = Timestamp::min();
+
+/**
+ * A default redaction strategy that generates easy to check results for testing purposes.
+ */
+std::string applyHmacForTest(StringData s) {
+ // Avoid ending in a parenthesis since the results will occur in a raw string where the )"
+ // sequence will accidentally terminate the string.
+ return str::stream() << "HASH<" << s << ">";
+}
+
+/**
+ * Computes a debug string meant to represent "any value of type t", where "t" is the type of the
+ * provided argument. For example "?number" for any number (int, double, etc.).
+ */
+StringData debugTypeString(BSONType t) {
+ // This is tightly coupled with 'canonicalizeBSONType' and therefore also with
+ // sorting/comparison semantics.
+ switch (t) {
+ case EOO:
+ case Undefined:
+ return kUndefinedTypeString;
+ case Symbol:
+ case String:
+ return kStringTypeString;
+ case NumberInt:
+ case NumberLong:
+ case NumberDouble:
+ case NumberDecimal:
+ return kNumberTypeString;
+ case MinKey:
+ return kMinKeyTypeString;
+ case Object:
+ return kObjectTypeString;
+ case Array:
+ // This case should only happen if we have an array within an array.
+ return kArrayTypeString;
+ case BinData:
+ return kBinDataTypeString;
+ case jstOID:
+ return kObjectIdTypeString;
+ case Bool:
+ return kBoolTypeString;
+ case Date:
+ return kDateTypeString;
+ case jstNULL:
+ return kNullTypeString;
+ case RegEx:
+ return kRegexTypeString;
+ case DBRef:
+ return kDbPointerTypeString;
+ case Code:
+ return kJavascriptTypeString;
+ case CodeWScope:
+ return kJavascriptWithScopeTypeString;
+ case bsonTimestamp:
+ return kTimestampTypeString;
+ case MaxKey:
+ return kMaxKeyTypeString;
+ default:
+ MONGO_UNREACHABLE_TASSERT(7539806);
+ }
+}
+
+/**
+ * Returns an arbitrary value of the same type as the one given. For any number, this will be the
+ * number 1. For any boolean this will be true.
+ * TODO if you need a different value to make sure it will parse, you should not use this API.
+ */
+ImplicitValue defaultLiteralOfType(BSONType t) {
+ // This is tightly coupled with 'canonicalizeBSONType' and therefore also with
+ // sorting/comparison semantics.
+ switch (t) {
+ case EOO:
+ case Undefined:
+ return BSONUndefined;
+ case Symbol:
+ case String:
+ return kRepresentativeString;
+ case NumberInt:
+ case NumberLong:
+ case NumberDouble:
+ case NumberDecimal:
+ return kRepresentativeNumber;
+ case MinKey:
+ return MINKEY;
+ case Object:
+ return kRepresentativeObject;
+ case Array:
+ // This case should only happen if we have an array within an array.
+ return kRepresentativeArray;
+ case BinData:
+ return kRepresentativeBinData;
+ case jstOID:
+ return kRepresentativeObjectId;
+ case Bool:
+ return kRepresentativeBool;
+ case Date:
+ return kRepresentativeDate;
+ case jstNULL:
+ return BSONNULL;
+ case RegEx:
+ return kRepresentativeRegex;
+ case DBRef:
+ return kRepresentativeDbPointer;
+ case Code:
+ return kRepresentativeJavascript;
+ case CodeWScope:
+ return kRepresentativeJavascriptWithScope;
+ case bsonTimestamp:
+ return kRepresentativeTimestamp;
+ case MaxKey:
+ return MAXKEY;
+ default:
+ MONGO_UNREACHABLE_TASSERT(7539803);
+ }
+}
+
+/**
+ * A struct representing the sub-type information for an array.
+ */
+struct ArraySubtypeInfo {
+ /**
+ * Whether the values of an array are all the same BSON type or not (mixed).
+ */
+ enum class NTypes { kEmpty, kOneType, kMixed };
+ ArraySubtypeInfo(NTypes nTypes_) : nTypes(nTypes_) {}
+ ArraySubtypeInfo(BSONType oneType) : nTypes(NTypes::kOneType), singleType(oneType) {}
+
+ NTypes nTypes;
+ boost::optional<BSONType> singleType = boost::none;
+};
+
+template <typename ValueType>
+using GetTypeFn = std::function<BSONType(ValueType)>;
+
+static GetTypeFn<BSONElement> getBSONElementType = [](const BSONElement& e) { return e.type(); };
+static GetTypeFn<Value> getValueType = [](const Value& v) { return v.getType(); };
+
+/**
+ * Scans 'arrayOfValues' to see if all values are of the same type or not. Returns this info in a
+ * struct - see the struct definition for how it is represented.
+ *
+ * Templated algorithm to handle both iterators of BSONElements or iterators of Values.
+ * 'getTypeCallback' is provided to abstract away the different '.type()' vs '.getType()' APIs.
+ */
+template <typename ArrayType, typename ValueType>
+ArraySubtypeInfo determineArraySubType(const ArrayType& arrayOfValues,
+ GetTypeFn<ValueType> getTypeCallback) {
+ boost::optional<BSONType> firstType = boost::none;
+ for (auto&& v : arrayOfValues) {
+ if (!firstType) {
+ firstType.emplace(getTypeCallback(v));
+ } else if (*firstType != getTypeCallback(v)) {
+ return {ArraySubtypeInfo::NTypes::kMixed};
+ }
+ }
+ return firstType ? ArraySubtypeInfo{*firstType}
+ : ArraySubtypeInfo{ArraySubtypeInfo::NTypes::kEmpty};
+}
+
+ArraySubtypeInfo determineArraySubType(const BSONObj& arrayAsObj) {
+ return determineArraySubType<BSONObj, BSONElement>(arrayAsObj, getBSONElementType);
+}
+ArraySubtypeInfo determineArraySubType(const std::vector<Value>& values) {
+ return determineArraySubType<std::vector<Value>, Value>(values, getValueType);
+}
+
+template <typename ValueType>
+StringData debugTypeString(
+ const ValueType& v,
+ GetTypeFn<ValueType> getTypeCallback,
+ std::function<ArraySubtypeInfo(ValueType)> determineArraySubTypeCallback) {
+ if (getTypeCallback(v) == BSONType::Array) {
+ // Iterating the array as .Obj(), as if it were a BSONObj (with field names '0', '1', etc.)
+ // is faster than converting the whole thing to an array which would force a copy.
+ auto typeInfo = determineArraySubTypeCallback(v);
+ switch (typeInfo.nTypes) {
+ case ArraySubtypeInfo::NTypes::kEmpty:
+ return "[]"_sd;
+ case ArraySubtypeInfo::NTypes::kOneType:
+ return kArrayTypeStringConstants.at(debugTypeString(*typeInfo.singleType));
+ case ArraySubtypeInfo::NTypes::kMixed:
+ return "?array<>";
+ default:
+ MONGO_UNREACHABLE_TASSERT(7539801);
+ }
+ }
+ return debugTypeString(getTypeCallback(v));
+}
+
+template <typename ValueType>
+ImplicitValue defaultLiteralOfType(
+ const ValueType& v,
+ GetTypeFn<ValueType> getTypeCallback,
+ std::function<ArraySubtypeInfo(ValueType)> determineArraySubTypeCallback) {
+ if (getTypeCallback(v) == BSONType::Array) {
+ auto typeInfo = determineArraySubTypeCallback(v);
+ switch (typeInfo.nTypes) {
+ case ArraySubtypeInfo::NTypes::kEmpty:
+ return BSONArray();
+ case ArraySubtypeInfo::NTypes::kOneType:
+ return std::vector<Value>{defaultLiteralOfType(*typeInfo.singleType)};
+ case ArraySubtypeInfo::NTypes::kMixed:
+ // We don't care which types, we'll use a number and a string as the canonical
+ // mixed type array regardless. This is to ensure we don't get 2^N possibilities
+ // for mixed type scenarios - we wish to collapse all "mixed type" arrays to one
+ // canonical mix. The choice of int and string is mostly arbitrary - hopefully
+ // somewhat comprehensible at a glance.
+ return std::vector<Value>{Value(2), Value("or more types"_sd)};
+ default:
+ MONGO_UNREACHABLE_TASSERT(7539805);
+ }
+ }
+ return defaultLiteralOfType(getTypeCallback(v));
+}
+
+ArraySubtypeInfo getSubTypeFromBSONElemArray(BSONElement arrayElem) {
+ // Iterating the array as .Obj(), as if it were a BSONObj (with field names '0', '1', etc.)
+ // is faster than converting the whole thing to an array which would force a copy.
+ return determineArraySubType(arrayElem.Obj());
+}
+ArraySubtypeInfo getSubTypeFromValueArray(const Value& arrayVal) {
+ return determineArraySubType(arrayVal.getArray());
+}
+
+void appendDefaultOfNonArrayType(BSONObjBuilder* bob, StringData name, const BSONElement& e) {
+ switch (e.type()) {
+ case EOO:
+ case Undefined:
+ bob->appendUndefined(name);
+ return;
+ case Symbol:
+ case String:
+ bob->append(name, kRepresentativeString);
+ return;
+ case NumberInt:
+ case NumberLong:
+ case NumberDouble:
+ case NumberDecimal:
+ bob->append(name, kRepresentativeNumber);
+ return;
+ case MinKey:
+ bob->appendMinKey(name);
+ return;
+ case Object:
+ bob->append(name, kRepresentativeObject);
+ return;
+ case Array:
+ // This case is more complicated and callers should use a more generic helper.
+ MONGO_UNREACHABLE_TASSERT(8094100);
+ case BinData:
+ bob->append(name, kRepresentativeBinData);
+ return;
+ case jstOID:
+ bob->append(name, kRepresentativeObjectId);
+ return;
+ case Bool:
+ bob->append(name, kRepresentativeBool);
+ return;
+ case Date:
+ bob->append(name, kRepresentativeDate);
+ return;
+ case jstNULL:
+ bob->appendNull(name);
+ return;
+ case RegEx:
+ bob->append(name, kRepresentativeRegex);
+ return;
+ case DBRef:
+ bob->append(name, kRepresentativeDbPointer);
+ return;
+ case Code:
+ bob->append(name, kRepresentativeJavascript);
+ return;
+ case CodeWScope:
+ bob->append(name, kRepresentativeJavascriptWithScope);
+ return;
+ case bsonTimestamp:
+ bob->append(name, kRepresentativeTimestamp);
+ return;
+ case MaxKey:
+ bob->appendMaxKey(name);
+ return;
+ default:
+ MONGO_UNREACHABLE_TASSERT(8094101);
+ };
+}
+} // namespace
+
+const SerializationOptions SerializationOptions::kRepresentativeQueryShapeSerializeOptions =
+ SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue};
+
+const SerializationOptions SerializationOptions::kDebugQueryShapeSerializeOptions =
+ SerializationOptions{LiteralSerializationPolicy::kToDebugTypeString};
+
+SerializationOptions::SerializationOptions(LiteralSerializationPolicy policy)
+ : literalPolicy(policy) {}
+SerializationOptions::SerializationOptions(
+ boost::optional<ExplainOptions::Verbosity> explainVerbosity)
+ : verbosity(explainVerbosity) {}
+
+SerializationOptions::SerializationOptions(LiteralSerializationPolicy policy,
+ bool transformIdentifiers,
+ TokenizeIdentifierFunc transformIdentifiersCallbackFn)
+ : literalPolicy(policy),
+ transformIdentifiers(transformIdentifiers),
+ transformIdentifiersCallback(transformIdentifiersCallbackFn) {}
+
+const SerializationOptions SerializationOptions::kMarkIdentifiers_FOR_TEST{
+ LiteralSerializationPolicy::kUnchanged, true, applyHmacForTest};
+
+const SerializationOptions SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST{
+ LiteralSerializationPolicy::kToDebugTypeString, true, applyHmacForTest};
+
+// Overloads for BSONElem and Value.
+StringData debugTypeString(BSONElement e) {
+ return debugTypeString<BSONElement>(e, getBSONElementType, getSubTypeFromBSONElemArray);
+}
+StringData debugTypeString(const Value& v) {
+ return debugTypeString<Value>(v, getValueType, getSubTypeFromValueArray);
+}
+
+// Overloads for BSONElem and Value.
+ImplicitValue defaultLiteralOfType(const Value& v) {
+ return defaultLiteralOfType<Value>(v, getValueType, getSubTypeFromValueArray);
+}
+ImplicitValue defaultLiteralOfType(BSONElement e) {
+ return defaultLiteralOfType<BSONElement>(e, getBSONElementType, getSubTypeFromBSONElemArray);
+}
+
+void SerializationOptions::appendLiteral(BSONObjBuilder* bob, const BSONElement& e) const {
+ appendLiteral(bob, e.fieldNameStringData(), e);
+}
+void SerializationOptions::appendLiteral(BSONObjBuilder* bob,
+ StringData name,
+ const BSONElement& e) const {
+ // The first two cases are particularly performance sensitive. We could answer everything here
+ // with the code inside the 'kToDebugTypeString' branch, but there are some relatively easy ways
+ // to accomplish the first two policy cases (in the common cases), so we'll special case those
+ // in order to avoid constructing a temporary Value.
+ switch (literalPolicy) {
+ case LiteralSerializationPolicy::kUnchanged:
+ bob->appendAs(e, name);
+ return;
+ case LiteralSerializationPolicy::kToRepresentativeParseableValue: {
+ if (e.type() != BSONType::Array) {
+ appendDefaultOfNonArrayType(bob, name, e);
+ return;
+ }
+ // If it's an array we'll default to the slow but general codepath below.
+ [[fallthrough]];
+ }
+ case LiteralSerializationPolicy::kToDebugTypeString: {
+ // Performance isn't as sensitive here.
+ return serializeLiteral(e).addToBsonObj(bob, name);
+ }
+ default:
+ MONGO_UNREACHABLE_TASSERT(8094102);
+ }
+}
+
+void SerializationOptions::appendLiteral(BSONObjBuilder* bob,
+ StringData fieldName,
+ const ImplicitValue& v,
+ const boost::optional<Value>& representativeValue) const {
+ serializeLiteral(v, representativeValue).addToBsonObj(bob, fieldName);
+}
+
+Value SerializationOptions::serializeLiteral(
+ const BSONElement& e, const boost::optional<Value>& representativeValue) const {
+ switch (literalPolicy) {
+ case LiteralSerializationPolicy::kUnchanged:
+ return Value(e);
+ case LiteralSerializationPolicy::kToDebugTypeString:
+ return Value(debugTypeString(e));
+ case LiteralSerializationPolicy::kToRepresentativeParseableValue:
+ return representativeValue.value_or(defaultLiteralOfType(e));
+ default:
+ MONGO_UNREACHABLE_TASSERT(7539802);
+ }
+}
+
+Value SerializationOptions::serializeLiteral(
+ const ImplicitValue& v, const boost::optional<Value>& representativeValue) const {
+ switch (literalPolicy) {
+ case LiteralSerializationPolicy::kUnchanged:
+ return v;
+ case LiteralSerializationPolicy::kToDebugTypeString:
+ return Value(debugTypeString(v));
+ case LiteralSerializationPolicy::kToRepresentativeParseableValue:
+ return representativeValue.value_or(defaultLiteralOfType(v));
+ default:
+ MONGO_UNREACHABLE_TASSERT(7539804);
+ }
+}
+
+std::string SerializationOptions::serializeFieldPathFromString(StringData path) const {
+ if (transformIdentifiers) {
+ try {
+ return serializeFieldPath(FieldPath(path, false));
+ } catch (DBException& ex) {
+ LOGV2_DEBUG(7549808,
+ 1,
+ "Failed to convert a path string to a FieldPath",
+ "pathString"_attr = path,
+ "failure"_attr = ex.toStatus());
+ return serializeFieldPath("invalidFieldPathPlaceholder");
+ }
+ }
+ return path.toString();
+}
+} // namespace mongo
diff --git a/src/mongo/db/query/query_shape/serialization_options.h b/src/mongo/db/query/query_shape/serialization_options.h
new file mode 100644
index 00000000000..226da7689d3
--- /dev/null
+++ b/src/mongo/db/query/query_shape/serialization_options.h
@@ -0,0 +1,236 @@
+/**
+ * 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.
+ */
+
+#pragma once
+#include "mongo/base/string_data.h"
+#include "mongo/bson/bsonobj.h"
+#include "mongo/bson/bsonobjbuilder.h"
+#include "mongo/db/exec/document_value/document.h"
+#include "mongo/db/exec/document_value/value.h"
+#include "mongo/db/pipeline/field_path.h"
+#include "mongo/db/query/explain_options.h"
+#include "mongo/util/assert_util.h"
+#include <boost/optional.hpp>
+#include <string>
+
+namespace mongo {
+namespace {
+// Should never be called, throw to ensure we catch this in tests.
+std::string defaultHmacStrategy(StringData s) {
+ MONGO_UNREACHABLE_TASSERT(7332410);
+}
+} // namespace
+
+/**
+ * A policy enum for how to serialize literal values.
+ */
+enum class LiteralSerializationPolicy {
+ // The default way to serialize. Just serialize whatever literals were given if they are still
+ // available, or whatever you parsed them to. This is expected to be able to parse again, since
+ // it worked the first time.
+ kUnchanged,
+ // Serialize any literal value as "?number" or similar. For example "?bool" for any boolean. Use
+ // 'debugTypeString()' helper.
+ kToDebugTypeString,
+ // Serialize any literal value to one canonical value of the given type, with the constraint
+ // that the chosen representative value should be parseable in this context. There are some
+ // default implementations that will usually work (e.g. using the number 1 almost always works
+ // for numbers), but serializers should be careful to think about and test this if their parsers
+ // reject certain values.
+ kToRepresentativeParseableValue,
+};
+
+/**
+ * A struct with options for how you want to serialize a match or aggregation expression.
+ */
+struct SerializationOptions {
+ using TokenizeIdentifierFunc = std::function<std::string(StringData)>;
+
+ // The default serialization options for a query shape. No need to redact identifiers for the
+ // this purpose. We may do that on the $queryStats read path.
+ static const SerializationOptions kRepresentativeQueryShapeSerializeOptions;
+ static const SerializationOptions kDebugQueryShapeSerializeOptions;
+ static const SerializationOptions kMarkIdentifiers_FOR_TEST;
+ static const SerializationOptions kDebugShapeAndMarkIdentifiers_FOR_TEST;
+
+ SerializationOptions() = default;
+ SerializationOptions(LiteralSerializationPolicy policy);
+ SerializationOptions(boost::optional<ExplainOptions::Verbosity> explain);
+ SerializationOptions(LiteralSerializationPolicy policy,
+ bool transformIdentifiers,
+ TokenizeIdentifierFunc transformIdentifiersCallbackFn);
+
+ /**
+ * Checks if this SerializationOptions represents the same options as another
+ * SerializationOptions. Note it cannot compare whether the two 'transformIdentifiersCallback's
+ * are the same - the language purposefully leaves the comparison operator undefined.
+ */
+ bool operator==(const SerializationOptions& other) const {
+ return this->transformIdentifiers == other.transformIdentifiers &&
+ // You cannot well determine std::function equivalence in C++, so this is the best we'll
+ // do.
+ (this->transformIdentifiersCallback == nullptr) ==
+ (other.transformIdentifiersCallback == nullptr) &&
+ this->literalPolicy == other.literalPolicy && this->verbosity == other.verbosity;
+ }
+ bool operator!=(const SerializationOptions& other) const {
+ return !(*this == other);
+ }
+
+ // Helper function for removing identifiable information (like collection/db names).
+ // Note: serializeFieldPath/serializeFieldPathFromString should be used for field
+ // names.
+ std::string serializeIdentifier(StringData str) const {
+ if (transformIdentifiers) {
+ return transformIdentifiersCallback(str);
+ }
+ return str.toString();
+ }
+
+ std::string serializeFieldPath(FieldPath path) const {
+ if (transformIdentifiers) {
+ std::stringstream hmaced;
+ for (size_t i = 0; i < path.getPathLength(); ++i) {
+ if (i > 0) {
+ hmaced << ".";
+ }
+ hmaced << transformIdentifiersCallback(path.getFieldName(i));
+ }
+ return hmaced.str();
+ }
+ return path.fullPath();
+ }
+
+ std::string serializeFieldPathWithPrefix(FieldPath path) const {
+ return "$" + serializeFieldPath(path);
+ }
+
+ std::string serializeFieldPathFromString(StringData path) const;
+
+ std::vector<std::string> serializeFieldPathFromString(
+ const std::vector<std::string>& paths) const {
+ std::vector<std::string> result;
+ result.reserve(paths.size());
+ for (auto& p : paths) {
+ result.push_back(serializeFieldPathFromString(p));
+ }
+ return result;
+ }
+
+ // Helper functions for applying hmac to BSONObj. Does not take into account anything to do with
+ // MQL semantics, removes all field names and literals in the passed in obj.
+ void addHmacedArrayToBuilder(BSONArrayBuilder* bab, std::vector<BSONElement> array) const {
+ for (const auto& elem : array) {
+ if (elem.type() == BSONType::Object) {
+ BSONObjBuilder subObj(bab->subobjStart());
+ addHmacedObjToBuilder(&subObj, elem.Obj());
+ subObj.done();
+ } else if (elem.type() == BSONType::Array) {
+ BSONArrayBuilder subArr(bab->subarrayStart());
+ addHmacedArrayToBuilder(&subArr, elem.Array());
+ subArr.done();
+ } else {
+ *bab << serializeLiteral(elem);
+ }
+ }
+ }
+
+ void addHmacedObjToBuilder(BSONObjBuilder* bob, BSONObj objToHmac) const {
+ for (const auto& elem : objToHmac) {
+ auto fieldName = serializeFieldPath(elem.fieldName());
+ if (elem.type() == BSONType::Object) {
+ BSONObjBuilder subObj(bob->subobjStart(fieldName));
+ addHmacedObjToBuilder(&subObj, elem.Obj());
+ subObj.done();
+ } else if (elem.type() == BSONType::Array) {
+ BSONArrayBuilder subArr(bob->subarrayStart(fieldName));
+ addHmacedArrayToBuilder(&subArr, elem.Array());
+ subArr.done();
+ } else {
+ appendLiteral(bob, fieldName, elem);
+ }
+ }
+ }
+
+ /**
+ * Helper method to call 'serializeLiteral()' on 'e' and append the resulting value to 'bob'
+ * using the same name as 'e'.
+ */
+ void appendLiteral(BSONObjBuilder* bob, const BSONElement& e) const;
+ void appendLiteral(BSONObjBuilder* bob, StringData name, const BSONElement& e) const;
+ /**
+ * Helper method to call 'serializeLiteral()' on 'v' and append the result to 'bob' using field
+ * name 'fieldName'.
+ */
+ void appendLiteral(BSONObjBuilder* bob,
+ StringData fieldName,
+ const ImplicitValue& v,
+ const boost::optional<Value>& representativeValue = boost::none) const;
+
+ /**
+ * Depending on the configured 'literalPolicy', serializeLiteral will return the appropriate
+ * value for adding literals to serialization output:
+ * - If 'literalPolicy' is 'kUnchanged', returns the input value unmodified.
+ * - If it is 'kToDebugTypeString', computes and returns the type string as a string Value.
+ * - If it is 'kToRepresentativeValue', it returns an arbitrary value of the same type as the
+ * one given. For any number, this will be the number 1. For any boolean this will be true.
+ * If the 'representativeValue' parameter if it is not none, returns it (regardless of type).
+ *
+ * Example usage: BSON("myArg" << options.serializeLiteral(_myArg));
+ */
+ Value serializeLiteral(const BSONElement& e,
+ const boost::optional<Value>& representativeValue = boost::none) const;
+ Value serializeLiteral(const ImplicitValue& v,
+ const boost::optional<Value>& representativeValue = boost::none) const;
+
+ // 'literalPolicy' is an independent option to serialize in a general format with the aim of
+ // similar "shaped" queries serializing to the same object. For example, if set to
+ // 'kToDebugTypeString', then the serialization of {a: {$gt: 2}} should result in {a: {$gt:
+ // '?number'}}, as will the serialization of {a: {$gt: 3}}.
+ //
+ // "Literal" here is meant to stand in contrast to expression arguments, as in the $gt
+ // expressions in {$and: [{a: {$gt: 3}}, {b: {$gt: 4}}]}. There the only literals are 3 and 4,
+ // so the serialization expected for 'kToDebugTypeString' would be {$and: [{a: {$gt:
+ // '?number'}}, {b: {$lt: '?number'}}]}.
+ LiteralSerializationPolicy literalPolicy = LiteralSerializationPolicy::kUnchanged;
+
+ // If true the caller must set transformIdentifiersCallback. 'transformIdentifiers' if set along
+ // with a strategy the redaction strategy will be called on any personal identifiable
+ // information (e.g., field paths/names, collection names) encountered before serializing them.
+ bool transformIdentifiers = false;
+ std::function<std::string(StringData)> transformIdentifiersCallback = defaultHmacStrategy;
+
+ // For aggregation indicate whether we should use the more verbose serialization format.
+ boost::optional<ExplainOptions::Verbosity> verbosity = boost::none;
+
+ // If set to true, serializes each stage and expression as needed for query analysis.
+ bool serializeForQueryAnalysis = false;
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/query/query_shape/shape_helpers.cpp b/src/mongo/db/query/query_shape/shape_helpers.cpp
new file mode 100644
index 00000000000..8eea475ab78
--- /dev/null
+++ b/src/mongo/db/query/query_shape/shape_helpers.cpp
@@ -0,0 +1,108 @@
+/**
+ * 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/query/query_shape/shape_helpers.h"
+
+#include "mongo/db/query/query_shape/query_shape_gen.h"
+
+namespace mongo::shape_helpers {
+
+static constexpr StringData hintSpecialField = "$hint"_sd;
+// A "Flat" object is one with only top-level fields. We won't descend recursively to shapify any
+// sub-objects.
+BSONObj shapifyFlatObj(BSONObj obj, const SerializationOptions& opts, bool valuesAreLiterals) {
+ if (obj.isEmpty()) {
+ // fast-path for the common case.
+ return obj;
+ }
+
+ BSONObjBuilder bob;
+ for (BSONElement elem : obj) {
+ if (hintSpecialField.compare(elem.fieldNameStringData()) == 0) {
+ if (elem.type() == BSONType::String) {
+ bob.append(hintSpecialField, opts.serializeFieldPathFromString(elem.String()));
+ } else if (elem.type() == BSONType::Object) {
+ opts.appendLiteral(&bob, hintSpecialField, elem.Obj());
+ } else {
+ // SERVER-85500: $hint syntax will not be validated if the collection does not
+ // exist, so we should accept a value that is neither string nor object here.
+ opts.appendLiteral(&bob, hintSpecialField, elem);
+ }
+ continue;
+ }
+
+ // $natural doesn't need to be redacted.
+ if (elem.fieldNameStringData().compare(query_request_helper::kNaturalSortField) == 0) {
+ bob.append(elem);
+ continue;
+ }
+
+ if (valuesAreLiterals) {
+ opts.appendLiteral(&bob, opts.serializeFieldPathFromString(elem.fieldName()), elem);
+ } else {
+ bob.appendAs(elem, opts.serializeFieldPathFromString(elem.fieldName()));
+ }
+ }
+ return bob.obj();
+}
+
+BSONObj extractHintShape(BSONObj hintObj, const SerializationOptions& opts) {
+ return shapifyFlatObj(hintObj, opts, /* valuesAreLiterals = */ false);
+}
+
+BSONObj extractMinOrMaxShape(BSONObj obj, const SerializationOptions& opts) {
+ return shapifyFlatObj(obj, opts, /* valuesAreLiterals = */ true);
+}
+
+void appendNamespaceShape(BSONObjBuilder& bob,
+ const NamespaceString& nss,
+ const SerializationOptions& opts) {
+ bob.append("db", opts.serializeIdentifier(nss.db()));
+ bob.append("coll", opts.serializeIdentifier(nss.coll()));
+}
+
+NamespaceStringOrUUID parseNamespaceShape(BSONElement cmdNsElt) {
+ tassert(7632900, "cmdNs must be an object.", cmdNsElt.type() == BSONType::Object);
+ auto cmdNs = query_shape::CommandNamespace::parse("cmdNs"_sd, cmdNsElt.embeddedObject());
+
+ if (cmdNs.getColl().has_value()) {
+ tassert(7632903,
+ "Exactly one of 'uuid' and 'coll' can be defined.",
+ !cmdNs.getUuid().has_value());
+ return NamespaceString(cmdNs.getDb(), cmdNs.getColl().value());
+ } else {
+ tassert(7632904,
+ "Exactly one of 'uuid' and 'coll' can be defined.",
+ !cmdNs.getColl().has_value());
+ UUID uuid = uassertStatusOK(UUID::parse(cmdNs.getUuid().value().toString()));
+ return NamespaceStringOrUUID(cmdNs.getDb().toString(), uuid);
+ }
+}
+
+} // namespace mongo::shape_helpers
diff --git a/src/mongo/db/query/query_shape/shape_helpers.h b/src/mongo/db/query/query_shape/shape_helpers.h
new file mode 100644
index 00000000000..4d0fadb4a47
--- /dev/null
+++ b/src/mongo/db/query/query_shape/shape_helpers.h
@@ -0,0 +1,101 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include "mongo/bson/simple_bsonobj_comparator.h"
+#include "mongo/db/query/query_shape/query_shape.h"
+#include "mongo/db/query/query_shape/serialization_options.h"
+
+namespace mongo::shape_helpers {
+
+int64_t inline optionalObjSize(boost::optional<BSONObj> optionalObj) {
+ if (!optionalObj)
+ return 0;
+ return optionalObj->objsize();
+}
+
+template <typename T>
+int64_t optionalSize(boost::optional<T> optionalVal) {
+ if (!optionalVal)
+ return 0;
+ return optionalVal->size();
+}
+
+template <typename T>
+std::function<size_t(size_t, const T&)> sizeAccumulatorFunc() {
+ MONGO_UNREACHABLE; // Don't know how to compute the size of this template type.
+};
+
+template <>
+inline std::function<size_t(size_t, const BSONObj&)> sizeAccumulatorFunc<BSONObj>() {
+ return [](size_t total, const BSONObj& obj) {
+ return total + sizeof(BSONObj) + static_cast<size_t>(obj.objsize());
+ };
+}
+
+template <>
+inline std::function<size_t(size_t, const NamespaceString&)>
+sizeAccumulatorFunc<NamespaceString>() {
+ return [](size_t total, const NamespaceString& nss) {
+ // For each element, we have to track the size of the
+ // nss as well as the size allocated by the nss. It would be
+ // ideal to be able to ask the underlying namespace string for
+ // its capacity, but it's not something we have access to.
+ // Further, namespace strings appear to shrink to fit (i.e
+ // resize to correct size), so it may not be necessary. Should
+ // we also try to consider short string optimization? At the
+ // very least, the current approach gives us a good upper bound
+ // memory usage (assuming shrink to fit).
+ return total + sizeof(nss) + nss.size();
+ };
+}
+
+template <typename Container>
+size_t containerSize(const Container& container) {
+ return std::accumulate(container.begin(),
+ container.end(),
+ 0,
+ sizeAccumulatorFunc<typename Container::value_type>());
+}
+
+/**
+ * Serializes the given 'hintObj' in accordance with the options. Assumes the hint is correct and
+ * contains field names. It is possible that this hint doesn't actually represent an index, but we
+ * can't detect that here.
+ */
+BSONObj extractHintShape(BSONObj hintObj, const SerializationOptions& opts);
+BSONObj extractMinOrMaxShape(BSONObj obj, const SerializationOptions& opts);
+
+NamespaceStringOrUUID parseNamespaceShape(BSONElement cmdNsElt);
+void appendNamespaceShape(BSONObjBuilder& bob,
+ const NamespaceString& nss,
+ const SerializationOptions& opts);
+
+} // namespace mongo::shape_helpers