summaryrefslogtreecommitdiff
path: root/src/mongo
diff options
context:
space:
mode:
authorBernard Gorman <bernard.gorman@gmail.com>2018-06-18 17:44:58 +0100
committerBernard Gorman <bernard.gorman@gmail.com>2018-07-13 12:23:15 +0100
commit576d8360407738e0e4f7abf23b8335116f4ba125 (patch)
treee5937a553f19241aff8b38fb5c2d828449e42966 /src/mongo
parentacdbc57731fa3176b0467425ece92e76a8d82958 (diff)
downloadmongo-576d8360407738e0e4f7abf23b8335116f4ba125.tar.gz
SERVER-35325 Implement key generation for "allPaths" indexes
Diffstat (limited to 'src/mongo')
-rw-r--r--src/mongo/db/exec/projection_exec_agg.cpp35
-rw-r--r--src/mongo/db/exec/projection_exec_agg.h22
-rw-r--r--src/mongo/db/exec/projection_exec_agg_test.cpp106
-rw-r--r--src/mongo/db/index/SConscript3
-rw-r--r--src/mongo/db/index/all_paths_access_method.cpp16
-rw-r--r--src/mongo/db/index/all_paths_access_method.h8
-rw-r--r--src/mongo/db/index/all_paths_key_generator.cpp167
-rw-r--r--src/mongo/db/index/all_paths_key_generator.h91
-rw-r--r--src/mongo/db/index/all_paths_key_generator_test.cpp906
-rw-r--r--src/mongo/db/index/index_descriptor.cpp1
-rw-r--r--src/mongo/db/index/index_descriptor.h62
-rw-r--r--src/mongo/db/pipeline/document_source_project.cpp8
-rw-r--r--src/mongo/db/pipeline/parsed_add_fields.h9
-rw-r--r--src/mongo/db/pipeline/parsed_aggregation_projection.cpp39
-rw-r--r--src/mongo/db/pipeline/parsed_aggregation_projection.h27
-rw-r--r--src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp543
-rw-r--r--src/mongo/db/pipeline/parsed_exclusion_projection.cpp40
-rw-r--r--src/mongo/db/pipeline/parsed_exclusion_projection.h14
-rw-r--r--src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp274
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection.cpp30
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection.h14
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp404
22 files changed, 2307 insertions, 512 deletions
diff --git a/src/mongo/db/exec/projection_exec_agg.cpp b/src/mongo/db/exec/projection_exec_agg.cpp
index 30aa62969b4..54b5132f118 100644
--- a/src/mongo/db/exec/projection_exec_agg.cpp
+++ b/src/mongo/db/exec/projection_exec_agg.cpp
@@ -42,13 +42,35 @@ public:
using ProjectionParseMode = ParsedAggregationProjection::ProjectionParseMode;
using TransformerType = TransformerInterface::TransformerType;
- ProjectionExecutor(BSONObj projSpec) {
+ ProjectionExecutor(BSONObj projSpec,
+ DefaultIdPolicy defaultIdPolicy,
+ ArrayRecursionPolicy arrayRecursionPolicy) {
// Construct a dummy ExpressionContext for ParsedAggregationProjection. It's OK to set the
// ExpressionContext's OperationContext and CollatorInterface to 'nullptr' here; since we
// ban computed fields from the projection, the ExpressionContext will never be used.
boost::intrusive_ptr<ExpressionContext> expCtx(new ExpressionContext(nullptr, nullptr));
+
+ // Default projection behaviour is to include _id if the projection spec omits it. If the
+ // caller has specified that we should *exclude* _id by default, do so here. We translate
+ // DefaultIdPolicy to ParsedAggregationProjection::ProjectionDefaultIdPolicy in order to
+ // avoid exposing internal aggregation types to the query system.
+ ParsedAggregationProjection::ProjectionDefaultIdPolicy idPolicy =
+ (defaultIdPolicy == ProjectionExecAgg::DefaultIdPolicy::kIncludeId
+ ? ParsedAggregationProjection::ProjectionDefaultIdPolicy::kIncludeId
+ : ParsedAggregationProjection::ProjectionDefaultIdPolicy::kExcludeId);
+
+ // By default, $project will recurse through nested arrays. If the caller has specified that
+ // it should not, we inhibit it from doing so here. We separate this class' internal enum
+ // ArrayRecursionPolicy from ParsedAggregationProjection::ProjectionArrayRecursionPolicy
+ // in order to avoid exposing aggregation types to the query system.
+ ParsedAggregationProjection::ProjectionArrayRecursionPolicy recursionPolicy =
+ (arrayRecursionPolicy == ArrayRecursionPolicy::kRecurseNestedArrays
+ ? ParsedAggregationProjection::ProjectionArrayRecursionPolicy::kRecurseNestedArrays
+ : ParsedAggregationProjection::ProjectionArrayRecursionPolicy::
+ kDoNotRecurseNestedArrays);
+
_projection = ParsedAggregationProjection::create(
- expCtx, projSpec, ProjectionParseMode::kBanComputedFields);
+ expCtx, projSpec, idPolicy, recursionPolicy, ProjectionParseMode::kBanComputedFields);
}
ProjectionType getType() const {
@@ -73,9 +95,12 @@ ProjectionExecAgg::ProjectionExecAgg(BSONObj projSpec, std::unique_ptr<Projectio
ProjectionExecAgg::~ProjectionExecAgg() = default;
-std::unique_ptr<ProjectionExecAgg> ProjectionExecAgg::create(BSONObj projSpec) {
- return std::unique_ptr<ProjectionExecAgg>(
- new ProjectionExecAgg(projSpec, std::make_unique<ProjectionExecutor>(projSpec)));
+std::unique_ptr<ProjectionExecAgg> ProjectionExecAgg::create(BSONObj projSpec,
+ DefaultIdPolicy defaultIdPolicy,
+ ArrayRecursionPolicy recursionPolicy) {
+ return std::unique_ptr<ProjectionExecAgg>(new ProjectionExecAgg(
+ projSpec,
+ std::make_unique<ProjectionExecutor>(projSpec, defaultIdPolicy, recursionPolicy)));
}
ProjectionExecAgg::ProjectionType ProjectionExecAgg::getType() const {
diff --git a/src/mongo/db/exec/projection_exec_agg.h b/src/mongo/db/exec/projection_exec_agg.h
index a71f611beda..b885ca6808c 100644
--- a/src/mongo/db/exec/projection_exec_agg.h
+++ b/src/mongo/db/exec/projection_exec_agg.h
@@ -40,9 +40,25 @@ namespace mongo {
*/
class ProjectionExecAgg {
public:
+ // Allows the caller to specify how the projection should handle nested arrays; that is, an
+ // array whose immediate parent is itself an array. For example, in the case of sample document
+ // {a: [1, 2, [3, 4], {b: [5, 6]}]} the array [3, 4] is a nested array. The array [5, 6] is not,
+ // because there is an intervening object between it and its closest array ancestor.
+ enum class ArrayRecursionPolicy { kRecurseNestedArrays, kDoNotRecurseNestedArrays };
+
+ // Allows the caller to indicate whether the projection should default to including or excluding
+ // the _id field in the event that the projection spec does not specify the desired behavior.
+ // For instance, given a projection {a: 1}, specifying 'kExcludeId' is equivalent to projecting
+ // {a: 1, _id: 0} while 'kIncludeId' is equivalent to the projection {a: 1, _id: 1}. If the user
+ // explicitly specifies a projection on _id, then this will override the default policy; for
+ // instance, {a: 1, _id: 0} will exclude _id for both 'kExcludeId' and 'kIncludeId'.
+ enum class DefaultIdPolicy { kIncludeId, kExcludeId };
+
enum class ProjectionType { kInclusionProjection, kExclusionProjection };
- static std::unique_ptr<ProjectionExecAgg> create(BSONObj projSpec);
+ static std::unique_ptr<ProjectionExecAgg> create(BSONObj projSpec,
+ DefaultIdPolicy defaultIdPolicy,
+ ArrayRecursionPolicy recursionPolicy);
~ProjectionExecAgg();
@@ -52,6 +68,10 @@ public:
return _projSpec;
}
+ const BSONObj& getSpec() const {
+ return _projSpec;
+ }
+
BSONObj applyProjection(BSONObj inputDoc) const;
private:
diff --git a/src/mongo/db/exec/projection_exec_agg_test.cpp b/src/mongo/db/exec/projection_exec_agg_test.cpp
index a8dcde138d3..8503e7a50f8 100644
--- a/src/mongo/db/exec/projection_exec_agg_test.cpp
+++ b/src/mongo/db/exec/projection_exec_agg_test.cpp
@@ -38,69 +38,88 @@
namespace mongo {
namespace {
+using ArrayRecursionPolicy = ProjectionExecAgg::ArrayRecursionPolicy;
+using DefaultIdPolicy = ProjectionExecAgg::DefaultIdPolicy;
+
template <typename T>
BSONObj wrapInLiteral(const T& arg) {
return BSON("$literal" << arg);
}
+// Helper to simplify the creation of a ProjectionExecAgg which includes _id and recurses nested
+// arrays by default.
+std::unique_ptr<ProjectionExecAgg> makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSONObj projSpec) {
+ return ProjectionExecAgg::create(
+ projSpec, DefaultIdPolicy::kIncludeId, ArrayRecursionPolicy::kRecurseNestedArrays);
+}
+
//
// Error cases.
//
TEST(ProjectionExecAggErrors, ShouldRejectMixOfInclusionAndComputedFields) {
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a" << true << "b" << wrapInLiteral(1))),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << true << "b" << wrapInLiteral(1))),
AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a" << wrapInLiteral(1) << "b" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << wrapInLiteral(1) << "b" << true)),
AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a.b" << true << "a.c" << wrapInLiteral(1))),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a.b" << true << "a.c" << wrapInLiteral(1))),
AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a.b" << wrapInLiteral(1) << "a.c" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a.b" << wrapInLiteral(1) << "a.c" << true)),
AssertionException);
- ASSERT_THROWS(
- ProjectionExecAgg::create(BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))),
+ AssertionException);
- ASSERT_THROWS(
- ProjectionExecAgg::create(BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << true))),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << true))),
+ AssertionException);
}
TEST(ProjectionExecAggErrors, ShouldRejectMixOfExclusionAndComputedFields) {
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a" << false << "b" << wrapInLiteral(1))),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << false << "b" << wrapInLiteral(1))),
AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a" << wrapInLiteral(1) << "b" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << wrapInLiteral(1) << "b" << false)),
AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a.b" << false << "a.c" << wrapInLiteral(1))),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a.b" << false << "a.c" << wrapInLiteral(1))),
AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(BSON("a.b" << wrapInLiteral(1) << "a.c" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a.b" << wrapInLiteral(1) << "a.c" << false)),
AssertionException);
- ASSERT_THROWS(
- ProjectionExecAgg::create(BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))),
+ AssertionException);
- ASSERT_THROWS(
- ProjectionExecAgg::create(BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))),
+ AssertionException);
}
TEST(ProjectionExecAggErrors, ShouldRejectOnlyComputedFields) {
- ASSERT_THROWS(
- ProjectionExecAgg::create(BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(1))),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(1))),
+ AssertionException);
- ASSERT_THROWS(
- ProjectionExecAgg::create(BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(1))),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(1))),
+ AssertionException);
- ASSERT_THROWS(ProjectionExecAgg::create(
+ ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(1)))),
AssertionException);
}
@@ -108,39 +127,50 @@ TEST(ProjectionExecAggErrors, ShouldRejectOnlyComputedFields) {
// Valid projections.
TEST(ProjectionExecAggType, ShouldAcceptInclusionProjection) {
- auto parsedProject = ProjectionExecAgg::create(BSON("a" << true));
+ auto parsedProject =
+ makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("a" << true));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id" << false << "a" << true));
+ parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("_id" << false << "a" << true));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id" << false << "a.b.c" << true));
+ parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("_id" << false << "a.b.c" << true));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id.x" << true));
+ parsedProject =
+ makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("_id.x" << true));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id" << BSON("x" << true)));
+ parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("_id" << BSON("x" << true)));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("x" << BSON("_id" << true)));
+ parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("x" << BSON("_id" << true)));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection);
}
TEST(ProjectionExecAggType, ShouldAcceptExclusionProjection) {
- auto parsedProject = ProjectionExecAgg::create(BSON("a" << false));
+ auto parsedProject =
+ makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("a" << false));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id.x" << false));
+ parsedProject =
+ makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("_id.x" << false));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id" << BSON("x" << false)));
+ parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("_id" << BSON("x" << false)));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("x" << BSON("_id" << false)));
+ parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(
+ BSON("x" << BSON("_id" << false)));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection);
- parsedProject = ProjectionExecAgg::create(BSON("_id" << false));
+ parsedProject =
+ makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("_id" << false));
ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection);
}
diff --git a/src/mongo/db/index/SConscript b/src/mongo/db/index/SConscript
index 3ec4e5a7d90..5fbf8868460 100644
--- a/src/mongo/db/index/SConscript
+++ b/src/mongo/db/index/SConscript
@@ -19,6 +19,7 @@ env.Library(
env.Library(
target='key_generator',
source=[
+ 'all_paths_key_generator.cpp',
'btree_key_generator.cpp',
'expression_keys_private.cpp',
'sort_key_generator.cpp',
@@ -30,6 +31,7 @@ env.Library(
'$BUILD_DIR/mongo/db/geo/geoparser',
'$BUILD_DIR/mongo/db/index_names',
'$BUILD_DIR/mongo/db/mongohasher',
+ '$BUILD_DIR/mongo/db/projection_exec_agg',
'$BUILD_DIR/mongo/db/query/collation/collator_interface',
'$BUILD_DIR/third_party/s2/s2',
'expression_params',
@@ -57,6 +59,7 @@ env.Library(
env.CppUnitTest(
target='key_generator_test',
source=[
+ 'all_paths_key_generator_test.cpp',
'2d_key_generator_test.cpp',
'btree_key_generator_test.cpp',
'hash_key_generator_test.cpp',
diff --git a/src/mongo/db/index/all_paths_access_method.cpp b/src/mongo/db/index/all_paths_access_method.cpp
index d2d143ebfff..46a3d432e4e 100644
--- a/src/mongo/db/index/all_paths_access_method.cpp
+++ b/src/mongo/db/index/all_paths_access_method.cpp
@@ -26,24 +26,26 @@
* it in the license file.
*/
+#include "mongo/platform/basic.h"
+
#include "mongo/db/index/all_paths_access_method.h"
#include "mongo/db/catalog/index_catalog_entry.h"
-#include "mongo/db/jsobj.h"
namespace mongo {
-// Standard AllPaths implementation below.
AllPathsAccessMethod::AllPathsAccessMethod(IndexCatalogEntry* allPathsState,
SortedDataInterface* btree)
- : IndexAccessMethod(allPathsState, btree) {
- // TODO: SERVER-35325: Implement AllPathsAcessMethod.
-}
+ : IndexAccessMethod(allPathsState, btree),
+ _keyGen(
+ _descriptor->keyPattern(), _descriptor->pathProjection(), _btreeState->getCollator()) {}
void AllPathsAccessMethod::doGetKeys(const BSONObj& obj,
BSONObjSet* keys,
MultikeyPaths* multikeyPaths) const {
- // TODO: SERVER-35325: Implement AllPathsAcessMethod.
+ // TODO SERVER-35748: Until MultikeyPaths has been updated to facilitate 'allPaths' indexes, we
+ // use AllPathsKeyGenerator::MultikeyPathsMock to separate multikey paths from RecordId keys.
+ auto multikeyPathsMock = SimpleBSONObjComparator::kInstance.makeBSONObjSet();
+ _keyGen.generateKeys(obj, keys, &multikeyPathsMock);
}
-
} // namespace mongo
diff --git a/src/mongo/db/index/all_paths_access_method.h b/src/mongo/db/index/all_paths_access_method.h
index f51c173ffac..926d8ed1166 100644
--- a/src/mongo/db/index/all_paths_access_method.h
+++ b/src/mongo/db/index/all_paths_access_method.h
@@ -28,14 +28,15 @@
#pragma once
+#include "mongo/db/index/all_paths_key_generator.h"
#include "mongo/db/index/index_access_method.h"
#include "mongo/db/jsobj.h"
namespace mongo {
/**
- * The IndexAccessMethod for an AllPaths index.
- * Any index created with {"field.$**": 1} or {"$**": 1} uses this.
+ * Class which is responsible for generating and providing access to AllPaths index keys. Any index
+ * created with { "$**": ±1 } or { "path.$**": ±1 } uses this class.
*/
class AllPathsAccessMethod : public IndexAccessMethod {
public:
@@ -43,6 +44,7 @@ public:
private:
void doGetKeys(const BSONObj& obj, BSONObjSet* keys, MultikeyPaths* multikeyPaths) const final;
-};
+ const AllPathsKeyGenerator _keyGen;
+};
} // namespace mongo
diff --git a/src/mongo/db/index/all_paths_key_generator.cpp b/src/mongo/db/index/all_paths_key_generator.cpp
new file mode 100644
index 00000000000..c894188436c
--- /dev/null
+++ b/src/mongo/db/index/all_paths_key_generator.cpp
@@ -0,0 +1,167 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * 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 GNU Affero General 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/platform/basic.h"
+
+#include "mongo/db/index/all_paths_key_generator.h"
+
+#include "mongo/db/jsobj.h"
+#include "mongo/db/query/collation/collation_index_key.h"
+
+namespace mongo {
+namespace {
+
+// If the user does not specify any projection, then we default to a projection of {_id: 0} in order
+// to prevent the _id field from being indexed, since it already has its own dedicated index.
+static const BSONObj kDefaultProjection = BSON("_id"_sd << 0);
+
+// If the enclosing object is an array, then the current element's fieldname is the array index, so
+// we omit this when computing the full path. Otherwise, the full path is the pathPrefix plus the
+// element's fieldname.
+void pushPathComponent(BSONElement elem, bool enclosingObjIsArray, FieldRef* pathPrefix) {
+ if (!enclosingObjIsArray) {
+ pathPrefix->appendPart(elem.fieldNameStringData());
+ }
+}
+
+// If the enclosing object is not an array, then the final path component should be its field name.
+// Verify that this is the case and then pop it off the FieldRef.
+void popPathComponent(BSONElement elem, bool enclosingObjIsArray, FieldRef* pathToElem) {
+ if (!enclosingObjIsArray) {
+ invariant(pathToElem->getPart(pathToElem->numParts() - 1) == elem.fieldNameStringData());
+ pathToElem->removeLastPart();
+ }
+}
+} // namespace
+
+constexpr StringData AllPathsKeyGenerator::kSubtreeSuffix;
+
+AllPathsKeyGenerator::AllPathsKeyGenerator(BSONObj keyPattern,
+ BSONObj pathProjection,
+ const CollatorInterface* collator)
+ : _collator(collator), _keyPattern(keyPattern) {
+ // We should never have a key pattern that contains more than a single element.
+ invariant(_keyPattern.nFields() == 1);
+
+ // The _keyPattern is either { "$**": ±1 } for all paths or { "path.$**": ±1 } for a single
+ // subtree. If we are indexing a single subtree, then we will project just that path.
+ auto indexRoot = _keyPattern.firstElement().fieldNameStringData();
+ auto suffixPos = indexRoot.find(kSubtreeSuffix);
+
+ // If we're indexing a single subtree, we can't also specify a path projection.
+ invariant(suffixPos == std::string::npos || pathProjection.isEmpty());
+
+ // If this is a subtree projection, the projection spec is { "path.to.subtree": 1 }. Otherwise,
+ // we use the path projection from the original command object. If the path projection is empty
+ // we default to {_id: 0}, since empty projections are illegal and will be rejected when parsed.
+ auto projSpec = (suffixPos != std::string::npos
+ ? BSON(indexRoot.substr(0, suffixPos) << 1)
+ : pathProjection.isEmpty() ? kDefaultProjection : pathProjection);
+
+ // If the projection spec does not explicitly specify _id, we exclude it by default. We also
+ // prevent the projection from recursing through nested arrays, in order to ensure that the
+ // output document aligns with the match system's expectations.
+ _projExec = ProjectionExecAgg::create(
+ projSpec,
+ ProjectionExecAgg::DefaultIdPolicy::kExcludeId,
+ ProjectionExecAgg::ArrayRecursionPolicy::kDoNotRecurseNestedArrays);
+}
+
+void AllPathsKeyGenerator::generateKeys(BSONObj inputDoc,
+ BSONObjSet* keys,
+ MultikeyPathsMock* multikeyPaths) const {
+ FieldRef workingPath;
+ _traverseAllPaths(
+ _projExec->applyProjection(inputDoc), false, &workingPath, keys, multikeyPaths);
+}
+
+void AllPathsKeyGenerator::_traverseAllPaths(BSONObj obj,
+ bool objIsArray,
+ FieldRef* path,
+ BSONObjSet* keys,
+ MultikeyPathsMock* multikeyPaths) const {
+ for (const auto elem : obj) {
+ // If this element is an empty object, fast-path skip it.
+ if (elem.type() == BSONType::Object && elem.Obj().isEmpty())
+ continue;
+
+ // Append the element's fieldname to the path, if the enclosing object is not an array.
+ pushPathComponent(elem, objIsArray, path);
+
+ switch (elem.type()) {
+ case BSONType::Array:
+ // If this is a nested array, we don't descend it but instead index it as a value.
+ if (_addKeyForNestedArray(elem, *path, objIsArray, keys))
+ break;
+
+ // Add an entry for the multi-key path, and then fall through to BSONType::Object.
+ _addMultiKey(*path, multikeyPaths);
+
+ case BSONType::Object:
+ _traverseAllPaths(
+ elem.Obj(), elem.type() == BSONType::Array, path, keys, multikeyPaths);
+ break;
+
+ default:
+ _addKey(elem, *path, keys);
+ }
+
+ // Remove the element's fieldname from the path, if it was pushed onto it earlier.
+ popPathComponent(elem, objIsArray, path);
+ }
+}
+
+bool AllPathsKeyGenerator::_addKeyForNestedArray(BSONElement elem,
+ const FieldRef& fullPath,
+ bool enclosingObjIsArray,
+ BSONObjSet* keys) const {
+ // If this element is an array whose parent is also an array, index it as a value.
+ if (enclosingObjIsArray && elem.type() == BSONType::Array) {
+ _addKey(elem, fullPath, keys);
+ return true;
+ }
+ return false;
+}
+
+void AllPathsKeyGenerator::_addKey(BSONElement elem,
+ const FieldRef& fullPath,
+ BSONObjSet* keys) const {
+ // AllPaths keys are of the form { "": "path.to.field", "": <collation-aware value> }
+ BSONObjBuilder bob;
+ bob.append("", fullPath.dottedField());
+ CollationIndexKey::collationAwareIndexKeyAppend(elem, _collator, &bob);
+ keys->insert(bob.obj());
+}
+
+void AllPathsKeyGenerator::_addMultiKey(const FieldRef& fullPath,
+ MultikeyPathsMock* multikeyPaths) const {
+ // Multikey paths are denoted by an entry of the form { "": 1, "": "path.to.array" }.
+ multikeyPaths->insert(BSON("" << 1 << "" << fullPath.dottedField()));
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/index/all_paths_key_generator.h b/src/mongo/db/index/all_paths_key_generator.h
new file mode 100644
index 00000000000..278a2e81d1a
--- /dev/null
+++ b/src/mongo/db/index/all_paths_key_generator.h
@@ -0,0 +1,91 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * 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 GNU Affero General 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/exec/projection_exec_agg.h"
+#include "mongo/db/field_ref.h"
+#include "mongo/db/query/collation/collator_interface.h"
+
+namespace mongo {
+
+/**
+ * This class is responsible for generating an aggregation projection based on the keyPattern and
+ * pathProjection specs, and for subsequently extracting the set of all path-value pairs for each
+ * document.
+ */
+class AllPathsKeyGenerator {
+public:
+ static constexpr StringData kSubtreeSuffix = ".$**"_sd;
+
+ /**
+ * TODO SERVER-35748: Currently, the MultikeyPaths structure used by IndexAccessMethod is not
+ * suitable for tracking multikey paths in AllPaths indexes. In order to keep multikey paths
+ * separate from RecordId keys, and to ensure that both this key generator and the
+ * AllPathsIndexAccessMethod can be trivially switched over to using the new MultikeyPaths
+ * tracker once it is implemented, we use a mock MultikeyPaths here.
+ */
+ using MultikeyPathsMock = BSONObjSet;
+
+ AllPathsKeyGenerator(BSONObj keyPattern,
+ BSONObj pathProjection,
+ const CollatorInterface* collator);
+
+ /**
+ * Applies the appropriate AllPaths projection to the input doc, and then adds one key-value
+ * pair to the BSONObjSet 'keys' for each leaf node in the post-projection document:
+ * { '': 'path.to.field', '': <collation-aware-field-value> }
+ * Also adds one entry to 'multikeyPaths' for each array encountered in the post-projection
+ * document, in the following format:
+ * { '': 1, '': 'path.to.array' }
+ */
+ void generateKeys(BSONObj inputDoc, BSONObjSet* keys, MultikeyPathsMock* multikeyPaths) const;
+
+private:
+ // Traverses every path of the post-projection document, adding keys to the set as it goes.
+ void _traverseAllPaths(BSONObj obj,
+ bool objIsArray,
+ FieldRef* path,
+ BSONObjSet* keys,
+ MultikeyPathsMock* multikeyPaths) const;
+
+ // Helper functions to format the entry appropriately before adding it to the key/path tracker.
+ void _addMultiKey(const FieldRef& fullPath, MultikeyPathsMock* multikeyPaths) const;
+ void _addKey(BSONElement elem, const FieldRef& fullPath, BSONObjSet* keys) const;
+
+ // Helper to check whether the element is a nested array, and conditionally add it to 'keys'.
+ bool _addKeyForNestedArray(BSONElement elem,
+ const FieldRef& fullPath,
+ bool enclosingObjIsArray,
+ BSONObjSet* keys) const;
+
+ std::unique_ptr<ProjectionExecAgg> _projExec;
+ const CollatorInterface* _collator;
+ const BSONObj _keyPattern;
+};
+} // namespace mongo
diff --git a/src/mongo/db/index/all_paths_key_generator_test.cpp b/src/mongo/db/index/all_paths_key_generator_test.cpp
new file mode 100644
index 00000000000..59b0d32cd9c
--- /dev/null
+++ b/src/mongo/db/index/all_paths_key_generator_test.cpp
@@ -0,0 +1,906 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * 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 GNU Affero General 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.
+ */
+
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kIndex
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/bson/json.h"
+#include "mongo/db/index/all_paths_key_generator.h"
+#include "mongo/db/query/collation/collator_interface_mock.h"
+#include "mongo/unittest/unittest.h"
+#include "mongo/util/log.h"
+
+namespace mongo {
+namespace {
+
+BSONObjSet makeKeySet(std::initializer_list<BSONObj> init = {}) {
+ return SimpleBSONObjComparator::kInstance.makeBSONObjSet(std::move(init));
+}
+
+std::string dumpKeyset(const BSONObjSet& objs) {
+ std::stringstream ss;
+ ss << "[ ";
+ for (BSONObjSet::iterator i = objs.begin(); i != objs.end(); ++i) {
+ ss << i->toString() << " ";
+ }
+ ss << "]";
+
+ return ss.str();
+}
+
+bool assertKeysetsEqual(const BSONObjSet& expectedKeys, const BSONObjSet& actualKeys) {
+ if (expectedKeys.size() != actualKeys.size()) {
+ log() << "Expected: " << dumpKeyset(expectedKeys) << ", "
+ << "Actual: " << dumpKeyset(actualKeys);
+ return false;
+ }
+
+ if (!std::equal(expectedKeys.begin(),
+ expectedKeys.end(),
+ actualKeys.begin(),
+ SimpleBSONObjComparator::kInstance.makeEqualTo())) {
+ log() << "Expected: " << dumpKeyset(expectedKeys) << ", "
+ << "Actual: " << dumpKeyset(actualKeys);
+ return false;
+ }
+
+ return true;
+}
+
+// Full-document tests with no projection.
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractTopLevelKey) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+ auto inputDoc = fromjson("{a: 1}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}")});
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractKeysFromNestedObject) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+ auto inputDoc = fromjson("{a: {b: 'one', c: 2}}");
+
+ auto expectedKeys =
+ makeKeySet({fromjson("{'': 'a.b', '': 'one'}"), fromjson("{'': 'a.c', '': 2}")});
+
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, DoNotExtractKeyForEmptyObject) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+ auto inputDoc = fromjson("{a: 1, b: {}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}")});
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractMultikeyPath) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+ auto inputDoc = fromjson("{a: [1, 2, {b: 'one', c: 2}, {d: 3}]}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.d', '': 3}")});
+
+ auto expectedMultikeyPaths = makeKeySet({fromjson("{'': 1, '': 'a'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractMultikeyPathAndDedupKeys) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+ auto inputDoc = fromjson("{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {d: 3}]}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.d', '': 3}")});
+
+ auto expectedMultikeyPaths = makeKeySet({fromjson("{'': 1, '': 'a'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractZeroElementMultikeyPath) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+ auto inputDoc = fromjson("{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {d: 3}], e: []}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.d', '': 3}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractNestedMultikeyPaths) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+
+ // Note: the 'e' array is nested within a subdocument in the enclosing 'a' array; it will
+ // generate a separate multikey entry 'a.e' and index keys for each of its elements. The raw
+ // array nested directly within the 'a' array will not, because the indexing system does not
+ // descend nested arrays without an intervening path component.
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]]}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': 5}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorFullDocumentTest, ExtractMixedPathTypesAndAllSubpaths) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+
+ // Tests a mix of multikey paths, various duplicate-key scenarios, and deeply-nested paths.
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': 5}"),
+ fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12.0}"),
+ fromjson("{'': 'l', '': 'string'}")});
+
+ auto expectedMultikeyPaths = makeKeySet({fromjson("{'': 1, '': 'a'}"),
+ fromjson("{'': 1, '': 'a.e'}"),
+ fromjson("{'': 1, '': 'g.h.j'}"),
+ fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+// Single-subtree implicit projection.
+
+TEST(AllPathsKeyGeneratorSingleSubtreeTest, ExtractSubtreeWithSinglePathComponent) {
+ AllPathsKeyGenerator keyGen{fromjson("{'g.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'g.h.j'}"), fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorSingleSubtreeTest, ExtractSubtreeWithMultiplePathComponents) {
+ AllPathsKeyGenerator keyGen{fromjson("{'g.h.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'g.h.j'}"), fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorSingleSubtreeTest, ExtractMultikeySubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'g.h.j.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'g.h.j'}"), fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorSingleSubtreeTest, ExtractNestedMultikeySubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'a.e.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ // We project through the 'a' array to the nested 'e' array. Both 'a' and 'a.e' are added as
+ // multikey paths.
+ auto expectedKeys =
+ makeKeySet({fromjson("{'': 'a.e', '': 4}"), fromjson("{'': 'a.e', '': 5}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+// Explicit inclusion tests.
+
+TEST(AllPathsKeyGeneratorInclusionTest, InclusionProjectionSingleSubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{g: 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'g.h.j'}"), fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorInclusionTest, InclusionProjectionNestedSubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'g.h': 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'g.h.j'}"), fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorInclusionTest, InclusionProjectionMultikeySubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'g.h.j': 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'g.h.j'}"), fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorInclusionTest, InclusionProjectionNestedMultikeySubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'a.e': 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys =
+ makeKeySet({fromjson("{'': 'a.e', '': 4}"), fromjson("{'': 'a.e', '': 5}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorInclusionTest, InclusionProjectionMultipleSubtrees) {
+ AllPathsKeyGenerator keyGen{
+ fromjson("{'$**': 1}"), fromjson("{'a.b': 1, 'a.c': 1, 'a.e': 1, 'g.h.i': 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': 5}"),
+ fromjson("{'': 'g.h.i', '': 9}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+// Explicit exclusion tests.
+
+TEST(AllPathsKeyGeneratorExclusionTest, ExclusionProjectionSingleSubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{g: 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': 5}"),
+ fromjson("{'': 'l', '': 'string'}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorExclusionTest, ExclusionProjectionNestedSubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'g.h': 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': 5}"),
+ fromjson("{'': 'l', '': 'string'}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorExclusionTest, ExclusionProjectionMultikeySubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'g.h.j': 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': 5}"),
+ fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.k', '': 12.0}"),
+ fromjson("{'': 'l', '': 'string'}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorExclusionTest, ExclusionProjectionNestedMultikeySubtree) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'a.e': 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'one'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'two'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12}"),
+ fromjson("{'': 'l', '': 'string'}")});
+
+ auto expectedMultikeyPaths = makeKeySet({fromjson("{'': 1, '': 'a'}"),
+ fromjson("{'': 1, '': 'g.h.j'}"),
+ fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorExclusionTest, ExclusionProjectionMultipleSubtrees) {
+ AllPathsKeyGenerator keyGen{
+ fromjson("{'$**': 1}"), fromjson("{'a.b': 0, 'a.c': 0, 'a.e': 0, 'g.h.i': 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{a: [1, 2, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: 3, e: [4, 5]}, [6, 7, {f: 8}]], "
+ "g: {h: {i: 9, j: [10, {k: 11}, {k: [11.5]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': 2}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': 11.5}"),
+ fromjson("{'': 'g.h.k', '': 12.0}"),
+ fromjson("{'': 'l', '': 'string'}")});
+
+ auto expectedMultikeyPaths = makeKeySet({fromjson("{'': 1, '': 'a'}"),
+ fromjson("{'': 1, '': 'g.h.j'}"),
+ fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+// Test _id inclusion and exclusion behaviour.
+
+TEST(AllPathsKeyGeneratorIdTest, ExcludeIdFieldIfProjectionIsEmpty) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 1}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, ExcludeIdFieldForSingleSubtreeKeyPattern) {
+ AllPathsKeyGenerator keyGen{fromjson("{'a.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 1}"),
+ fromjson("{'': 'a.e', '': 4}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, PermitIdFieldAsSingleSubtreeKeyPattern) {
+ AllPathsKeyGenerator keyGen{fromjson("{'_id.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys =
+ makeKeySet({fromjson("{'': '_id.id1', '': 1}"), fromjson("{'': '_id.id2', '': 2}")});
+
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, PermitIdSubfieldAsSingleSubtreeKeyPattern) {
+ AllPathsKeyGenerator keyGen{fromjson("{'_id.id1.$**': 1}"), {}, nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': '_id.id1', '': 1}")});
+
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, ExcludeIdFieldByDefaultForInclusionProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{a: 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 1}"),
+ fromjson("{'': 'a.e', '': 4}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, PermitIdSubfieldInclusionInExplicitProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'_id.id1': 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': '_id.id1', '': 1}")});
+
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, ExcludeIdFieldByDefaultForExclusionProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{a: 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys =
+ makeKeySet({fromjson("{'': 'g.h.i', '': 9}"), fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, PermitIdSubfieldExclusionInExplicitProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{'_id.id1': 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': '_id.id2', '': 2}"),
+ fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 1}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, IncludeIdFieldIfExplicitlySpecifiedInProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{_id: 1, a: 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': '_id.id1', '': 1}"),
+ fromjson("{'': '_id.id2', '': 2}"),
+ fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 1}"),
+ fromjson("{'': 'a.e', '': 4}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, ExcludeIdFieldIfExplicitlySpecifiedInProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{_id: 0, a: 1}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 1}"),
+ fromjson("{'': 'a.e', '': 4}")});
+
+ auto expectedMultikeyPaths =
+ makeKeySet({fromjson("{'': 1, '': 'a'}"), fromjson("{'': 1, '': 'a.e'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+TEST(AllPathsKeyGeneratorIdTest, IncludeIdFieldIfExplicitlySpecifiedInExclusionProjection) {
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), fromjson("{_id: 1, a: 0}"), nullptr};
+
+ auto inputDoc = fromjson(
+ "{_id: {id1: 1, id2: 2}, a: [1, {b: 1, e: [4]}, [6, 7, {f: 8}]], g: {h: {i: 9, k: 12.0}}}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': '_id.id1', '': 1}"),
+ fromjson("{'': '_id.id2', '': 2}"),
+ fromjson("{'': 'g.h.i', '': 9}"),
+ fromjson("{'': 'g.h.k', '': 12.0}")});
+
+ auto expectedMultikeyPaths = makeKeySet();
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+// Collation tests.
+
+TEST(AllPathsKeyGeneratorCollationTest, CollationMixedPathAndKeyTypes) {
+ CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
+ AllPathsKeyGenerator keyGen{fromjson("{'$**': 1}"), {}, &collator};
+
+ // Verify that the collation is only applied to String values, but all types are indexed.
+ auto dateVal = "{'$date': 1529453450288}"_sd;
+ auto oidVal = "{'$oid': '520e6431b7fa4ea22d6b1872'}"_sd;
+ auto tsVal = "{'$timestamp': {'t': 1, 'i': 100}}"_sd;
+ auto undefVal = "{'$undefined': true}"_sd;
+
+ auto inputDoc =
+ fromjson("{a: [1, null, {b: 'one', c: 2}, {c: 2, d: 3}, {c: 'two', d: " + dateVal +
+ ", e: [4, " + oidVal + "]}, [6, 7, {f: 8}]], g: {h: {i: " + tsVal +
+ ", j: [10, {k: 11}, {k: [" + undefVal + "]}], k: 12.0}}, l: 'string'}");
+
+ auto expectedKeys = makeKeySet({fromjson("{'': 'a', '': 1}"),
+ fromjson("{'': 'a', '': null}"),
+ fromjson("{'': 'a', '': [6, 7, {f: 8}]}"),
+ fromjson("{'': 'a.b', '': 'eno'}"),
+ fromjson("{'': 'a.c', '': 2}"),
+ fromjson("{'': 'a.c', '': 'owt'}"),
+ fromjson("{'': 'a.d', '': 3}"),
+ fromjson("{'': 'a.d', '': " + dateVal + "}"),
+ fromjson("{'': 'a.e', '': 4}"),
+ fromjson("{'': 'a.e', '': " + oidVal + "}"),
+ fromjson("{'': 'g.h.i', '': " + tsVal + "}"),
+ fromjson("{'': 'g.h.j', '': 10}"),
+ fromjson("{'': 'g.h.j.k', '': 11}"),
+ fromjson("{'': 'g.h.j.k', '': " + undefVal + "}"),
+ fromjson("{'': 'g.h.k', '': 12.0}"),
+ fromjson("{'': 'l', '': 'gnirts'}")});
+
+ auto expectedMultikeyPaths = makeKeySet({fromjson("{'': 1, '': 'a'}"),
+ fromjson("{'': 1, '': 'a.e'}"),
+ fromjson("{'': 1, '': 'g.h.j'}"),
+ fromjson("{'': 1, '': 'g.h.j.k'}")});
+
+ auto outputKeys = makeKeySet();
+ auto multikeyPathsMock = makeKeySet();
+ keyGen.generateKeys(inputDoc, &outputKeys, &multikeyPathsMock);
+
+ ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
+ ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyPathsMock));
+}
+
+} // namespace
+} // namespace mongo
diff --git a/src/mongo/db/index/index_descriptor.cpp b/src/mongo/db/index/index_descriptor.cpp
index 76cccb142ae..1514acd6f81 100644
--- a/src/mongo/db/index/index_descriptor.cpp
+++ b/src/mongo/db/index/index_descriptor.cpp
@@ -90,6 +90,7 @@ constexpr StringData IndexDescriptor::kKeyPatternFieldName;
constexpr StringData IndexDescriptor::kLanguageOverrideFieldName;
constexpr StringData IndexDescriptor::kNamespaceFieldName;
constexpr StringData IndexDescriptor::kPartialFilterExprFieldName;
+constexpr StringData IndexDescriptor::kPathProjectionFieldName;
constexpr StringData IndexDescriptor::kSparseFieldName;
constexpr StringData IndexDescriptor::kStorageEngineFieldName;
constexpr StringData IndexDescriptor::kTextVersionFieldName;
diff --git a/src/mongo/db/index/index_descriptor.h b/src/mongo/db/index/index_descriptor.h
index b62cbeb9e38..d6945fe7301 100644
--- a/src/mongo/db/index/index_descriptor.h
+++ b/src/mongo/db/index/index_descriptor.h
@@ -1,32 +1,32 @@
// index_descriptor.cpp
/**
-* Copyright (C) 2013 10gen Inc.
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License, version 3,
-* as published by the Free Software Foundation.
-*
-* 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
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see <http://www.gnu.org/licenses/>.
-*
-* 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 GNU Affero General 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.
-*/
+ * Copyright (C) 2013 10gen Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * 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 GNU Affero General 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
@@ -77,6 +77,7 @@ public:
static constexpr StringData kLanguageOverrideFieldName = "language_override"_sd;
static constexpr StringData kNamespaceFieldName = "ns"_sd;
static constexpr StringData kPartialFilterExprFieldName = "partialFilterExpression"_sd;
+ static constexpr StringData kPathProjectionFieldName = "starPathsTempName"_sd;
static constexpr StringData kSparseFieldName = "sparse"_sd;
static constexpr StringData kStorageEngineFieldName = "storageEngine"_sd;
static constexpr StringData kTextVersionFieldName = "textIndexVersion"_sd;
@@ -93,6 +94,7 @@ public:
_infoObj(infoObj.getOwned()),
_numFields(infoObj.getObjectField(IndexDescriptor::kKeyPatternFieldName).nFields()),
_keyPattern(infoObj.getObjectField(IndexDescriptor::kKeyPatternFieldName).getOwned()),
+ _projection(infoObj.getObjectField(IndexDescriptor::kPathProjectionFieldName).getOwned()),
_indexName(infoObj.getStringField(IndexDescriptor::kIndexNameFieldName)),
_parentNS(infoObj.getStringField(IndexDescriptor::kNamespaceFieldName)),
_isIdIndex(isIdIndexPattern(_keyPattern)),
@@ -148,6 +150,13 @@ public:
}
/**
+ * Return the path projection spec, if one exists. This is only applicable for '$**' indexes.
+ */
+ const BSONObj& pathProjection() const {
+ return _projection;
+ }
+
+ /**
* Test only command for testing behavior resulting from an incorrect key
* pattern.
*/
@@ -278,6 +287,7 @@ private:
int64_t _numFields; // How many fields are indexed?
BSONObj _keyPattern;
+ BSONObj _projection;
std::string _indexName;
std::string _parentNS;
std::string _indexNamespace;
diff --git a/src/mongo/db/pipeline/document_source_project.cpp b/src/mongo/db/pipeline/document_source_project.cpp
index f5d560c4c9d..61bdfb41fe2 100644
--- a/src/mongo/db/pipeline/document_source_project.cpp
+++ b/src/mongo/db/pipeline/document_source_project.cpp
@@ -41,6 +41,9 @@ namespace mongo {
using boost::intrusive_ptr;
using parsed_aggregation_projection::ParsedAggregationProjection;
+using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy;
+using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy;
+
REGISTER_DOCUMENT_SOURCE(project,
LiteParsedDocumentSourceDefault::parse,
DocumentSourceProject::createFromBson);
@@ -50,7 +53,10 @@ intrusive_ptr<DocumentSource> DocumentSourceProject::create(
const bool isIndependentOfAnyCollection = false;
intrusive_ptr<DocumentSource> project(new DocumentSourceSingleDocumentTransformation(
expCtx,
- ParsedAggregationProjection::create(expCtx, projectSpec),
+ ParsedAggregationProjection::create(expCtx,
+ projectSpec,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays),
"$project",
isIndependentOfAnyCollection));
return project;
diff --git a/src/mongo/db/pipeline/parsed_add_fields.h b/src/mongo/db/pipeline/parsed_add_fields.h
index a7fa9c3e0eb..18e8cfbe514 100644
--- a/src/mongo/db/pipeline/parsed_add_fields.h
+++ b/src/mongo/db/pipeline/parsed_add_fields.h
@@ -51,8 +51,15 @@ namespace parsed_aggregation_projection {
*/
class ParsedAddFields : public ParsedAggregationProjection {
public:
+ /**
+ * TODO SERVER-25510: The ParsedAggregationProjection _id and array-recursion policies are not
+ * applicable to the $addFields "projection" stage. We make them non-configurable here.
+ */
ParsedAddFields(const boost::intrusive_ptr<ExpressionContext>& expCtx)
- : ParsedAggregationProjection(expCtx), _root(new InclusionNode()) {}
+ : ParsedAggregationProjection(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays),
+ _root(new InclusionNode(_arrayRecursionPolicy)) {}
/**
* Creates the data needed to perform an AddFields.
diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp
index 5e106efe1e4..f600da57322 100644
--- a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp
+++ b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp
@@ -236,19 +236,30 @@ private:
elem.isBoolean() || elem.isNumber() ||
_parseMode != ProjectionParseMode::kBanComputedFields);
- if ((elem.isBoolean() || elem.isNumber()) && !elem.trueValue()) {
- // A top-level exclusion of "_id" is allowed in either an inclusion projection or an
- // exclusion projection, so doesn't affect '_parsedType'.
- if (pathToElem.fullPath() != "_id") {
- uassert(40178,
- str::stream() << "Bad projection specification, cannot exclude fields "
- "other than '_id' in an inclusion projection: "
+ if (pathToElem.fullPath() == "_id") {
+ // If the _id field is a computed value, then this must be an inclusion projection. If
+ // it is numeric or boolean, then this does not determine the projection type, due to
+ // the fact that inclusions may explicitly exclude _id and exclusions may include _id.
+ if (!elem.isBoolean() && !elem.isNumber()) {
+ uassert(ErrorCodes::FailedToParse,
+ str::stream() << "Bad projection specification, '_id' may not be a "
+ "computed field in an exclusion projection: "
<< _rawObj.toString(),
!_parsedType ||
- (*_parsedType ==
- TransformerInterface::TransformerType::kExclusionProjection));
- _parsedType = TransformerInterface::TransformerType::kExclusionProjection;
+ _parsedType ==
+ TransformerInterface::TransformerType::kInclusionProjection);
+ _parsedType = TransformerInterface::TransformerType::kInclusionProjection;
}
+ } else if ((elem.isBoolean() || elem.isNumber()) && !elem.trueValue()) {
+ // If this is an excluded field other than '_id', ensure that the projection type has
+ // not already been set to kInclusionProjection.
+ uassert(40178,
+ str::stream() << "Bad projection specification, cannot exclude fields "
+ "other than '_id' in an inclusion projection: "
+ << _rawObj.toString(),
+ !_parsedType || (*_parsedType ==
+ TransformerInterface::TransformerType::kExclusionProjection));
+ _parsedType = TransformerInterface::TransformerType::kExclusionProjection;
} else {
// A boolean true, a truthy numeric value, or any expression can only be used with an
// inclusion projection. Note that literal values like "string" or null are also treated
@@ -310,6 +321,8 @@ private:
std::unique_ptr<ParsedAggregationProjection> ParsedAggregationProjection::create(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const BSONObj& spec,
+ ProjectionDefaultIdPolicy defaultIdPolicy,
+ ProjectionArrayRecursionPolicy arrayRecursionPolicy,
ProjectionParseMode parseMode) {
// Check that the specification was valid. Status returned is unspecific because validate()
// is used by the $addFields stage as well as $project.
@@ -325,8 +338,10 @@ std::unique_ptr<ParsedAggregationProjection> ParsedAggregationProjection::create
// We can't use make_unique() here, since the branches have different types.
std::unique_ptr<ParsedAggregationProjection> parsedProject(
projectionType == TransformerType::kInclusionProjection
- ? static_cast<ParsedAggregationProjection*>(new ParsedInclusionProjection(expCtx))
- : static_cast<ParsedAggregationProjection*>(new ParsedExclusionProjection(expCtx)));
+ ? static_cast<ParsedAggregationProjection*>(
+ new ParsedInclusionProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy))
+ : static_cast<ParsedAggregationProjection*>(
+ new ParsedExclusionProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy)));
// Actually parse the specification.
parsedProject->parse(spec);
diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.h b/src/mongo/db/pipeline/parsed_aggregation_projection.h
index f3ec7f23c85..6c0db1070d7 100644
--- a/src/mongo/db/pipeline/parsed_aggregation_projection.h
+++ b/src/mongo/db/pipeline/parsed_aggregation_projection.h
@@ -140,6 +140,20 @@ private:
*/
class ParsedAggregationProjection : public TransformerInterface {
public:
+ // Allows the caller to indicate whether the projection should default to including or excluding
+ // the _id field in the event that the projection spec does not specify the desired behavior.
+ // For instance, given a projection {a: 1}, specifying 'kExcludeId' is equivalent to projecting
+ // {a: 1, _id: 0} while 'kIncludeId' is equivalent to the projection {a: 1, _id: 1}. If the user
+ // explicitly specifies a projection on _id, then this will override the default policy; for
+ // instance, {a: 1, _id: 0} will exclude _id for both 'kExcludeId' and 'kIncludeId'.
+ enum class ProjectionDefaultIdPolicy { kIncludeId, kExcludeId };
+
+ // Allows the caller to specify how the projection should handle nested arrays; that is, an
+ // array whose immediate parent is itself an array. For example, in the case of sample document
+ // {a: [1, 2, [3, 4], {b: [5, 6]}]} the array [3, 4] is a nested array. The array [5, 6] is not,
+ // because there is an intervening object between it and its closest array ancestor.
+ enum class ProjectionArrayRecursionPolicy { kRecurseNestedArrays, kDoNotRecurseNestedArrays };
+
// Allows the caller to specify whether computed fields should be allowed within inclusion
// projections; they are implicitly prohibited within exclusion projections.
enum class ProjectionParseMode {
@@ -155,6 +169,8 @@ public:
static std::unique_ptr<ParsedAggregationProjection> create(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const BSONObj& spec,
+ ProjectionDefaultIdPolicy defaultIdPolicy,
+ ProjectionArrayRecursionPolicy arrayRecursionPolicy,
ProjectionParseMode parseRules = ProjectionParseMode::kAllowComputedFields);
virtual ~ParsedAggregationProjection() = default;
@@ -187,8 +203,12 @@ public:
}
protected:
- ParsedAggregationProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx)
- : _expCtx(expCtx){};
+ ParsedAggregationProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ ProjectionDefaultIdPolicy defaultIdPolicy,
+ ProjectionArrayRecursionPolicy arrayRecursionPolicy)
+ : _expCtx(expCtx),
+ _arrayRecursionPolicy(arrayRecursionPolicy),
+ _defaultIdPolicy(defaultIdPolicy){};
/**
* Apply the projection to 'input'.
@@ -196,6 +216,9 @@ protected:
virtual Document applyProjection(const Document& input) const = 0;
boost::intrusive_ptr<ExpressionContext> _expCtx;
+
+ ProjectionArrayRecursionPolicy _arrayRecursionPolicy;
+ ProjectionDefaultIdPolicy _defaultIdPolicy;
};
} // namespace parsed_aggregation_projection
} // namespace mongo
diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp
index d7d50ea7316..3054838f6a9 100644
--- a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp
+++ b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp
@@ -44,6 +44,8 @@ namespace mongo {
namespace parsed_aggregation_projection {
namespace {
+using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy;
+using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy;
using ProjectionParseMode = ParsedAggregationProjection::ProjectionParseMode;
template <typename T>
@@ -51,6 +53,17 @@ BSONObj wrapInLiteral(const T& arg) {
return BSON("$literal" << arg);
}
+// Helper to simplify the creation of a ParsedAggregationProjection which includes _id and recurses
+// nested arrays by default.
+std::unique_ptr<ParsedAggregationProjection> makeProjectionWithDefaultPolicies(BSONObj projSpec) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ return ParsedAggregationProjection::create(
+ expCtx,
+ projSpec,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+}
+
//
// Error cases.
//
@@ -58,73 +71,69 @@ BSONObj wrapInLiteral(const T& arg) {
TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateFieldNames) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Include/exclude the same field twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << true << "a" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a" << true)),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << false << "a" << false)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << false << "b" << false))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << false)),
AssertionException);
-
- // Mix of include/exclude and adding a field.
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << wrapInLiteral(1) << "a" << true)),
- AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << false << "a" << wrapInLiteral(0))),
+ makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "b" << false))),
AssertionException);
- // Adding the same field twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << wrapInLiteral(1) << "a" << wrapInLiteral(0))),
+ // Mix of include/exclude and adding a field.
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << true)),
+ AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << wrapInLiteral(0))),
AssertionException);
+
+ // Adding the same field twice.
+ ASSERT_THROWS(
+ makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << wrapInLiteral(0))),
+ AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateIds) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Include/exclude _id twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("_id" << true << "_id" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id" << true)),
+ AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << false)),
AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("_id" << false << "_id" << false)),
- AssertionException);
// Mix of including/excluding and adding _id.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id" << wrapInLiteral(1) << "_id" << true)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id" << false << "_id" << wrapInLiteral(0))),
- AssertionException);
+ ASSERT_THROWS(
+ makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id" << true)),
+ AssertionException);
+ ASSERT_THROWS(
+ makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << wrapInLiteral(0))),
+ AssertionException);
// Adding _id twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id" << wrapInLiteral(1) << "_id" << wrapInLiteral(0))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("_id" << wrapInLiteral(1) << "_id" << wrapInLiteral(0))),
AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldsWithSharedPrefix) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Include/exclude Fields with a shared prefix.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << true << "a.b" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a.b" << true)),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a.b" << false << "a" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << false)),
AssertionException);
// Mix of include/exclude and adding a shared prefix.
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a.b" << true)),
+ AssertionException);
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << wrapInLiteral(1) << "a.b" << true)),
+ makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << wrapInLiteral(0))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a.b" << false << "a" << wrapInLiteral(0))),
- AssertionException);
// Adding a shared prefix twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << wrapInLiteral(1) << "a.b" << wrapInLiteral(0))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << wrapInLiteral(1) << "a.b" << wrapInLiteral(0))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a.b.c.d" << wrapInLiteral(1) << "a.b.c" << wrapInLiteral(0))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a.b.c.d" << wrapInLiteral(1) << "a.b.c" << wrapInLiteral(0))),
AssertionException);
}
@@ -132,44 +141,39 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNum
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Include/exclude non-alphanumeric fields with a shared prefix. First assert that the non-
// alphanumeric fields are accepted when no prefixes are present.
- ASSERT(ParsedAggregationProjection::create(
- expCtx, BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true)));
- ASSERT(ParsedAggregationProjection::create(
- expCtx, BSON("a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c" << false)));
+ ASSERT(makeProjectionWithDefaultPolicies(
+ BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true)));
+ ASSERT(makeProjectionWithDefaultPolicies(
+ BSON("a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c" << false)));
// Then assert that we throw when we introduce a prefixed field.
ASSERT_THROWS(
- ParsedAggregationProjection::create(
- expCtx,
+ makeProjectionWithDefaultPolicies(
BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true << "a.b.d"
<< true)),
AssertionException);
ASSERT_THROWS(
- ParsedAggregationProjection::create(
- expCtx,
- BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" << false
- << "a.b-c"
- << false)),
+ makeProjectionWithDefaultPolicies(BSON(
+ "a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c"
+ << false)),
AssertionException);
// Adding the same field twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a.b?c" << wrapInLiteral(1) << "a.b?c" << wrapInLiteral(0))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a.b?c" << wrapInLiteral(1) << "a.b?c" << wrapInLiteral(0))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a.b c" << wrapInLiteral(0) << "a.b c" << wrapInLiteral(1))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a.b c" << wrapInLiteral(0) << "a.b c" << wrapInLiteral(1))),
AssertionException);
// Mix of include/exclude and adding a shared prefix.
ASSERT_THROWS(
- ParsedAggregationProjection::create(
- expCtx,
+ makeProjectionWithDefaultPolicies(
BSON("a.b-c" << true << "a.b" << wrapInLiteral(1) << "a.b?c" << true << "a.b c" << true
<< "a.b.d"
<< true)),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx,
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b"
<< wrapInLiteral(0)
<< "a.b-c"
@@ -177,8 +181,7 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNum
AssertionException);
// Adding a shared prefix twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx,
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
BSON("a.b-c" << wrapInLiteral(1) << "a.b" << wrapInLiteral(1) << "a.b?c"
<< wrapInLiteral(1)
<< "a.b c"
@@ -186,8 +189,7 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNum
<< "a.b.d"
<< wrapInLiteral(0))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx,
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
BSON("a.b.d" << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b?c"
<< wrapInLiteral(1)
<< "a.b"
@@ -200,232 +202,206 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNum
TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfIdAndSubFieldsOfId) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Include/exclude _id twice.
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id.x" << true)),
+ AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << false)),
+ AssertionException);
+
+ // Mix of including/excluding and adding _id.
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("_id" << true << "_id.x" << true)),
+ makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id.x" << true)),
AssertionException);
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("_id.x" << false << "_id" << false)),
+ makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << wrapInLiteral(0))),
AssertionException);
- // Mix of including/excluding and adding _id.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id" << wrapInLiteral(1) << "_id.x" << true)),
+ // Adding _id twice.
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("_id" << wrapInLiteral(1) << "_id.x" << wrapInLiteral(0))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id.x" << false << "_id" << wrapInLiteral(0))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("_id.b.c.d" << wrapInLiteral(1) << "_id.b.c" << wrapInLiteral(0))),
AssertionException);
+}
- // Adding _id twice.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id" << wrapInLiteral(1) << "_id.x" << wrapInLiteral(0))),
- AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(
- expCtx, BSON("_id.b.c.d" << wrapInLiteral(1) << "_id.b.c" << wrapInLiteral(0))),
- AssertionException);
+TEST(ParsedAggregationProjectionErrors, ShouldAllowMixOfIdInclusionAndExclusion) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+
+ // Mixing "_id" inclusion with exclusion.
+ auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a" << false));
+ ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
+
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false << "_id" << true));
+ ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
+
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a.b.c" << false));
+ ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfInclusionAndExclusion) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Simple mix.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << true << "b" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << false)),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << false << "b" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << true)),
AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << BSON("b" << false << "c" << true))),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("_id" << BSON("b" << false << "c" << true))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "c" << true))),
AssertionException);
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("_id.b" << false << "a.c" << true)),
+ makeProjectionWithDefaultPolicies(BSON("_id" << BSON("b" << false << "c" << true))),
AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.b" << false << "a.c" << true)),
+ AssertionException);
// Mix while also adding a field.
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << true << "b" << wrapInLiteral(1) << "c" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << true << "b" << wrapInLiteral(1) << "c" << false)),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << false << "b" << wrapInLiteral(1) << "c" << true)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << false << "b" << wrapInLiteral(1) << "c" << true)),
AssertionException);
- // Mixing "_id" inclusion with exclusion.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("_id" << true << "a" << false)),
+ // Mix of "_id" subfield inclusion and exclusion.
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << true << "a.b.c" << false)),
AssertionException);
+}
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << false << "_id" << true)),
+TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfExclusionAndComputedFields) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << wrapInLiteral(1))),
AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("_id" << true << "a.b.c" << false)),
- AssertionException);
-
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("_id.x" << true << "a.b.c" << false)),
- AssertionException);
-}
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "b" << false)),
+ AssertionException);
-TEST(ParsedAggregationProjectionType, ShouldRejectMixOfExclusionAndComputedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << false << "b" << wrapInLiteral(1))),
+ makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a.c" << wrapInLiteral(1))),
AssertionException);
ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << wrapInLiteral(1) << "b" << false)),
+ makeProjectionWithDefaultPolicies(BSON("a.b" << wrapInLiteral(1) << "a.c" << false)),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a.b" << false << "a.c" << wrapInLiteral(1))),
- AssertionException);
-
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a.b" << wrapInLiteral(1) << "a.c" << false)),
- AssertionException);
-
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))),
AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectDottedFieldInSubDocument) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << BSON("b.c" << true))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << true))),
+ AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << wrapInLiteral(1)))),
AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << BSON("b.c" << wrapInLiteral(1)))),
- AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldNamesStartingWithADollar) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("$dollar" << 0)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("$dollar" << 1)),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 0)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 1)), AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("b.$dollar" << 0)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("b.$dollar" << 1)),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 0)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 1)), AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("b" << BSON("$dollar" << 0))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 0))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("b" << BSON("$dollar" << 1))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 1))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("$add" << 0)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("$add" << 1)),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 0)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 1)), AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectTopLevelExpressions) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("$add" << BSON_ARRAY(4 << 2))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << BSON_ARRAY(4 << 2))),
AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectExpressionWithMultipleFieldNames) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << 1 << "$add" << BSON_ARRAY(4 << 2)))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("b" << 1 << "$add" << BSON_ARRAY(4 << 2)))),
+ AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("b" << BSON("c" << 1 << "$add" << BSON_ARRAY(4 << 2))))),
+ AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("b" << BSON("$add" << BSON_ARRAY(4 << 2) << "c" << 1)))),
AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << BSON("c" << 1 << "$add" << BSON_ARRAY(4 << 2))))),
- AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << BSON("$add" << BSON_ARRAY(4 << 2) << "c" << 1)))),
- AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyProjection) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSONObj()), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSONObj()), AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyNestedObject) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << BSONObj())),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSONObj())), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << BSONObj())),
AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << false << "b" << BSONObj())),
- AssertionException);
- ASSERT_THROWS(
- ParsedAggregationProjection::create(expCtx, BSON("a" << true << "b" << BSONObj())),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a.b" << BSONObj())),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << BSONObj())),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << BSON("b" << BSONObj()))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << BSONObj())), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << BSONObj()))),
AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidExpression) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(
- expCtx, BSON("a" << true << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(
+ BSON("a" << true << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))),
AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidFieldPath) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
// Empty field names.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("" << wrapInLiteral(2))),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("" << true)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << wrapInLiteral(2))),
AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << true)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << false)), AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << BSON("" << true))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << true))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a" << BSON("" << false))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << false))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("" << BSON("a" << true))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << true))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("" << BSON("a" << false))),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << false))),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a." << true)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("a." << false)),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << true)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << false)), AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON(".a" << true)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON(".a" << false)),
- AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << true)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << false)), AssertionException);
// Not testing field names with null bytes, since that is invalid BSON, and won't make it to the
// $project stage without a previous error.
// Field names starting with '$'.
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("$x" << wrapInLiteral(2))),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("c.$d" << true)),
- AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx, BSON("c.$d" << false)),
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$x" << wrapInLiteral(2))),
AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << true)), AssertionException);
+ ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << false)), AssertionException);
}
TEST(ParsedAggregationProjectionErrors, ShouldNotErrorOnTwoNestedFields) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedAggregationProjection::create(expCtx, BSON("a.b" << true << "a.c" << true));
- ParsedAggregationProjection::create(expCtx, BSON("a.b" << true << "a" << BSON("c" << true)));
+ makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << true));
+ makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a" << BSON("c" << true)));
}
//
@@ -434,202 +410,259 @@ TEST(ParsedAggregationProjectionErrors, ShouldNotErrorOnTwoNestedFields) {
TEST(ParsedAggregationProjectionType, ShouldDefaultToInclusionProjection) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- auto parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id" << true));
+ auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id" << wrapInLiteral(1)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("a" << wrapInLiteral(1)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
}
TEST(ParsedAggregationProjectionType, ShouldDetectExclusionProjection) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- auto parsedProject = ParsedAggregationProjection::create(expCtx, BSON("a" << false));
+ auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id.x" << false));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << false));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id" << BSON("x" << false)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << false)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("x" << BSON("_id" << false)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << false)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id" << false));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
}
TEST(ParsedAggregationProjectionType, ShouldDetectInclusionProjection) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- auto parsedProject = ParsedAggregationProjection::create(expCtx, BSON("a" << true));
+ auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << true));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject =
- ParsedAggregationProjection::create(expCtx, BSON("_id" << false << "a" << true));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << true));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject =
- ParsedAggregationProjection::create(expCtx, BSON("_id" << false << "a.b.c" << true));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << true));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id.x" << true));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << true));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id" << BSON("x" << true)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << true)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("x" << BSON("_id" << true)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << true)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
}
TEST(ParsedAggregationProjectionType, ShouldTreatOnlyComputedFieldsAsAnInclusionProjection) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- auto parsedProject = ParsedAggregationProjection::create(expCtx, BSON("a" << wrapInLiteral(1)));
+ auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << false << "a" << wrapInLiteral(1)));
+ parsedProject =
+ makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << false << "a.b.c" << wrapInLiteral(1)));
+ parsedProject =
+ makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx, BSON("_id.x" << wrapInLiteral(1)));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject =
- ParsedAggregationProjection::create(expCtx, BSON("_id" << BSON("x" << wrapInLiteral(1))));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << wrapInLiteral(1))));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject =
- ParsedAggregationProjection::create(expCtx, BSON("x" << BSON("_id" << wrapInLiteral(1))));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << wrapInLiteral(1))));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
}
TEST(ParsedAggregationProjectionType, ShouldAllowMixOfInclusionAndComputedFields) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto parsedProject =
- ParsedAggregationProjection::create(expCtx, BSON("a" << true << "b" << wrapInLiteral(1)));
+ makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("a.b" << true << "a.c" << wrapInLiteral(1)));
+ parsedProject =
+ makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << wrapInLiteral(1)));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1))));
+ parsedProject = makeProjectionWithDefaultPolicies(
+ BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1))));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(expCtx,
- BSON("a" << BSON("b" << true << "c"
- << "stringLiteral")));
+ parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << true << "c"
+ << "stringLiteral")));
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
}
-TEST(ParsedAggregationProjectionType, ShouldRejectMixOfInclusionAndComputedFieldsInStrictMode) {
+TEST(ParsedAggregationProjectionType, ShouldRejectMixOfInclusionAndBannedComputedFields) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx,
- BSON("a" << true << "b" << wrapInLiteral(1)),
- ProjectionParseMode::kBanComputedFields),
- AssertionException);
+ ASSERT_THROWS(
+ ParsedAggregationProjection::create(expCtx,
+ BSON("a" << true << "b" << wrapInLiteral(1)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields),
+ AssertionException);
ASSERT_THROWS(
ParsedAggregationProjection::create(expCtx,
BSON("a.b" << true << "a.c" << wrapInLiteral(1)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
ProjectionParseMode::kBanComputedFields),
AssertionException);
ASSERT_THROWS(ParsedAggregationProjection::create(
expCtx,
BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1))),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
ProjectionParseMode::kBanComputedFields),
AssertionException);
- ASSERT_THROWS(ParsedAggregationProjection::create(expCtx,
- BSON("a" << BSON("b" << true << "c"
- << "stringLiteral")),
- ProjectionParseMode::kBanComputedFields),
- AssertionException);
+ ASSERT_THROWS(
+ ParsedAggregationProjection::create(expCtx,
+ BSON("a" << BSON("b" << true << "c"
+ << "stringLiteral")),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields),
+ AssertionException);
}
-TEST(ParsedAggregationProjectionType, ShouldRejectOnlyComputedFieldsInStrictMode) {
+TEST(ParsedAggregationProjectionType, ShouldRejectOnlyComputedFieldsWhenComputedFieldsAreBanned) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
ASSERT_THROWS(ParsedAggregationProjection::create(
expCtx,
BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(2)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
ProjectionParseMode::kBanComputedFields),
AssertionException);
ASSERT_THROWS(ParsedAggregationProjection::create(
expCtx,
BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(2)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
ProjectionParseMode::kBanComputedFields),
AssertionException);
ASSERT_THROWS(ParsedAggregationProjection::create(
expCtx,
BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2))),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
ProjectionParseMode::kBanComputedFields),
AssertionException);
ASSERT_THROWS(ParsedAggregationProjection::create(
expCtx,
BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2))),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
ProjectionParseMode::kBanComputedFields),
AssertionException);
}
-TEST(ParsedAggregationProjectionType, ShouldAcceptInclusionProjectionInStrictMode) {
+TEST(ParsedAggregationProjectionType, ShouldAcceptInclusionProjectionWhenComputedFieldsAreBanned) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- auto parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("a" << true), ProjectionParseMode::kBanComputedFields);
+ auto parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("a" << true),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << false << "a" << true), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id" << false << "a" << true),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << false << "a.b.c" << true), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id" << false << "a.b.c" << true),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id.x" << true), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id.x" << true),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << BSON("x" << true)), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id" << BSON("x" << true)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("x" << BSON("_id" << true)), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("x" << BSON("_id" << true)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection);
}
-TEST(ParsedAggregationProjectionType, ShouldAcceptExclusionProjectionInStrictMode) {
+TEST(ParsedAggregationProjectionType, ShouldAcceptExclusionProjectionWhenComputedFieldsAreBanned) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- auto parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("a" << false), ProjectionParseMode::kBanComputedFields);
+ auto parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("a" << false),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id.x" << false), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id.x" << false),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << BSON("x" << false)), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id" << BSON("x" << false)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("x" << BSON("_id" << false)), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("x" << BSON("_id" << false)),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
- parsedProject = ParsedAggregationProjection::create(
- expCtx, BSON("_id" << false), ProjectionParseMode::kBanComputedFields);
+ parsedProject =
+ ParsedAggregationProjection::create(expCtx,
+ BSON("_id" << false),
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays,
+ ProjectionParseMode::kBanComputedFields);
ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection);
}
@@ -637,8 +670,7 @@ TEST(ParsedAggregationProjectionType, ShouldCoerceNumericsToBools) {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
std::vector<Value> zeros = {Value(0), Value(0LL), Value(0.0), Value(Decimal128(0))};
for (auto&& zero : zeros) {
- auto parsedProject =
- ParsedAggregationProjection::create(expCtx, Document{{"a", zero}}.toBson());
+ auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", zero}}.toBson());
ASSERT(parsedProject->getType() ==
TransformerInterface::TransformerType::kExclusionProjection);
}
@@ -646,8 +678,7 @@ TEST(ParsedAggregationProjectionType, ShouldCoerceNumericsToBools) {
std::vector<Value> nonZeroes = {
Value(1), Value(-1), Value(3), Value(1LL), Value(1.0), Value(Decimal128(1))};
for (auto&& nonZero : nonZeroes) {
- auto parsedProject =
- ParsedAggregationProjection::create(expCtx, Document{{"a", nonZero}}.toBson());
+ auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", nonZero}}.toBson());
ASSERT(parsedProject->getType() ==
TransformerInterface::TransformerType::kInclusionProjection);
}
diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection.cpp
index 4dffd87f64f..40b561f24c7 100644
--- a/src/mongo/db/pipeline/parsed_exclusion_projection.cpp
+++ b/src/mongo/db/pipeline/parsed_exclusion_projection.cpp
@@ -43,7 +43,8 @@ namespace parsed_aggregation_projection {
// ExclusionNode.
//
-ExclusionNode::ExclusionNode(std::string pathToNode) : _pathToNode(std::move(pathToNode)) {}
+ExclusionNode::ExclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode)
+ : _arrayRecursionPolicy(recursionPolicy), _pathToNode(std::move(pathToNode)) {}
Document ExclusionNode::serialize() const {
MutableDocument output;
@@ -90,8 +91,8 @@ ExclusionNode* ExclusionNode::getChild(std::string field) const {
ExclusionNode* ExclusionNode::addChild(std::string field) {
auto pathToChild = _pathToNode.empty() ? field : _pathToNode + "." + field;
- auto emplacedPair = _children.emplace(
- std::make_pair(std::move(field), stdx::make_unique<ExclusionNode>(pathToChild)));
+ auto emplacedPair = _children.emplace(std::make_pair(
+ std::move(field), stdx::make_unique<ExclusionNode>(_arrayRecursionPolicy, pathToChild)));
// emplacedPair is a pair<iterator position, bool inserted>.
invariant(emplacedPair.second);
@@ -112,7 +113,12 @@ Value ExclusionNode::applyProjectionToValue(Value val) const {
// instead will result in {a: [{b: 0}, {b: 1}]}.
std::vector<Value> values = val.getArray();
for (auto it = values.begin(); it != values.end(); it++) {
- *it = applyProjectionToValue(*it);
+ // If this is a nested array and our policy is to not recurse, leave the array
+ // as-is. Otherwise, descend into the array and project each element individually.
+ const bool shouldSkip = it->isArray() &&
+ _arrayRecursionPolicy ==
+ ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays;
+ *it = (shouldSkip ? *it : applyProjectionToValue(*it));
}
return Value(std::move(values));
}
@@ -145,23 +151,29 @@ Document ParsedExclusionProjection::applyProjection(const Document& inputDoc) co
}
void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, size_t depth) {
+ bool idSpecified = false;
+
for (auto elem : spec) {
- const auto fieldName = elem.fieldNameStringData().toString();
+ const auto fieldName = elem.fieldNameStringData();
- // A $ should have been detected in ParsedAggregationProjection's parsing before we get
- // here.
+ // A $ should have been detected by ParsedAggregationProjection before we get here.
invariant(fieldName[0] != '$');
+ // Track whether the projection spec specifies a desired behavior for the _id field.
+ idSpecified = idSpecified || fieldName == "_id"_sd || fieldName.startsWith("_id."_sd);
+
switch (elem.type()) {
case BSONType::Bool:
case BSONType::NumberInt:
case BSONType::NumberLong:
case BSONType::NumberDouble:
case BSONType::NumberDecimal: {
- // We have already verified this is an exclusion projection.
- invariant(!elem.trueValue());
-
- node->excludePath(FieldPath(fieldName));
+ // We have already verified this is an exclusion projection. _id is the only field
+ // which is permitted to be explicitly included here.
+ invariant(!elem.trueValue() || elem.fieldNameStringData() == "_id"_sd);
+ if (!elem.trueValue()) {
+ node->excludePath(FieldPath(fieldName));
+ }
break;
}
case BSONType::Object: {
@@ -195,6 +207,12 @@ void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node,
default: { MONGO_UNREACHABLE; }
}
}
+
+ // If _id was not specified, then doing nothing will cause it to be included. If the default _id
+ // policy is kExcludeId, we add a new entry for _id to the ExclusionNode tree here.
+ if (!idSpecified && _defaultIdPolicy == ProjectionDefaultIdPolicy::kExcludeId) {
+ _root->excludePath({FieldPath("_id")});
+ }
}
} // namespace parsed_aggregation_projection
diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.h b/src/mongo/db/pipeline/parsed_exclusion_projection.h
index 381143f2390..8f4799d69c5 100644
--- a/src/mongo/db/pipeline/parsed_exclusion_projection.h
+++ b/src/mongo/db/pipeline/parsed_exclusion_projection.h
@@ -49,7 +49,10 @@ namespace parsed_aggregation_projection {
*/
class ExclusionNode {
public:
- ExclusionNode(std::string pathToNode = "");
+ using ProjectionArrayRecursionPolicy =
+ ParsedAggregationProjection::ProjectionArrayRecursionPolicy;
+
+ ExclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode = "");
/**
* Serialize this exclusion.
@@ -84,6 +87,8 @@ private:
// Fields excluded at this level.
stdx::unordered_set<std::string> _excludedFields;
+ ProjectionArrayRecursionPolicy _arrayRecursionPolicy;
+
std::string _pathToNode;
stdx::unordered_map<std::string, std::unique_ptr<ExclusionNode>> _children;
};
@@ -97,8 +102,11 @@ private:
*/
class ParsedExclusionProjection : public ParsedAggregationProjection {
public:
- ParsedExclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx)
- : ParsedAggregationProjection(expCtx), _root(new ExclusionNode()) {}
+ ParsedExclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ ProjectionDefaultIdPolicy defaultIdPolicy,
+ ProjectionArrayRecursionPolicy arrayRecursionPolicy)
+ : ParsedAggregationProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy),
+ _root(new ExclusionNode(_arrayRecursionPolicy)) {}
TransformerType getType() const final {
return TransformerType::kExclusionProjection;
diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp
index c5f31b1ad67..6287f64d374 100644
--- a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp
+++ b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp
@@ -48,8 +48,21 @@
namespace mongo {
namespace parsed_aggregation_projection {
namespace {
+
+using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy;
+using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy;
+
using std::vector;
+// Helper to simplify the creation of a ParsedExclusionProjection which includes _id and recurses
+// nested arrays by default.
+ParsedExclusionProjection makeExclusionProjectionWithDefaultPolicies() {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ return ParsedExclusionProjection(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+}
+
//
// Errors.
//
@@ -57,31 +70,32 @@ using std::vector;
DEATH_TEST(ExclusionProjection,
ShouldRejectComputedField,
"Invariant failure fieldName[0] != '$'") {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
// Top-level expression.
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << false << "b" << BSON("$literal" << 1)));
}
DEATH_TEST(ExclusionProjection,
- ShouldFailWhenGivenIncludedField,
- "Invariant failure !elem.trueValue()") {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ ShouldFailWhenGivenIncludedNonIdField,
+ "Invariant failure !elem.trueValue() || elem.fieldNameStringData() == \"_id\"_sd") {
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << true));
}
-DEATH_TEST(ExclusionProjection,
- ShouldFailWhenGivenIncludedId,
- "Invariant failure !elem.trueValue()") {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+DEATH_TEST(ExclusionProjectionExecutionTest,
+ ShouldFailWhenGivenIncludedIdSubfield,
+ "Invariant failure !elem.trueValue() || elem.fieldNameStringData() == \"_id\"_sd") {
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
+ exclusion.parse(BSON("_id.id1" << true));
+}
+
+TEST(ExclusionProjection, ShouldAllowExplicitIdInclusionInExclusionSpec) {
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("_id" << true << "a" << false));
}
TEST(ExclusionProjection, ShouldSerializeToEquivalentProjection) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(
fromjson("{a: 0, b: {c: NumberLong(0), d: 0.0}, 'x.y': false, _id: NumberInt(0)}"));
@@ -111,8 +125,7 @@ TEST(ExclusionProjection, ShouldNotAddAnyDependencies) {
// need to include the "a" in the dependencies of this projection, since it will just be ignored
// later. If there are no later stages, then we will finish the dependency computation
// cycle without full knowledge of which fields are needed, and thus include all the fields.
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("_id" << false << "a" << false << "b.c" << false << "x.y.z" << false));
DepsTracker deps;
@@ -124,8 +137,7 @@ TEST(ExclusionProjection, ShouldNotAddAnyDependencies) {
}
TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModified) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("_id" << false << "a" << false << "b.c" << false));
auto modifiedPaths = exclusion.getModifiedPaths();
@@ -137,8 +149,7 @@ TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModified) {
}
TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModifiedWhenSpecifiedAsNestedObj) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << BSON("b" << false << "c" << BSON("d" << false))));
auto modifiedPaths = exclusion.getModifiedPaths();
@@ -153,8 +164,7 @@ TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModifiedWhenSpecifiedAsNes
//
TEST(ExclusionProjectionExecutionTest, ShouldExcludeTopLevelField) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << false));
// More than one field in document.
@@ -179,8 +189,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeTopLevelField) {
}
TEST(ExclusionProjectionExecutionTest, ShouldCoerceNumericsToBools) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << Value(0) << "b" << Value(0LL) << "c" << Value(0.0) << "d"
<< Value(Decimal128(0))));
@@ -191,8 +200,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldCoerceNumericsToBools) {
}
TEST(ExclusionProjectionExecutionTest, ShouldPreserveOrderOfExistingFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("second" << false));
auto result = exclusion.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}});
auto expectedResult = Document{{"first", 0}, {"third", 2}};
@@ -200,8 +208,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldPreserveOrderOfExistingFields) {
}
TEST(ExclusionProjectionExecutionTest, ShouldImplicitlyIncludeId) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << false));
auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}});
auto expectedResult = Document{{"b", 2}, {"_id", "ID"_sd}};
@@ -209,8 +216,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldImplicitlyIncludeId) {
}
TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdIfExplicitlyExcluded) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << false << "_id" << false));
auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}});
auto expectedResult = Document{{"b", 2}};
@@ -218,8 +224,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdIfExplicitlyExcluded) {
}
TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdAndKeepAllOtherFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("_id" << false));
auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}});
auto expectedResult = Document{{"a", 1}, {"b", 2}};
@@ -231,8 +236,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdAndKeepAllOtherFields) {
//
TEST(ExclusionProjectionExecutionTest, ShouldExcludeSubFieldsOfId) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("_id.x" << false << "_id" << BSON("y" << false)));
auto result = exclusion.applyProjection(
Document{{"_id", Document{{"x", 1}, {"y", 2}, {"z", 3}}}, {"a", 1}});
@@ -241,8 +245,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeSubFieldsOfId) {
}
TEST(ExclusionProjectionExecutionTest, ShouldExcludeSimpleDottedFieldFromSubDoc) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a.b" << false));
// More than one field in sub document.
@@ -267,8 +270,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeSimpleDottedFieldFromSubDoc)
}
TEST(ExclusionProjectionExecutionTest, ShouldNotCreateSubDocIfDottedExcludedFieldDoesNotExist) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("sub.target" << false));
// Should not add the path if it doesn't exist.
@@ -283,8 +285,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotCreateSubDocIfDottedExcludedFiel
}
TEST(ExclusionProjectionExecutionTest, ShouldApplyDottedExclusionToEachElementInArray) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a.b" << false));
std::vector<Value> nestedValues = {
@@ -307,8 +308,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldApplyDottedExclusionToEachElementIn
}
TEST(ExclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
// Exclude all of "a.b", "a.c", "a.d", and "a.e".
exclusion.parse(
BSON("a.b" << false << "a.c" << false << "a" << BSON("d" << false << "e" << false)));
@@ -319,8 +319,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) {
}
TEST(ExclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedExclusionProjection exclusion(expCtx);
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
exclusion.parse(BSON("a" << false));
MutableDocument inputDocBuilder(Document{{"_id", "ID"_sd}, {"a", 1}});
@@ -335,6 +334,195 @@ TEST(ExclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc)
ASSERT_DOCUMENT_EQ(result, expectedDoc.freeze());
}
+//
+// _id exclusion policy.
+//
+
+TEST(ExclusionProjectionExecutionTest, ShouldIncludeIdByDefault) {
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
+ exclusion.parse(BSON("a" << false));
+
+ auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"_id", 2}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldIncludeIdWithExplicitPolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("a" << false));
+
+ auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"_id", 2}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdWithExplicitPolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kExcludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("a" << false));
+
+ auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitExcludeIdSpec) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("_id" << false << "a" << false));
+
+ auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIncludeIdSpec) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kExcludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("_id" << true << "a" << false));
+
+ auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}, {"b", 4}});
+ auto expectedResult = Document{{"_id", 2}, {"b", 4}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefaultIncludePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("_id.id1" << false << "a" << false));
+
+ auto result = exclusion.applyProjection(
+ Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}});
+ auto expectedResult = Document{{"_id", Document{{"id2", 2}}}, {"b", 4}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefaultExcludePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kExcludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("_id.id1" << false << "a" << false));
+
+ auto result = exclusion.applyProjection(
+ Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}});
+ auto expectedResult = Document{{"_id", Document{{"id2", 2}}}, {"b", 4}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+//
+// Nested array recursion.
+//
+
+TEST(ExclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) {
+ auto exclusion = makeExclusionProjectionWithDefaultPolicies();
+ exclusion.parse(BSON("a.b" << false));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{c: 5}], {d: 6}]}
+ auto result = exclusion.applyProjection(
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}});
+
+ auto expectedResult = Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"c", 3}}),
+ Value(vector<Value>{Value(Document{{"c", 5}})}),
+ Value(Document{{"d", 6}})}}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldRecurseNestedArraysForExplicitProRecursePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ exclusion.parse(BSON("a.b" << false));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{c: 5}], {d: 6}]}
+ auto result = exclusion.applyProjection(
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}});
+
+ auto expectedResult = Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"c", 3}}),
+ Value(vector<Value>{Value(Document{{"c", 5}})}),
+ Value(Document{{"d", 6}})}}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecursePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays);
+ exclusion.parse(BSON("a.b" << false));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{b: 4, c: 5}], {d: 6}]}
+ auto result = exclusion.applyProjection(
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}});
+
+ auto expectedResult =
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(ExclusionProjectionExecutionTest, ShouldNotRetainNestedArraysIfNoRecursionNeeded) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedExclusionProjection exclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays);
+ exclusion.parse(BSON("a" << false));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {}
+ const auto inputDoc =
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}};
+
+ auto result = exclusion.applyProjection(inputDoc);
+ const auto expectedResult = Document{};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
} // namespace
} // namespace parsed_aggregation_projection
} // namespace mongo
diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp
index 45a9998e3ff..7e0c6bb05f7 100644
--- a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp
+++ b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp
@@ -43,7 +43,8 @@ using std::unique_ptr;
// InclusionNode
//
-InclusionNode::InclusionNode(std::string pathToNode) : _pathToNode(std::move(pathToNode)) {}
+InclusionNode::InclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode)
+ : _arrayRecursionPolicy(recursionPolicy), _pathToNode(std::move(pathToNode)) {}
void InclusionNode::optimize() {
for (auto&& expressionIt : _expressions) {
@@ -129,7 +130,11 @@ Value InclusionNode::applyInclusionsToValue(Value inputValue) const {
} else if (inputValue.getType() == BSONType::Array) {
std::vector<Value> values = inputValue.getArray();
for (auto it = values.begin(); it != values.end(); ++it) {
- *it = applyInclusionsToValue(*it);
+ // If this is a nested array and our policy is to not recurse, remove the array.
+ // Otherwise, descend into the array and project each element individually.
+ const bool shouldSkip = it->isArray() &&
+ _arrayRecursionPolicy == ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays;
+ *it = (shouldSkip ? Value() : applyInclusionsToValue(*it));
}
return Value(std::move(values));
} else {
@@ -222,8 +227,9 @@ InclusionNode* InclusionNode::addChild(string field) {
invariant(!str::contains(field, "."));
_orderToProcessAdditionsAndChildren.push_back(field);
auto childPath = FieldPath::getFullyQualifiedPath(_pathToNode, field);
- auto insertedPair = _children.emplace(
- std::make_pair(std::move(field), stdx::make_unique<InclusionNode>(std::move(childPath))));
+ auto insertedPair = _children.emplace(std::make_pair(
+ std::move(field),
+ stdx::make_unique<InclusionNode>(_arrayRecursionPolicy, std::move(childPath))));
return insertedPair.first->second.get();
}
@@ -262,15 +268,15 @@ void InclusionNode::addComputedPaths(std::set<std::string>* computedPaths,
//
void ParsedInclusionProjection::parse(const BSONObj& spec) {
- // It is illegal to specify a projection with no output fields.
+ // It is illegal to specify an inclusion with no output fields.
bool atLeastOneFieldInOutput = false;
- // Tracks whether or not we should implicitly include "_id".
+ // Tracks whether or not we should apply the default _id projection policy.
bool idSpecified = false;
for (auto elem : spec) {
auto fieldName = elem.fieldNameStringData();
- idSpecified = idSpecified || fieldName == "_id" || fieldName.startsWith("_id.");
+ idSpecified = idSpecified || fieldName == "_id"_sd || fieldName.startsWith("_id."_sd);
if (fieldName == "_id") {
const bool idIsExcluded = (!elem.trueValue() && (elem.isNumber() || elem.isBoolean()));
if (idIsExcluded) {
@@ -327,9 +333,13 @@ void ParsedInclusionProjection::parse(const BSONObj& spec) {
}
if (!idSpecified) {
- // "_id" wasn't specified, so include it by default.
- atLeastOneFieldInOutput = true;
- _root->addIncludedField(FieldPath("_id"));
+ // _id wasn't specified, so apply the default _id projection policy here.
+ if (_defaultIdPolicy == ProjectionDefaultIdPolicy::kExcludeId) {
+ _idExcluded = true;
+ } else {
+ atLeastOneFieldInOutput = true;
+ _root->addIncludedField(FieldPath("_id"));
+ }
}
uassert(16403,
diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.h b/src/mongo/db/pipeline/parsed_inclusion_projection.h
index 81fdd0d1db2..644a44409fd 100644
--- a/src/mongo/db/pipeline/parsed_inclusion_projection.h
+++ b/src/mongo/db/pipeline/parsed_inclusion_projection.h
@@ -52,7 +52,10 @@ namespace parsed_aggregation_projection {
*/
class InclusionNode {
public:
- InclusionNode(std::string pathToNode = "");
+ using ProjectionArrayRecursionPolicy =
+ ParsedAggregationProjection::ProjectionArrayRecursionPolicy;
+
+ InclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode = "");
/**
* Optimize any computed expressions.
@@ -156,6 +159,8 @@ private:
*/
bool subtreeContainsComputedFields() const;
+ ProjectionArrayRecursionPolicy _arrayRecursionPolicy;
+
std::string _pathToNode;
// Our projection semantics are such that all field additions need to be processed in the order
@@ -184,8 +189,11 @@ private:
*/
class ParsedInclusionProjection : public ParsedAggregationProjection {
public:
- ParsedInclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx)
- : ParsedAggregationProjection(expCtx), _root(new InclusionNode()) {}
+ ParsedInclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ ProjectionDefaultIdPolicy defaultIdPolicy,
+ ProjectionArrayRecursionPolicy arrayRecursionPolicy)
+ : ParsedAggregationProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy),
+ _root(new InclusionNode(_arrayRecursionPolicy)) {}
TransformerType getType() const final {
return TransformerType::kInclusionProjection;
diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp
index 46d28083680..3996db1036a 100644
--- a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp
+++ b/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp
@@ -40,11 +40,16 @@
#include "mongo/db/pipeline/document_value_test_util.h"
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/pipeline/value.h"
+#include "mongo/unittest/death_test.h"
#include "mongo/unittest/unittest.h"
namespace mongo {
namespace parsed_aggregation_projection {
namespace {
+
+using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy;
+using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy;
+
using std::vector;
template <typename T>
@@ -52,23 +57,43 @@ BSONObj wrapInLiteral(const T& arg) {
return BSON("$literal" << arg);
}
-TEST(InclusionProjection, ShouldThrowWhenParsingInvalidExpression) {
+// Helper to simplify the creation of a ParsedInclusionProjection which includes _id and recurses
+// nested arrays by default.
+ParsedInclusionProjection makeInclusionProjectionWithDefaultPolicies() {
const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ return ParsedInclusionProjection(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+}
+
+DEATH_TEST(InclusionProjection,
+ ShouldFailWhenGivenExcludedNonIdField,
+ "Invariant failure elem.trueValue()") {
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
+ inclusion.parse(BSON("a" << false));
+}
+
+DEATH_TEST(InclusionProjection,
+ ShouldFailWhenGivenIncludedIdSubfield,
+ "Invariant failure elem.trueValue()") {
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
+ inclusion.parse(BSON("_id.id1" << false));
+}
+
+TEST(InclusionProjection, ShouldThrowWhenParsingInvalidExpression) {
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
ASSERT_THROWS(inclusion.parse(BSON("a" << BSON("$gt" << BSON("bad"
<< "arguments")))),
AssertionException);
}
TEST(InclusionProjection, ShouldRejectProjectionWithNoOutputFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
ASSERT_THROWS(inclusion.parse(BSON("_id" << false)), AssertionException);
}
TEST(InclusionProjection, ShouldAddIncludedFieldsToDependencies) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("_id" << false << "a" << true << "x.y" << true));
DepsTracker deps;
@@ -81,8 +106,7 @@ TEST(InclusionProjection, ShouldAddIncludedFieldsToDependencies) {
}
TEST(InclusionProjection, ShouldAddIdToDependenciesIfNotSpecified) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true));
DepsTracker deps;
@@ -94,8 +118,7 @@ TEST(InclusionProjection, ShouldAddIdToDependenciesIfNotSpecified) {
}
TEST(InclusionProjection, ShouldAddDependenciesOfComputedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a"
<< "$a"
<< "x"
@@ -111,8 +134,7 @@ TEST(InclusionProjection, ShouldAddDependenciesOfComputedFields) {
}
TEST(InclusionProjection, ShouldAddPathToDependenciesForNestedComputedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("x.y"
<< "$z"));
@@ -129,8 +151,7 @@ TEST(InclusionProjection, ShouldAddPathToDependenciesForNestedComputedFields) {
}
TEST(InclusionProjection, ShouldSerializeToEquivalentProjection) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(fromjson("{a: {$add: ['$a', 2]}, b: {d: 3}, 'x.y': {$literal: 4}}"));
// Adds implicit "_id" inclusion, converts numbers to bools, serializes expressions.
@@ -148,8 +169,7 @@ TEST(InclusionProjection, ShouldSerializeToEquivalentProjection) {
}
TEST(InclusionProjection, ShouldSerializeExplicitExclusionOfId) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("_id" << false << "a" << true));
// Adds implicit "_id" inclusion, converts numbers to bools, serializes expressions.
@@ -167,8 +187,7 @@ TEST(InclusionProjection, ShouldSerializeExplicitExclusionOfId) {
TEST(InclusionProjection, ShouldOptimizeTopLevelExpressions) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2))));
inclusion.optimize();
@@ -186,8 +205,7 @@ TEST(InclusionProjection, ShouldOptimizeTopLevelExpressions) {
}
TEST(InclusionProjection, ShouldOptimizeNestedExpressions) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b" << BSON("$add" << BSON_ARRAY(1 << 2))));
inclusion.optimize();
@@ -206,8 +224,7 @@ TEST(InclusionProjection, ShouldOptimizeNestedExpressions) {
}
TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModified) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON(
"a" << wrapInLiteral("computedVal") << "b.c" << wrapInLiteral("computedVal") << "d" << true
<< "e.f"
@@ -226,8 +243,7 @@ TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModified) {
}
TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModifiedWithIdExclusion) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("_id" << false << "a" << wrapInLiteral("computedVal") << "b.c"
<< wrapInLiteral("computedVal")
<< "d"
@@ -254,8 +270,7 @@ TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModifiedWith
//
TEST(InclusionProjectionExecutionTest, ShouldIncludeTopLevelField) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true));
// More than one field in document.
@@ -280,8 +295,7 @@ TEST(InclusionProjectionExecutionTest, ShouldIncludeTopLevelField) {
}
TEST(InclusionProjectionExecutionTest, ShouldAddComputedTopLevelField) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("newField" << wrapInLiteral("computedVal")));
auto result = inclusion.applyProjection(Document{});
auto expectedResult = Document{{"newField", "computedVal"_sd}};
@@ -294,8 +308,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAddComputedTopLevelField) {
}
TEST(InclusionProjectionExecutionTest, ShouldApplyBothInclusionsAndComputedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "newField" << wrapInLiteral("computedVal")));
auto result = inclusion.applyProjection(Document{{"a", 1}});
auto expectedResult = Document{{"a", 1}, {"newField", "computedVal"_sd}};
@@ -303,8 +316,7 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyBothInclusionsAndComputedField
}
TEST(InclusionProjectionExecutionTest, ShouldIncludeFieldsInOrderOfInputDoc) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("first" << true << "second" << true << "third" << true));
auto inputDoc = Document{{"second", 1}, {"first", 0}, {"third", 2}};
auto result = inclusion.applyProjection(inputDoc);
@@ -312,8 +324,7 @@ TEST(InclusionProjectionExecutionTest, ShouldIncludeFieldsInOrderOfInputDoc) {
}
TEST(InclusionProjectionExecutionTest, ShouldApplyComputedFieldsInOrderSpecified) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("firstComputed" << wrapInLiteral("FIRST") << "secondComputed"
<< wrapInLiteral("SECOND")));
auto result = inclusion.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}});
@@ -322,8 +333,7 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyComputedFieldsInOrderSpecified
}
TEST(InclusionProjectionExecutionTest, ShouldImplicitlyIncludeId) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true));
auto result = inclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}});
auto expectedResult = Document{{"_id", "ID"_sd}, {"a", 1}};
@@ -336,8 +346,7 @@ TEST(InclusionProjectionExecutionTest, ShouldImplicitlyIncludeId) {
}
TEST(InclusionProjectionExecutionTest, ShouldImplicitlyIncludeIdWithComputedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("newField" << wrapInLiteral("computedVal")));
auto result = inclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}});
auto expectedResult = Document{{"_id", "ID"_sd}, {"newField", "computedVal"_sd}};
@@ -345,8 +354,7 @@ TEST(InclusionProjectionExecutionTest, ShouldImplicitlyIncludeIdWithComputedFiel
}
TEST(InclusionProjectionExecutionTest, ShouldIncludeIdIfExplicitlyIncluded) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "_id" << true << "b" << true));
auto result =
inclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}, {"c", 3}});
@@ -355,8 +363,7 @@ TEST(InclusionProjectionExecutionTest, ShouldIncludeIdIfExplicitlyIncluded) {
}
TEST(InclusionProjectionExecutionTest, ShouldExcludeIdIfExplicitlyExcluded) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "_id" << false));
auto result = inclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}});
auto expectedResult = Document{{"a", 1}};
@@ -364,8 +371,7 @@ TEST(InclusionProjectionExecutionTest, ShouldExcludeIdIfExplicitlyExcluded) {
}
TEST(InclusionProjectionExecutionTest, ShouldReplaceIdWithComputedId) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("_id" << wrapInLiteral("newId")));
auto result = inclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}});
auto expectedResult = Document{{"_id", "newId"_sd}};
@@ -377,8 +383,7 @@ TEST(InclusionProjectionExecutionTest, ShouldReplaceIdWithComputedId) {
//
TEST(InclusionProjectionExecutionTest, ShouldIncludeSimpleDottedFieldFromSubDoc) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b" << true));
// More than one field in sub document.
@@ -403,8 +408,7 @@ TEST(InclusionProjectionExecutionTest, ShouldIncludeSimpleDottedFieldFromSubDoc)
}
TEST(InclusionProjectionExecutionTest, ShouldNotCreateSubDocIfDottedIncludedFieldDoesNotExist) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("sub.target" << true));
// Should not add the path if it doesn't exist.
@@ -419,8 +423,7 @@ TEST(InclusionProjectionExecutionTest, ShouldNotCreateSubDocIfDottedIncludedFiel
}
TEST(InclusionProjectionExecutionTest, ShouldApplyDottedInclusionToEachElementInArray) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b" << true));
vector<Value> nestedValues = {Value(1),
@@ -444,8 +447,7 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyDottedInclusionToEachElementIn
}
TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToSubDocument) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("sub.target" << wrapInLiteral("computedVal")));
// Other fields exist in sub document, one of which is the specified field.
@@ -465,8 +467,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToSubDocument
}
TEST(InclusionProjectionExecutionTest, ShouldCreateSubDocIfDottedComputedFieldDoesntExist) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("sub.target" << wrapInLiteral("computedVal")));
// Should add the path if it doesn't exist.
@@ -480,8 +481,7 @@ TEST(InclusionProjectionExecutionTest, ShouldCreateSubDocIfDottedComputedFieldDo
}
TEST(InclusionProjectionExecutionTest, ShouldCreateNestedSubDocumentsAllTheWayToComputedField) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b.c.d" << wrapInLiteral("computedVal")));
// Should add the path if it doesn't exist.
@@ -496,8 +496,7 @@ TEST(InclusionProjectionExecutionTest, ShouldCreateNestedSubDocumentsAllTheWayTo
}
TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToEachElementInArray) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED")));
vector<Value> nestedValues = {Value(1),
@@ -520,8 +519,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToEachElement
}
TEST(InclusionProjectionExecutionTest, ShouldApplyInclusionsAndAdditionsToEachElementInArray) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.inc" << true << "a.comp" << wrapInLiteral("COMPUTED")));
vector<Value> nestedValues = {Value(1),
@@ -548,8 +546,7 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyInclusionsAndAdditionsToEachEl
}
TEST(InclusionProjectionExecutionTest, ShouldAddOrIncludeSubFieldsOfId) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("_id.X" << true << "_id.Z" << wrapInLiteral("NEW")));
auto result = inclusion.applyProjection(Document{{"_id", Document{{"X", 1}, {"Y", 2}}}});
auto expectedResult = Document{{"_id", Document{{"X", 1}, {"Z", "NEW"_sd}}}};
@@ -557,8 +554,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAddOrIncludeSubFieldsOfId) {
}
TEST(InclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
// Include all of "a.b", "a.c", "a.d", and "a.e".
// Add new computed fields "a.W", "a.X", "a.Y", and "a.Z".
inclusion.parse(BSON(
@@ -582,8 +578,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) {
}
TEST(InclusionProjectionExecutionTest, ShouldApplyNestedComputedFieldsInOrderSpecified) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << wrapInLiteral("FIRST") << "b.c" << wrapInLiteral("SECOND")));
auto result = inclusion.applyProjection(Document{});
auto expectedResult = Document{{"a", "FIRST"_sd}, {"b", Document{{"c", "SECOND"_sd}}}};
@@ -591,8 +586,7 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyNestedComputedFieldsInOrderSpe
}
TEST(InclusionProjectionExecutionTest, ShouldApplyComputedFieldsAfterAllInclusions) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("b.c" << wrapInLiteral("NEW") << "a" << true));
auto result = inclusion.applyProjection(Document{{"a", 1}});
auto expectedResult = Document{{"a", 1}, {"b", Document{{"c", "NEW"_sd}}}};
@@ -611,8 +605,7 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyComputedFieldsAfterAllInclusio
}
TEST(InclusionProjectionExecutionTest, ComputedFieldReplacingExistingShouldAppearAfterInclusions) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("b" << wrapInLiteral("NEW") << "a" << true));
auto result = inclusion.applyProjection(Document{{"b", 1}, {"a", 1}});
auto expectedResult = Document{{"a", 1}, {"b", "NEW"_sd}};
@@ -623,12 +616,252 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldReplacingExistingShouldAppea
}
//
+// _id inclusion policy.
+//
+
+TEST(InclusionProjectionExecutionTest, ShouldIncludeIdByDefault) {
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
+ inclusion.parse(BSON("a" << true));
+
+ auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"_id", 2}, {"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldIncludeIdWithIncludePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("a" << true));
+
+ auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"_id", 2}, {"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldExcludeIdWithExcludePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kExcludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("a" << true));
+
+ auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitExcludeIdSpec) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("_id" << false << "a" << true));
+
+ auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIncludeIdSpec) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kExcludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("_id" << true << "a" << true));
+
+ auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}});
+ auto expectedResult = Document{{"_id", 2}, {"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefaultIncludePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("_id.id1" << true << "a" << true));
+
+ auto result = inclusion.applyProjection(
+ Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}});
+ auto expectedResult = Document{{"_id", Document{{"id1", 1}}}, {"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefaultExcludePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kExcludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("_id.id1" << true << "a" << true));
+
+ auto result = inclusion.applyProjection(
+ Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}});
+ auto expectedResult = Document{{"_id", Document{{"id1", 1}}}, {"a", 3}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+//
+// Nested array recursion.
+//
+
+TEST(InclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) {
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
+ inclusion.parse(BSON("a.b" << true));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, [{b: 4}], {}]}
+ auto result = inclusion.applyProjection(
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}});
+
+ auto expectedResult = Document{{"a",
+ vector<Value>{Value(),
+ Value(Document{{"b", 2}}),
+ Value(vector<Value>{Value(Document{{"b", 4}})}),
+ Value(Document{})}}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldRecurseNestedArraysForExplicitProRecursePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("a.b" << true));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, [{b: 4}], {}]}
+ auto result = inclusion.applyProjection(
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}});
+
+ auto expectedResult = Document{{"a",
+ vector<Value>{Value(),
+ Value(Document{{"b", 2}}),
+ Value(vector<Value>{Value(Document{{"b", 4}})}),
+ Value(Document{})}}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecursePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays);
+ inclusion.parse(BSON("a.b" << true));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, {}]}
+ auto result = inclusion.applyProjection(
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}});
+
+ auto expectedResult = Document{
+ {"a", vector<Value>{Value(), Value(Document{{"b", 2}}), Value(), Value(Document{})}}};
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ShouldRetainNestedArraysIfNoRecursionNeeded) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays);
+ inclusion.parse(BSON("a" << true));
+
+ // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => [output doc identical to input]
+ const auto inputDoc =
+ Document{{"a",
+ vector<Value>{Value(1),
+ Value(Document{{"b", 2}, {"c", 3}}),
+ Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}),
+ Value(Document{{"d", 6}})}}};
+
+ auto result = inclusion.applyProjection(inputDoc);
+ const auto& expectedResult = inputDoc;
+
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ComputedFieldIsAddedToNestedArrayElementsForRecursePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kRecurseNestedArrays);
+ inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED")));
+
+ vector<Value> nestedValues = {Value(1),
+ Value(Document{}),
+ Value(Document{{"b", 1}}),
+ Value(Document{{"b", 1}, {"c", 2}}),
+ Value(vector<Value>{}),
+ Value(vector<Value>{Value(1), Value(Document{{"c", 1}})})};
+ vector<Value> expectedNestedValues = {
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(vector<Value>{}),
+ Value(vector<Value>{Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}})})};
+ auto result = inclusion.applyProjection(Document{{"a", nestedValues}});
+ auto expectedResult = Document{{"a", expectedNestedValues}};
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+TEST(InclusionProjectionExecutionTest, ComputedFieldShouldReplaceNestedArrayForNoRecursePolicy) {
+ const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ ParsedInclusionProjection inclusion(expCtx,
+ ProjectionDefaultIdPolicy::kIncludeId,
+ ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays);
+ inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED")));
+
+ // For kRecurseNestedArrays, the computed field (1) replaces any scalar values in the array with
+ // a subdocument containing the new field, and (2) is added to each element of the array and all
+ // nested arrays individually. With kDoNotRecurseNestedArrays, the nested arrays are replaced
+ // rather than being traversed, in exactly the same way as scalar values.
+ vector<Value> nestedValues = {Value(1),
+ Value(Document{}),
+ Value(Document{{"b", 1}}),
+ Value(Document{{"b", 1}, {"c", 2}}),
+ Value(vector<Value>{}),
+ Value(vector<Value>{Value(1), Value(Document{{"c", 1}})})};
+
+ vector<Value> expectedNestedValues = {Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}}),
+ Value(Document{{"b", "COMPUTED"_sd}})};
+
+ auto result = inclusion.applyProjection(Document{{"a", nestedValues}});
+ auto expectedResult = Document{{"a", expectedNestedValues}};
+ ASSERT_DOCUMENT_EQ(result, expectedResult);
+}
+
+//
// Misc.
//
TEST(InclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true));
MutableDocument inputDocBuilder(Document{{"a", 1}});
@@ -648,8 +881,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc)
//
TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalProjection) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "b" << true));
auto proj = BSON("_id" << false << "a" << true << "b" << true);
@@ -658,8 +890,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalProjection) {
}
TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjection) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "b" << true));
auto proj = BSON("_id" << false << "a" << true << "b" << true << "c" << true);
@@ -668,8 +899,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjection) {
}
TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalNestedProjection) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b" << true));
auto proj = BSON("_id" << false << "a.b" << true);
@@ -678,8 +908,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalNestedProjecti
}
TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjectionWithNestedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "c" << BSON("d" << true)));
auto proj = BSON("_id" << false << "a" << true << "b" << true << "c.d" << true);
@@ -688,8 +917,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjectionWithN
}
TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissingFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "b" << true));
auto proj = BSON("_id" << false << "a" << true);
@@ -701,8 +929,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissin
TEST(InclusionProjectionSubsetTest,
ShouldDetectNonSubsetForSupersetProjectionWithoutComputedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true << "b" << true << "c" << BSON("$literal" << 1)));
auto proj = BSON("_id" << false << "a" << true << "b" << true);
@@ -711,8 +938,7 @@ TEST(InclusionProjectionSubsetTest,
}
TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissingNestedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a.b" << true << "a.c" << true));
auto proj = BSON("_id" << false << "a.b" << true);
@@ -721,8 +947,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissin
}
TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithRenamedFields) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a"
<< "$b"));
@@ -732,8 +957,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithRename
}
TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissingIdField) {
- const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- ParsedInclusionProjection inclusion(expCtx);
+ auto inclusion = makeInclusionProjectionWithDefaultPolicies();
inclusion.parse(BSON("a" << true));
auto proj = BSON("a" << true);