summaryrefslogtreecommitdiff
path: root/src/mongo/db/exec
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/exec')
-rw-r--r--src/mongo/db/exec/SConscript1
-rw-r--r--src/mongo/db/exec/add_fields_projection_executor.h6
-rw-r--r--src/mongo/db/exec/bucket_unpacker.cpp28
-rw-r--r--src/mongo/db/exec/bucket_unpacker.h17
-rw-r--r--src/mongo/db/exec/bucket_unpacker_test.cpp238
-rw-r--r--src/mongo/db/exec/collection_scan.cpp41
-rw-r--r--src/mongo/db/exec/collection_scan_common.h12
-rw-r--r--src/mongo/db/exec/document_value/document.cpp11
-rw-r--r--src/mongo/db/exec/document_value/document.h7
-rw-r--r--src/mongo/db/exec/document_value/document_internal.h9
-rw-r--r--src/mongo/db/exec/document_value/document_value_test.cpp10
-rw-r--r--src/mongo/db/exec/document_value/document_value_test_util.h7
-rw-r--r--src/mongo/db/exec/document_value/value.cpp19
-rw-r--r--src/mongo/db/exec/document_value/value.h9
-rw-r--r--src/mongo/db/exec/exclusion_projection_executor.cpp7
-rw-r--r--src/mongo/db/exec/exclusion_projection_executor.h21
-rw-r--r--src/mongo/db/exec/fetch.cpp6
-rw-r--r--src/mongo/db/exec/inclusion_projection_executor.cpp9
-rw-r--r--src/mongo/db/exec/inclusion_projection_executor.h34
-rw-r--r--src/mongo/db/exec/index_scan.cpp4
-rw-r--r--src/mongo/db/exec/or.cpp6
-rw-r--r--src/mongo/db/exec/projection.h1
-rw-r--r--src/mongo/db/exec/projection_executor.h15
-rw-r--r--src/mongo/db/exec/projection_executor_builder.cpp2
-rw-r--r--src/mongo/db/exec/projection_executor_builder.h1
-rw-r--r--src/mongo/db/exec/projection_executor_redaction_test.cpp208
-rw-r--r--src/mongo/db/exec/projection_node.cpp26
-rw-r--r--src/mongo/db/exec/projection_node.h18
-rw-r--r--src/mongo/db/exec/sbe/stages/hash_agg.cpp28
-rw-r--r--src/mongo/db/exec/sbe/stages/hash_agg.h3
-rw-r--r--src/mongo/db/exec/sbe/stages/hash_lookup.cpp54
-rw-r--r--src/mongo/db/exec/sbe/stages/hash_lookup.h17
-rw-r--r--src/mongo/db/exec/sbe/util/spilling.cpp163
-rw-r--r--src/mongo/db/exec/sbe/util/spilling.h137
-rw-r--r--src/mongo/db/exec/sbe/values/slot.cpp24
-rw-r--r--src/mongo/db/exec/sbe/values/value_builder.h14
-rw-r--r--src/mongo/db/exec/sbe/values/value_serialization_test.cpp13
-rw-r--r--src/mongo/db/exec/sbe/vm/arith.cpp2
-rw-r--r--src/mongo/db/exec/skip.cpp8
-rw-r--r--src/mongo/db/exec/skip.h9
-rw-r--r--src/mongo/db/exec/text_or.cpp4
-rw-r--r--src/mongo/db/exec/upsert_stage.cpp2
42 files changed, 908 insertions, 343 deletions
diff --git a/src/mongo/db/exec/SConscript b/src/mongo/db/exec/SConscript
index eac1c6a3105..fb803f2cb53 100644
--- a/src/mongo/db/exec/SConscript
+++ b/src/mongo/db/exec/SConscript
@@ -130,6 +130,7 @@ env.CppUnitTest(
"find_projection_executor_test.cpp",
"inclusion_projection_executor_test.cpp",
"projection_executor_builder_test.cpp",
+ "projection_executor_redaction_test.cpp",
"projection_executor_test.cpp",
"projection_executor_utils_test.cpp",
"projection_executor_wildcard_access_test.cpp",
diff --git a/src/mongo/db/exec/add_fields_projection_executor.h b/src/mongo/db/exec/add_fields_projection_executor.h
index 12f7bbfe19b..b1349d715d6 100644
--- a/src/mongo/db/exec/add_fields_projection_executor.h
+++ b/src/mongo/db/exec/add_fields_projection_executor.h
@@ -94,9 +94,9 @@ public:
*/
void parse(const BSONObj& spec);
- Document serializeTransformation(
- boost::optional<ExplainOptions::Verbosity> explain) const final {
- return _root->serialize(explain);
+ Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain,
+ const SerializationOptions& options = {}) const final {
+ return _root->serialize(explain, options);
}
/**
diff --git a/src/mongo/db/exec/bucket_unpacker.cpp b/src/mongo/db/exec/bucket_unpacker.cpp
index db14d8b08cc..8bba2da9e4d 100644
--- a/src/mongo/db/exec/bucket_unpacker.cpp
+++ b/src/mongo/db/exec/bucket_unpacker.cpp
@@ -986,9 +986,9 @@ BSONObj BucketSpec::pushdownPredicate(
BSONObjBuilder result;
if (metaOnlyPredicate)
- metaOnlyPredicate->serialize(&result);
+ metaOnlyPredicate->serialize(&result, {});
if (bucketMetricPredicate)
- bucketMetricPredicate->serialize(&result);
+ bucketMetricPredicate->serialize(&result, {});
return result.obj();
}
@@ -1546,7 +1546,9 @@ BSONObj BucketUnpacker::getNextBson() {
// Add computed meta projections.
for (auto&& name : _spec.computedMetaProjFields()) {
- builder.appendAs(_computedMetaProjections[name], name);
+ if (_computedMetaProjections[name]) {
+ builder.appendAs(_computedMetaProjections[name], name);
+ }
}
return builder.obj();
@@ -1757,12 +1759,18 @@ void BucketUnpacker::eraseMetaFromFieldSetAndDetermineIncludeMeta() {
}
}
-void BucketUnpacker::eraseExcludedComputedMetaProjFields() {
- if (_spec.behavior() == BucketSpec::Behavior::kExclude) {
- for (const auto& field : _spec.fieldSet()) {
- _spec.eraseFromComputedMetaProjFields(field);
- }
- }
+void BucketUnpacker::eraseUnneededComputedMetaProjFields() {
+ // If this is an inclusion spec and the current computed field is not part of in the include
+ // fields, it means the computed field should not be available after the current unpack stage.
+ // Similarly, for exclusion spec, if the current computed field is part of the exclude fields,
+ // the computed fields should not be available after the current unpack stage. This can happen
+ // if there was a $project stage after a $addFields stage.
+ bool removeIfInFieldSet = _spec.behavior() == BucketSpec::Behavior::kExclude;
+ auto conditionToErase = [&](const std::string& computedField) {
+ bool inFieldSet = _spec.fieldSet().find(computedField) != _spec.fieldSet().end();
+ return inFieldSet == removeIfInFieldSet;
+ };
+ _spec.eraseIfPredTrueFromComputedMetaProjFields(conditionToErase);
}
void BucketUnpacker::setBucketSpec(BucketSpec&& bucketSpec) {
@@ -1770,7 +1778,7 @@ void BucketUnpacker::setBucketSpec(BucketSpec&& bucketSpec) {
eraseMetaFromFieldSetAndDetermineIncludeMeta();
determineIncludeTimeField();
- eraseExcludedComputedMetaProjFields();
+ eraseUnneededComputedMetaProjFields();
_includeMinTimeAsMetadata = _spec.includeMinTimeAsMetadata;
_includeMaxTimeAsMetadata = _spec.includeMaxTimeAsMetadata;
diff --git a/src/mongo/db/exec/bucket_unpacker.h b/src/mongo/db/exec/bucket_unpacker.h
index 29f2f1f30d2..3c63d6abe49 100644
--- a/src/mongo/db/exec/bucket_unpacker.h
+++ b/src/mongo/db/exec/bucket_unpacker.h
@@ -115,8 +115,16 @@ public:
return _computedMetaProjFields;
}
- void eraseFromComputedMetaProjFields(const std::string& field) {
- _computedMetaProjFields.erase(field);
+ // Remove fields that the predicate function evaluates to true for.
+ void eraseIfPredTrueFromComputedMetaProjFields(const std::function<bool(std::string)> pred) {
+ auto it = _computedMetaProjFields.begin();
+ while (it != _computedMetaProjFields.end()) {
+ if (pred(*it)) {
+ it = _computedMetaProjFields.erase(it);
+ } else {
+ ++it;
+ }
+ }
}
void setUsesExtendedRange(bool usesExtendedRange) {
@@ -374,8 +382,9 @@ private:
// included in the materialized measurements.
void eraseMetaFromFieldSetAndDetermineIncludeMeta();
- // Erase computed meta projection fields if they are present in the exclusion field set.
- void eraseExcludedComputedMetaProjFields();
+ // Erase computed meta projection fields if they are present in the exclusion field set or if
+ // they are not present in the inclusion set.
+ void eraseUnneededComputedMetaProjFields();
BucketSpec _spec;
diff --git a/src/mongo/db/exec/bucket_unpacker_test.cpp b/src/mongo/db/exec/bucket_unpacker_test.cpp
index 9eecea624c5..4a638467bea 100644
--- a/src/mongo/db/exec/bucket_unpacker_test.cpp
+++ b/src/mongo/db/exec/bucket_unpacker_test.cpp
@@ -170,6 +170,11 @@ public:
}
return root.obj();
}
+
+ bool computedMetaProjFieldsContainsField(std::set<std::string>& computedMetaProjFields,
+ std::string field) {
+ return computedMetaProjFields.find(field) != computedMetaProjFields.end();
+ }
};
TEST_F(BucketUnpackerTest, UnpackBasicIncludeAllMeasurementFields) {
@@ -178,7 +183,7 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeAllMeasurementFields) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto unpacker = makeBucketUnpacker(std::move(fields),
@@ -188,11 +193,12 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeAllMeasurementFields) {
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")});
+ assertGetNext(
+ unpacker,
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")});
ASSERT_FALSE(unpacker.hasNext());
}
@@ -201,7 +207,7 @@ TEST_F(BucketUnpackerTest, ExcludeASingleField) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto test = [&](BSONObj bucket) {
@@ -211,12 +217,14 @@ TEST_F(BucketUnpackerTest, ExcludeASingleField) {
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
+ assertGetNext(
+ unpacker,
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, a: 2}")});
+ assertGetNext(
+ unpacker,
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, a: 2}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -229,7 +237,7 @@ TEST_F(BucketUnpackerTest, EmptyIncludeGetsEmptyMeasurements) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto test = [&](BSONObj bucket) {
@@ -255,7 +263,7 @@ TEST_F(BucketUnpackerTest, EmptyExcludeMaterializesAllFields) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto test = [&](BSONObj bucket) {
@@ -264,13 +272,14 @@ TEST_F(BucketUnpackerTest, EmptyExcludeMaterializesAllFields) {
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
+ assertGetNext(
+ unpacker,
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(
unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")});
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -283,7 +292,7 @@ TEST_F(BucketUnpackerTest, SparseColumnsWhereOneColumnIsExhaustedBeforeTheOther)
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1}, b:{'1':1}}}");
auto test = [&](BSONObj bucket) {
@@ -292,11 +301,13 @@ TEST_F(BucketUnpackerTest, SparseColumnsWhereOneColumnIsExhaustedBeforeTheOther)
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
+ assertGetNext(
+ unpacker,
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, b: 1}")});
+ assertGetNext(
+ unpacker,
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, b: 1}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -310,7 +321,7 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeWithDollarPrefix) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"$a:{'0':1, '1':2}, b:{'1':1}}}");
auto test = [&](BSONObj bucket) {
@@ -319,13 +330,14 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeWithDollarPrefix) {
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, $a: 1}")});
-
- ASSERT_TRUE(unpacker.hasNext());
assertGetNext(
unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, $a: 2, b: 1}")});
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, $a: 1}")});
+
+ ASSERT_TRUE(unpacker.hasNext());
+ assertGetNext(unpacker,
+ Document{fromjson(
+ "{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, $a: 2, b: 1}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -338,7 +350,7 @@ TEST_F(BucketUnpackerTest, BucketsWithMetadataOnly) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}}}");
+ "time: {'0':Date(1), '1':Date(2)}}}");
auto test = [&](BSONObj bucket) {
auto unpacker = makeBucketUnpacker(fields,
@@ -347,10 +359,10 @@ TEST_F(BucketUnpackerTest, BucketsWithMetadataOnly) {
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1}")});
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1}")});
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2}")});
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -363,8 +375,8 @@ TEST_F(BucketUnpackerTest, UnorderedRowKeysDoesntAffectMaterialization) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'1':1, '0':2, '2': "
- "3}, time: {'1':1, '0': 2, "
- "'2': 3}}}");
+ "3}, time: {'1':Date(1), '0': Date(2), "
+ "'2': Date(3)}}}");
auto test = [&](BSONObj bucket) {
auto unpacker = makeBucketUnpacker(fields,
@@ -373,15 +385,15 @@ TEST_F(BucketUnpackerTest, UnorderedRowKeysDoesntAffectMaterialization) {
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(unpacker,
- Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1}")});
+ Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1}")});
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(unpacker,
- Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2}")});
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2}")});
ASSERT_TRUE(unpacker.hasNext());
assertGetNext(unpacker,
- Document{fromjson("{time: 3, myMeta: {m1: 999, m2: 9999}, _id: 3}")});
+ Document{fromjson("{time: Date(3), myMeta: {m1: 999, m2: 9999}, _id: 3}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -393,8 +405,9 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadata) {
std::set<std::string> fields{};
auto bucket = fromjson(
- "{control: {'version': 1}, data: {_id: {'0':1, '1':2, '2': 3}, time: {'0':1, '1': 2, '2': "
- "3}}}");
+ "{control: {'version': 1}, data: {_id: {'0':1, '1':2, '2': 3}, time: {'0':Date(1), '1': "
+ "Date(2), '2': "
+ "Date(3)}}}");
auto test = [&](BSONObj bucket) {
auto unpacker = makeBucketUnpacker(fields,
@@ -402,13 +415,13 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadata) {
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 1, _id: 1}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(1), _id: 1}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 2, _id: 2}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(2), _id: 2}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 3, _id: 3}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(3), _id: 3}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -420,8 +433,9 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadataUnorderedKey
std::set<std::string> fields{};
auto bucket = fromjson(
- "{control: {'version': 1}, data: {_id: {'1':1, '0':2, '2': 3}, time: {'1':1, '0': 2, '2': "
- "3}}}");
+ "{control: {'version': 1}, data: {_id: {'1':1, '0':2, '2': 3}, time: {'1':Date(1), '0': "
+ "Date(2), '2': "
+ "Date(3)}}}");
auto test = [&](BSONObj bucket) {
auto unpacker = makeBucketUnpacker(fields,
@@ -429,13 +443,13 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadataUnorderedKey
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 1, _id: 1}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(1), _id: 1}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 2, _id: 2}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(2), _id: 2}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 3, _id: 3}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(3), _id: 3}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -448,8 +462,8 @@ TEST_F(BucketUnpackerTest, ExcludedMetaFieldDoesntMaterializeMetadataWhenBucketH
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2, '2': "
- "3}, time: {'0':1, '1': 2, "
- "'2': 3}}}");
+ "3}, time: {'0':Date(1), '1': Date(2), "
+ "'2': Date(3)}}}");
auto test = [&](BSONObj bucket) {
auto unpacker = makeBucketUnpacker(fields,
@@ -457,13 +471,13 @@ TEST_F(BucketUnpackerTest, ExcludedMetaFieldDoesntMaterializeMetadataWhenBucketH
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 1, _id: 1}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(1), _id: 1}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 2, _id: 2}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(2), _id: 2}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 3, _id: 3}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(3), _id: 3}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -476,7 +490,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnUndefinedMeta) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: undefined, data: {_id: {'0':1, '1':2, '2': 3}, time: "
- "{'0':1, '1': 2, '2': 3}}}");
+ "{'0':Date(1), '1': Date(2), '2': Date(3)}}}");
auto test = [&](BSONObj bucket) {
assertUnpackerThrowsCode(fields,
@@ -495,8 +509,8 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnUnexpectedMeta) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2, '2': "
- "3}, time: {'0':1, '1': 2, "
- "'2': 3}}}");
+ "3}, time: {'0':Date(1), '1': Date(2), "
+ "'2': Date(3)}}}");
auto test = [&](BSONObj bucket) {
assertUnpackerThrowsCode(fields,
@@ -514,8 +528,9 @@ TEST_F(BucketUnpackerTest, NullMetaInBucketMaterializesAsNull) {
std::set<std::string> fields{};
auto bucket = fromjson(
- "{control: {'version': 1}, meta: null, data: {_id: {'0':4, '1':5, '2':6}, time: {'0':4, "
- "'1': 5, '2': 6}}}");
+ "{control: {'version': 1}, meta: null, data: {_id: {'0':4, '1':5, '2':6}, time: "
+ "{'0':Date(4), "
+ "'1': Date(5), '2': Date(6)}}}");
auto test = [&](BSONObj bucket) {
auto unpacker = makeBucketUnpacker(fields,
@@ -523,13 +538,13 @@ TEST_F(BucketUnpackerTest, NullMetaInBucketMaterializesAsNull) {
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 4, myMeta: null, _id: 4}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(4), myMeta: null, _id: 4}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 5, myMeta: null, _id: 5}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(5), myMeta: null, _id: 5}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 6, myMeta: null, _id: 6}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(6), myMeta: null, _id: 6}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -545,7 +560,7 @@ TEST_F(BucketUnpackerTest, GetNextHandlesMissingMetaInBucket) {
control: {version: 1},
data: {
_id: {'0':4, '1':5, '2':6},
- time: {'0':4, '1': 5, '2': 6}
+ time: {'0':Date(4), '1': Date(5), '2': Date(6)}
}
})");
@@ -555,13 +570,13 @@ TEST_F(BucketUnpackerTest, GetNextHandlesMissingMetaInBucket) {
std::move(bucket),
kUserDefinedMetaName.toString());
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 4, _id: 4}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(4), _id: 4}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 5, _id: 5}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(5), _id: 5}")});
ASSERT_TRUE(unpacker.hasNext());
- assertGetNext(unpacker, Document{fromjson("{time: 6, _id: 6}")});
+ assertGetNext(unpacker, Document{fromjson("{time: Date(6), _id: 6}")});
ASSERT_FALSE(unpacker.hasNext());
};
@@ -606,7 +621,7 @@ TEST_F(BucketUnpackerTest, EraseMetaFromFieldSetAndDetermineIncludeMeta) {
control: {version: 1},
data: {
_id: {'0':4, '1':5, '2':6},
- time: {'0':4, '1': 5, '2': 6}
+ time: {'0':Date(4), '1': Date(5), '2': Date(6)}
}
})");
auto unpacker = makeBucketUnpacker(empFields,
@@ -659,13 +674,83 @@ TEST_F(BucketUnpackerTest, EraseMetaFromFieldSetAndDetermineIncludeMeta) {
ASSERT_FALSE(unpacker.includeMetaField());
}
+TEST_F(BucketUnpackerTest, EraseUnneededComputedMetaProjFieldsWithInclusiveProject) {
+ auto bucket = fromjson(R"(
+{
+ control: {version: 1},
+ data: {
+ _id: {'0':4, '1':5, '2':6},
+ time: {'0':Date(4), '1': Date(5), '2': Date(6)}
+ }
+})");
+ std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()};
+ auto unpacker = makeBucketUnpacker(unpackerFields,
+ BucketSpec::Behavior::kInclude,
+ std::move(bucket),
+ kUserDefinedMetaName.toString());
+
+ // Add fields to '_computedMetaProjFields'.
+ unpacker.addComputedMetaProjFields({"hello"_sd, "bye"_sd});
+ auto computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields();
+ ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello"));
+ ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye"));
+
+ auto spec = unpacker.bucketSpec();
+ std::set<std::string> includeFields{kUserDefinedTimeName.toString(), "bye"};
+ spec.setFieldSet(includeFields);
+ spec.setBehavior(BucketSpec::Behavior::kInclude);
+
+ // This calls eraseUnneededComputedMetaProjFields().
+ unpacker.setBucketSpec(std::move(spec));
+ computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields();
+ // As "hello" was not in the includes, it should be removed.
+ ASSERT_FALSE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello"));
+ // As "bye" was in the includes, it should still be in '_computedMetaProjFields'.
+ ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye"));
+}
+
+TEST_F(BucketUnpackerTest, EraseUnneededComputedMetaProjFieldsWithExclusiveProject) {
+ auto bucket = fromjson(R"(
+{
+ control: {version: 1},
+ data: {
+ _id: {'0':4, '1':5, '2':6},
+ time: {'0':Date(4), '1': Date(5), '2': Date(6)}
+ }
+})");
+ std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()};
+ auto unpacker = makeBucketUnpacker(unpackerFields,
+ BucketSpec::Behavior::kInclude,
+ std::move(bucket),
+ kUserDefinedMetaName.toString());
+
+ // Add fields to '_computedMetaProjFields'.
+ unpacker.addComputedMetaProjFields({"hello"_sd, "bye"_sd});
+ auto computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields();
+ ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello"));
+ ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye"));
+
+ auto spec = unpacker.bucketSpec();
+ std::set<std::string> excludeFields{kUserDefinedTimeName.toString(), "bye"};
+ spec.setFieldSet(excludeFields);
+ spec.setBehavior(BucketSpec::Behavior::kExclude);
+
+ // This calls eraseUnneededComputedMetaProjFields().
+ unpacker.setBucketSpec(std::move(spec));
+ computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields();
+ // As "hello" was not excluded, it should still exist.
+ ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello"));
+ // As "bye" was in the excludes, it should be removed from '_computedMetaProjFields'.
+ ASSERT_FALSE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye"));
+}
+
TEST_F(BucketUnpackerTest, DetermineIncludeTimeField) {
auto bucket = fromjson(R"(
{
control: {version: 1},
data: {
_id: {'0':4, '1':5, '2':6},
- time: {'0':4, '1': 5, '2': 6}
+ time: {'0':Date(4), '1': Date(5), '2': Date(6)}
}
})");
std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()};
@@ -900,7 +985,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountLess) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto compressedBucket =
@@ -913,8 +998,9 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountLess) {
std::move(modifiedCompressedBucket),
kUserDefinedMetaName.toString());
- auto doc0 = Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
- auto doc1 = Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")};
+ auto doc0 = Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
+ auto doc1 =
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")};
// 1 is reported when asking for numberOfMeasurements()
ASSERT_EQ(unpacker.numberOfMeasurements(), 1);
@@ -935,7 +1021,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMore) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto compressedBucket =
@@ -948,8 +1034,9 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMore) {
std::move(modifiedCompressedBucket),
kUserDefinedMetaName.toString());
- auto doc0 = Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
- auto doc1 = Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")};
+ auto doc0 = Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
+ auto doc1 =
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")};
ASSERT_EQ(unpacker.numberOfMeasurements(), 3);
ASSERT_DOCUMENT_EQ(unpacker.extractSingleMeasurement(0), doc0);
@@ -970,7 +1057,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMissing) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto compressedBucket =
@@ -983,8 +1070,9 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMissing) {
std::move(modifiedCompressedBucket),
kUserDefinedMetaName.toString());
- auto doc0 = Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
- auto doc1 = Document{fromjson("{time: 2, myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")};
+ auto doc0 = Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
+ auto doc1 =
+ Document{fromjson("{time: Date(2), myMeta: {m1: 999, m2: 9999}, _id: 2, a :2, b: 1}")};
// Missing count field will make the unpacker measure the number of time fields for an accurate
// count
@@ -1006,7 +1094,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchDataField) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto compressedBucket =
@@ -1020,7 +1108,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchDataField) {
std::move(modifiedCompressedBucket),
kUserDefinedMetaName.toString());
- auto doc0 = Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
+ auto doc0 = Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
ASSERT_EQ(unpacker.numberOfMeasurements(), 2);
ASSERT_DOCUMENT_EQ(unpacker.extractSingleMeasurement(0), doc0);
@@ -1041,7 +1129,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchTimeField) {
auto bucket = fromjson(
"{control: {'version': 1}, meta: {'m1': 999, 'm2': 9999}, data: {_id: {'0':1, '1':2}, "
- "time: {'0':1, '1':2}, "
+ "time: {'0':Date(1), '1':Date(2)}, "
"a:{'0':1, '1':2}, b:{'1':1}}}");
auto compressedBucket =
@@ -1055,7 +1143,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchTimeField) {
std::move(modifiedCompressedBucket),
kUserDefinedMetaName.toString());
- auto doc0 = Document{fromjson("{time: 1, myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
+ auto doc0 = Document{fromjson("{time: Date(1), myMeta: {m1: 999, m2: 9999}, _id: 1, a: 1}")};
ASSERT_EQ(unpacker.numberOfMeasurements(), 2);
ASSERT_DOCUMENT_EQ(unpacker.extractSingleMeasurement(0), doc0);
diff --git a/src/mongo/db/exec/collection_scan.cpp b/src/mongo/db/exec/collection_scan.cpp
index f8550dbe81d..877603a0965 100644
--- a/src/mongo/db/exec/collection_scan.cpp
+++ b/src/mongo/db/exec/collection_scan.cpp
@@ -275,19 +275,36 @@ void CollectionScan::setLatestOplogEntryTimestamp(const Record& record) {
}
void CollectionScan::assertTsHasNotFallenOffOplog(const Record& record) {
- // If the first entry we see in the oplog is the replset initialization, then it doesn't matter
- // if its timestamp is later than the timestamp that should not have fallen off the oplog; no
- // events earlier can have fallen off this oplog. Otherwise, verify that the timestamp of the
- // first observed oplog entry is earlier than or equal to timestamp that should not have fallen
- // off the oplog.
- auto oplogEntry = uassertStatusOK(repl::OplogEntry::parse(record.data.toBson()));
+ const auto oplogEntry = record.data.toBson();
+ const repl::OplogEntryParserNonStrict oplogEntryParser{oplogEntry};
invariant(_specificStats.docsTested == 0);
- const bool isNewRS =
- oplogEntry.getObject().binaryEqual(BSON("msg" << repl::kInitiatingSetMsg)) &&
- oplogEntry.getOpType() == repl::OpTypeEnum::kNoop;
+
+ // Indicates that 'oplogEntry' means initialization of a replica set.
+ bool isNewRS{false};
+
+ // Indicates that the timestamp of the observed oplog entry 'oplogEntry' is earlier than or
+ // equal to timestamp that should not have fallen off the oplog.
+ bool tsHasNotFallenOff{false};
+ try {
+ tsHasNotFallenOff =
+ oplogEntryParser.getOpTime().getTimestamp() <= *_params.assertTsHasNotFallenOffOplog;
+
+ // If the first entry we see in the oplog is the replset initialization, then it doesn't
+ // matter if its timestamp is later than the timestamp that should not have fallen off the
+ // oplog; no events earlier can have fallen off this oplog.
+ // NOTE: A change collection can be created at any moment as such it might not have replset
+ // initialization message, as such this case is not fully applicable for the change
+ // collection.
+ isNewRS = oplogEntryParser.getOpType() == repl::OpTypeEnum::kNoop &&
+ oplogEntryParser.getObject().binaryEqual(BSON("msg" << repl::kInitiatingSetMsg));
+ } catch (const AssertionException& exception) {
+ uasserted(8881102,
+ str::stream() << "Failed to parse the oldest oplog entry" << causedBy(exception));
+ }
uassert(ErrorCodes::OplogQueryMinTsMissing,
"Specified timestamp has already fallen off the oplog",
- isNewRS || oplogEntry.getTimestamp() <= *_params.assertTsHasNotFallenOffOplog);
+ isNewRS || tsHasNotFallenOff);
+
// We don't need to check this assertion again after we've confirmed the first oplog event.
_params.assertTsHasNotFallenOffOplog = boost::none;
}
@@ -431,9 +448,7 @@ void CollectionScan::doReattachToOperationContext() {
unique_ptr<PlanStageStats> CollectionScan::getStats() {
// Add a BSON representation of the filter to the stats tree, if there is one.
if (nullptr != _filter) {
- BSONObjBuilder bob;
- _filter->serialize(&bob);
- _commonStats.filter = bob.obj();
+ _commonStats.filter = _filter->serialize();
}
unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_COLLSCAN);
diff --git a/src/mongo/db/exec/collection_scan_common.h b/src/mongo/db/exec/collection_scan_common.h
index 5770943229c..8aecb6750fc 100644
--- a/src/mongo/db/exec/collection_scan_common.h
+++ b/src/mongo/db/exec/collection_scan_common.h
@@ -42,12 +42,16 @@ struct CollectionScanParams {
};
enum class ScanBoundInclusion {
- kExcludeBothStartAndEndRecords,
- kIncludeStartRecordOnly,
- kIncludeEndRecordOnly,
- kIncludeBothStartAndEndRecords,
+ kExcludeBothStartAndEndRecords = 0b00,
+ kIncludeStartRecordOnly = 0b01,
+ kIncludeEndRecordOnly = 0b10,
+ kIncludeBothStartAndEndRecords = 0b11,
};
+ static ScanBoundInclusion makeInclusion(bool startInclusive, bool endInclusive) {
+ return ScanBoundInclusion(int(startInclusive) | (int(endInclusive) << 1));
+ }
+
// If present, this parameter sets the start point of a forward scan or the end point of a
// reverse scan. A forward scan will start scanning at the document with the lowest RecordId
// greater than or equal to minRecord. A reverse scan will stop and return EOF on the first
diff --git a/src/mongo/db/exec/document_value/document.cpp b/src/mongo/db/exec/document_value/document.cpp
index 7172370e4c3..2bcb637d17b 100644
--- a/src/mongo/db/exec/document_value/document.cpp
+++ b/src/mongo/db/exec/document_value/document.cpp
@@ -255,7 +255,8 @@ Value& DocumentStorage::appendField(T field, ValueElement::Kind kind) {
append(nextCollision);
append(nameSize);
append(kind);
- field.copyTo(dest, true);
+ dest += field.copy(dest, field.size());
+ *dest++ = '\0'; // Like std::string, there is both an explicit size and final NUL byte.
// Padding for alignment handled above
#undef append
@@ -432,6 +433,12 @@ Document DocumentStorage::shred() const {
return md.freeze();
}
+void DocumentStorage::loadIntoCache() const {
+ for (DocumentStorageIterator it = iterator(); !it.atEnd(); it.advance()) {
+ it.get();
+ }
+}
+
void DocumentStorage::loadLazyMetadata() const {
if (_haveLazyLoadedMetadata) {
return;
@@ -819,7 +826,7 @@ void Document::serializeForSorter(BufBuilder& buf) const {
buf.appendNum(static_cast<int>(numElems));
for (DocumentStorageIterator it = storage().iterator(); !it.atEnd(); it.advance()) {
- buf.appendStr(it->nameSD(), /*NUL byte*/ true);
+ buf.appendCStr(it->nameSD());
it->val.serializeForSorter(buf);
}
diff --git a/src/mongo/db/exec/document_value/document.h b/src/mongo/db/exec/document_value/document.h
index 6114aee792d..4ff1522f3a9 100644
--- a/src/mongo/db/exec/document_value/document.h
+++ b/src/mongo/db/exec/document_value/document.h
@@ -254,6 +254,13 @@ public:
return storage().shred();
}
+ /**
+ * Loads the whole document into cache.
+ */
+ void loadIntoCache() const {
+ return storage().loadIntoCache();
+ }
+
/** Calculate a hash value.
*
* Meant to be used to create composite hashes suitable for
diff --git a/src/mongo/db/exec/document_value/document_internal.h b/src/mongo/db/exec/document_value/document_internal.h
index d7fb3b337d6..372130c3e68 100644
--- a/src/mongo/db/exec/document_value/document_internal.h
+++ b/src/mongo/db/exec/document_value/document_internal.h
@@ -296,8 +296,8 @@ public:
return _sd.size();
}
- inline void copyTo(char* dest, bool includeEndingNull) const {
- return _sd.copyTo(dest, includeEndingNull);
+ inline size_t copy(char* dest, size_t len) const {
+ return _sd.copy(dest, len);
}
constexpr const char* rawData() const noexcept {
@@ -385,6 +385,11 @@ public:
*/
Document shred() const;
+ /**
+ * Loads the whole document into cache.
+ */
+ void loadIntoCache() const;
+
static const DocumentStorage& emptyDoc() {
return kEmptyDoc;
}
diff --git a/src/mongo/db/exec/document_value/document_value_test.cpp b/src/mongo/db/exec/document_value/document_value_test.cpp
index 80441c17f25..ca1123a7555 100644
--- a/src/mongo/db/exec/document_value/document_value_test.cpp
+++ b/src/mongo/db/exec/document_value/document_value_test.cpp
@@ -46,16 +46,6 @@
#include "mongo/dbtests/dbtests.h"
#include "mongo/logv2/log.h"
-#define ASSERT_DOES_NOT_THROW(EXPRESSION) \
- try { \
- EXPRESSION; \
- } catch (const AssertionException& e) { \
- ::mongo::str::stream err; \
- err << "Threw an exception incorrectly: " << e.toString() \
- << " Exception occured in: " << #EXPRESSION; \
- ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, err).stream(); \
- }
-
namespace DocumentTests {
using std::numeric_limits;
diff --git a/src/mongo/db/exec/document_value/document_value_test_util.h b/src/mongo/db/exec/document_value/document_value_test_util.h
index b6959a7d17f..7b88c9688fa 100644
--- a/src/mongo/db/exec/document_value/document_value_test_util.h
+++ b/src/mongo/db/exec/document_value/document_value_test_util.h
@@ -59,6 +59,13 @@
#define _ASSERT_DOCVAL_COMPARISON(NAME, a, b) \
::mongo::unittest::assertComparison_##NAME(__FILE__, __LINE__, #a, #b, a, b)
+// TODO SERVER-87736 make these not say "AUTO".
+// These are backport-special macros, adapted from the "AUTO" version on more recent branches. The
+// automatic functionality doesn't exist on this branch. But the assertions should still pass.
+#define ASSERT_VALUE_EQ_AUTO(expected, val) ASSERT_EQ(expected, val.toString())
+#define ASSERT_DOCUMENT_EQ_AUTO(expected, actual) \
+ ASSERT_BSONOBJ_EQ(fromjson(expected), actual.toBson())
+
namespace mongo {
namespace unittest {
diff --git a/src/mongo/db/exec/document_value/value.cpp b/src/mongo/db/exec/document_value/value.cpp
index 616efb2128a..a4f47313b49 100644
--- a/src/mongo/db/exec/document_value/value.cpp
+++ b/src/mongo/db/exec/document_value/value.cpp
@@ -117,7 +117,7 @@ void ValueStorage::putString(StringData s) {
if (sizeNoNUL <= sizeof(shortStrStorage)) {
shortStr = true;
shortStrSize = s.size();
- s.copyTo(shortStrStorage, false); // no NUL
+ s.copy(shortStrStorage, s.size());
// All memory is zeroed before this is called, so we know that
// the nulTerminator field will definitely contain a NUL byte.
@@ -148,8 +148,9 @@ void ValueStorage::putRegEx(const BSONRegEx& re) {
// Need to copy since putString doesn't support scatter-gather.
std::unique_ptr<char[]> buf(new char[totalLen]);
- re.pattern.copyTo(buf.get(), true);
- re.flags.copyTo(buf.get() + patternLen + 1, false); // no NUL
+ auto dest = buf.get();
+ dest = str::copyAsCString(dest, re.pattern);
+ re.flags.copy(dest, re.flags.size()); // NUL added automatically by putString()
putString(StringData(buf.get(), totalLen));
}
@@ -1310,7 +1311,7 @@ void Value::serializeForSorter(BufBuilder& buf) const {
case Code: {
StringData str = getRawData();
buf.appendNum(int(str.size()));
- buf.appendStr(str, /*NUL byte*/ false);
+ buf.appendStrBytes(str);
break;
}
@@ -1318,13 +1319,13 @@ void Value::serializeForSorter(BufBuilder& buf) const {
StringData str = getRawData();
buf.appendChar(_storage.binDataType());
buf.appendNum(int(str.size()));
- buf.appendStr(str, /*NUL byte*/ false);
+ buf.appendStrBytes(str);
break;
}
case RegEx:
- buf.appendStr(getRegex(), /*NUL byte*/ true);
- buf.appendStr(getRegexFlags(), /*NUL byte*/ true);
+ buf.appendCStr(getRegex());
+ buf.appendCStr(getRegexFlags());
break;
case Object:
@@ -1333,13 +1334,13 @@ void Value::serializeForSorter(BufBuilder& buf) const {
case DBRef:
buf.appendStruct(_storage.getDBRef()->oid);
- buf.appendStr(_storage.getDBRef()->ns, /*NUL byte*/ true);
+ buf.appendCStr(_storage.getDBRef()->ns);
break;
case CodeWScope: {
intrusive_ptr<const RCCodeWScope> cws = _storage.getCodeWScope();
buf.appendNum(int(cws->code.size()));
- buf.appendStr(cws->code, /*NUL byte*/ false);
+ buf.appendStrBytes(cws->code);
cws->scope.serializeForSorter(buf);
break;
}
diff --git a/src/mongo/db/exec/document_value/value.h b/src/mongo/db/exec/document_value/value.h
index a69494f1995..62a31f25727 100644
--- a/src/mongo/db/exec/document_value/value.h
+++ b/src/mongo/db/exec/document_value/value.h
@@ -430,13 +430,16 @@ public:
ImplicitValue(T&& arg) : Value(std::forward<T>(arg)) {}
ImplicitValue(std::initializer_list<ImplicitValue> values) : Value(convertToValues(values)) {}
+ ImplicitValue(std::vector<ImplicitValue> values) : Value(convertToValues(values)) {}
- ImplicitValue(std::vector<int> values) : Value(convertToValues(values)) {}
+ template <typename T>
+ ImplicitValue(std::vector<T> values) : Value(convertToValues(values)) {}
- static std::vector<Value> convertToValues(const std::vector<int>& vec) {
+ template <typename T>
+ static std::vector<Value> convertToValues(const std::vector<T>& vec) {
std::vector<Value> values;
values.reserve(vec.size());
- for_each(vec.begin(), vec.end(), ([&](const int& val) { values.emplace_back(val); }));
+ for_each(vec.begin(), vec.end(), ([&](const T& val) { values.emplace_back(val); }));
return values;
}
diff --git a/src/mongo/db/exec/exclusion_projection_executor.cpp b/src/mongo/db/exec/exclusion_projection_executor.cpp
index 9823ed1b125..2061bf4fe93 100644
--- a/src/mongo/db/exec/exclusion_projection_executor.cpp
+++ b/src/mongo/db/exec/exclusion_projection_executor.cpp
@@ -38,14 +38,15 @@ std::pair<BSONObj, bool> ExclusionNode::extractProjectOnFieldAndRename(const Str
BSONObjBuilder extractedExclusion;
// Check for a projection directly on 'oldName'. For example, {oldName: 0}.
- if (auto it = _projectedFields.find(oldName); it != _projectedFields.end()) {
+ if (auto it = _projectedFieldsSet.find(oldName); it != _projectedFieldsSet.end()) {
extractedExclusion.append(newName, false);
- _projectedFields.erase(it);
+ _projectedFieldsSet.erase(it);
+ _projectedFields.remove(std::string(oldName));
}
// Check for a projection on subfields of 'oldName'. For example, {oldName: {a: 0, b: 0}}.
if (auto it = _children.find(oldName); it != _children.end()) {
- extractedExclusion.append(newName, it->second->serialize(boost::none).toBson());
+ extractedExclusion.append(newName, it->second->serialize(boost::none, {}).toBson());
_children.erase(it);
}
diff --git a/src/mongo/db/exec/exclusion_projection_executor.h b/src/mongo/db/exec/exclusion_projection_executor.h
index d7a441e3b5c..37623206723 100644
--- a/src/mongo/db/exec/exclusion_projection_executor.h
+++ b/src/mongo/db/exec/exclusion_projection_executor.h
@@ -99,10 +99,12 @@ protected:
*/
class ExclusionProjectionExecutor : public ProjectionExecutor {
public:
- ExclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx,
- ProjectionPolicies policies,
- bool allowFastPath = false)
- : ProjectionExecutor(expCtx, policies), _root(new ExclusionNode(_policies)) {}
+ ExclusionProjectionExecutor(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ ProjectionPolicies policies,
+ bool allowFastPath = false,
+ boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none)
+ : ProjectionExecutor(expCtx, policies, proj), _root(new ExclusionNode(_policies)) {}
TransformerType getType() const final {
return TransformerType::kExclusionProjection;
@@ -116,16 +118,17 @@ public:
return _root.get();
}
- Document serializeTransformation(
- boost::optional<ExplainOptions::Verbosity> explain) const final {
+ Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain,
+ const SerializationOptions& options = {}) const final {
MutableDocument output;
// The ExclusionNode tree in '_root' will always have a top-level _id node if _id is to be
// excluded. If the _id node is not present, then explicitly set {_id: true} to avoid
// ambiguity in the expected behavior of the serialized projection.
- _root->serialize(explain, &output);
- if (output.peek()["_id"].missing()) {
- output.addField("_id", Value{true});
+ _root->serialize(explain, &output, options);
+ auto idFieldName = options.serializeFieldPath("_id");
+ if (output.peek()[idFieldName].missing()) {
+ output.addField(idFieldName, Value{true});
}
return output.freeze();
}
diff --git a/src/mongo/db/exec/fetch.cpp b/src/mongo/db/exec/fetch.cpp
index 9d51dad5ca4..2059faf7d79 100644
--- a/src/mongo/db/exec/fetch.cpp
+++ b/src/mongo/db/exec/fetch.cpp
@@ -181,10 +181,8 @@ unique_ptr<PlanStageStats> FetchStage::getStats() {
_commonStats.isEOF = isEOF();
// Add a BSON representation of the filter to the stats tree, if there is one.
- if (nullptr != _filter) {
- BSONObjBuilder bob;
- _filter->serialize(&bob);
- _commonStats.filter = bob.obj();
+ if (_filter) {
+ _commonStats.filter = _filter->serialize();
}
unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_FETCH);
diff --git a/src/mongo/db/exec/inclusion_projection_executor.cpp b/src/mongo/db/exec/inclusion_projection_executor.cpp
index 35828091f16..d06cedf61b6 100644
--- a/src/mongo/db/exec/inclusion_projection_executor.cpp
+++ b/src/mongo/db/exec/inclusion_projection_executor.cpp
@@ -67,7 +67,7 @@ void FastPathEligibleInclusionNode::_applyProjections(BSONObj bson, BSONObjBuild
const auto bsonElement{it.next()};
const auto fieldName{bsonElement.fieldNameStringData()};
- if (_projectedFields.find(fieldName) != _projectedFields.end()) {
+ if (_projectedFieldsSet.find(fieldName) != _projectedFieldsSet.end()) {
bob->append(bsonElement);
--nFieldsNeeded;
} else if (auto childIt = _children.find(fieldName); childIt != _children.end()) {
@@ -219,11 +219,12 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject(
for (const auto& expressionSpec : addFieldsExpressions) {
auto&& fieldName = std::get<0>(expressionSpec).toString();
auto oldExpr = std::get<1>(expressionSpec);
- oldExpr->serialize(false).addToBsonObj(&bb, fieldName);
+ oldExpr->serialize().addToBsonObj(&bb, fieldName);
if (std::get<2>(expressionSpec)) {
// Replace the expression with an inclusion projected field.
- _projectedFields.insert(fieldName);
+ auto it = _projectedFields.insert(_projectedFields.end(), fieldName);
+ _projectedFieldsSet.insert(StringData(*it));
_expressions.erase(fieldName);
// Only computed projections at the beginning of the list were marked to become
// projected fields. The new projected field is at the beginning of the
@@ -302,7 +303,7 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInAddFields(
for (const auto& expressionSpec : addFieldsExpressions) {
auto&& fieldName = expressionSpec.first.toString();
auto expr = expressionSpec.second;
- expr->serialize(false).addToBsonObj(&bb, fieldName);
+ expr->serialize().addToBsonObj(&bb, fieldName);
// Remove the expression from this inclusion node.
_expressions.erase(fieldName);
diff --git a/src/mongo/db/exec/inclusion_projection_executor.h b/src/mongo/db/exec/inclusion_projection_executor.h
index aef4c07b434..7f505f54797 100644
--- a/src/mongo/db/exec/inclusion_projection_executor.h
+++ b/src/mongo/db/exec/inclusion_projection_executor.h
@@ -173,19 +173,24 @@ private:
*/
class InclusionProjectionExecutor : public ProjectionExecutor {
public:
- InclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx,
- ProjectionPolicies policies,
- std::unique_ptr<InclusionNode> root)
- : ProjectionExecutor(expCtx, policies), _root(std::move(root)) {}
-
- InclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx,
- ProjectionPolicies policies,
- bool allowFastPath = false)
+ InclusionProjectionExecutor(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ ProjectionPolicies policies,
+ std::unique_ptr<InclusionNode> root,
+ boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none)
+ : ProjectionExecutor(expCtx, policies, proj), _root(std::move(root)) {}
+
+ InclusionProjectionExecutor(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ ProjectionPolicies policies,
+ bool allowFastPath = false,
+ boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none)
: InclusionProjectionExecutor(
expCtx,
policies,
allowFastPath ? std::make_unique<FastPathEligibleInclusionNode>(policies)
- : std::make_unique<InclusionNode>(policies)) {}
+ : std::make_unique<InclusionNode>(policies),
+ proj) {}
TransformerType getType() const final {
return TransformerType::kInclusionProjection;
@@ -202,16 +207,17 @@ public:
/**
* Serialize the projection.
*/
- Document serializeTransformation(
- boost::optional<ExplainOptions::Verbosity> explain) const final {
+ Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain,
+ const SerializationOptions& options = {}) const final {
MutableDocument output;
// The InclusionNode tree in '_root' will always have a top-level _id node if _id is to be
// included. If the _id node is not present, then explicitly set {_id: false} to avoid
// ambiguity in the expected behavior of the serialized projection.
- _root->serialize(explain, &output);
- if (output.peek()["_id"].missing()) {
- output.addField("_id", Value{false});
+ _root->serialize(explain, &output, options);
+ auto idFieldName = options.serializeFieldPath("_id");
+ if (output.peek()[idFieldName].missing()) {
+ output.addField(idFieldName, Value{false});
}
return output.freeze();
diff --git a/src/mongo/db/exec/index_scan.cpp b/src/mongo/db/exec/index_scan.cpp
index 06399c4e33b..c61fc452ec7 100644
--- a/src/mongo/db/exec/index_scan.cpp
+++ b/src/mongo/db/exec/index_scan.cpp
@@ -280,9 +280,7 @@ std::unique_ptr<PlanStageStats> IndexScan::getStats() {
// Add a BSON representation of the filter to the stats tree, if there is one.
if (nullptr != _filter) {
- BSONObjBuilder bob;
- _filter->serialize(&bob);
- _commonStats.filter = bob.obj();
+ _commonStats.filter = _filter->serialize();
}
// These specific stats fields never change.
diff --git a/src/mongo/db/exec/or.cpp b/src/mongo/db/exec/or.cpp
index ec0d680ac37..078765ffc84 100644
--- a/src/mongo/db/exec/or.cpp
+++ b/src/mongo/db/exec/or.cpp
@@ -122,10 +122,8 @@ unique_ptr<PlanStageStats> OrStage::getStats() {
_commonStats.isEOF = isEOF();
// Add a BSON representation of the filter to the stats tree, if there is one.
- if (nullptr != _filter) {
- BSONObjBuilder bob;
- _filter->serialize(&bob);
- _commonStats.filter = bob.obj();
+ if (_filter) {
+ _commonStats.filter = _filter->serialize();
}
unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_OR);
diff --git a/src/mongo/db/exec/projection.h b/src/mongo/db/exec/projection.h
index 00e7fb33dbc..236792ce64a 100644
--- a/src/mongo/db/exec/projection.h
+++ b/src/mongo/db/exec/projection.h
@@ -33,6 +33,7 @@
#include "mongo/db/exec/projection_executor.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/matcher/expression.h"
+#include "mongo/db/query/projection.h"
#include "mongo/db/query/projection_ast.h"
#include "mongo/db/record_id.h"
diff --git a/src/mongo/db/exec/projection_executor.h b/src/mongo/db/exec/projection_executor.h
index ca8e0d3990c..449ef8c3a7b 100644
--- a/src/mongo/db/exec/projection_executor.h
+++ b/src/mongo/db/exec/projection_executor.h
@@ -38,6 +38,7 @@
#include "mongo/db/pipeline/expression_context.h"
#include "mongo/db/pipeline/field_path.h"
#include "mongo/db/pipeline/transformer_interface.h"
+#include "mongo/db/query/projection_ast.h"
#include "mongo/db/query/projection_policies.h"
namespace mongo::projection_executor {
@@ -97,10 +98,20 @@ public:
*/
virtual boost::optional<std::set<FieldRef>> extractExhaustivePaths() const = 0;
+ /**
+ * The query shape is made by serializing the first parsed representation of the query, which in
+ * the case of $project queries is a projection_ast::Projection. The ProjectionExecutor, holds
+ * onto the root node of the AST for only $project queries, so that the first parsed
+ * representation is accessible at serialization.
+ */
+ boost::optional<projection_ast::ProjectionPathASTNode> projection = boost::none;
+
protected:
ProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx,
- ProjectionPolicies policies)
- : _expCtx(expCtx),
+ ProjectionPolicies policies,
+ boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none)
+ : projection(proj),
+ _expCtx(expCtx),
_policies(policies),
_projectionPostImageVarId{
_expCtx->variablesParseState.defineVariable(kProjectionPostImageVarName)} {}
diff --git a/src/mongo/db/exec/projection_executor_builder.cpp b/src/mongo/db/exec/projection_executor_builder.cpp
index 1b712685ea9..f5e94be3f78 100644
--- a/src/mongo/db/exec/projection_executor_builder.cpp
+++ b/src/mongo/db/exec/projection_executor_builder.cpp
@@ -251,7 +251,7 @@ auto buildProjectionExecutor(boost::intrusive_ptr<ExpressionContext> expCtx,
const ProjectionPolicies policies,
const BuilderParamsBitSet params) {
ProjectionExecutorVisitorContext<Executor> context{
- {std::make_unique<Executor>(expCtx, policies, params[kAllowFastPath]), expCtx}};
+ {std::make_unique<Executor>(expCtx, policies, params[kAllowFastPath], *root), expCtx}};
ProjectionExecutorVisitor<Executor> executorVisitor{&context};
projection_ast::PathTrackingWalker walker{&context, {&executorVisitor}, {}};
tree_walker::walk<true, projection_ast::ASTNode>(root, &walker);
diff --git a/src/mongo/db/exec/projection_executor_builder.h b/src/mongo/db/exec/projection_executor_builder.h
index 476f2b63a25..12f683a1cba 100644
--- a/src/mongo/db/exec/projection_executor_builder.h
+++ b/src/mongo/db/exec/projection_executor_builder.h
@@ -32,6 +32,7 @@
#include <bitset>
#include "mongo/db/exec/projection_executor.h"
+#include "mongo/db/query/projection.h"
#include "mongo/db/query/projection_ast.h"
namespace mongo::projection_executor {
diff --git a/src/mongo/db/exec/projection_executor_redaction_test.cpp b/src/mongo/db/exec/projection_executor_redaction_test.cpp
new file mode 100644
index 00000000000..70eb59855bb
--- /dev/null
+++ b/src/mongo/db/exec/projection_executor_redaction_test.cpp
@@ -0,0 +1,208 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "document_value/document_value_test_util.h"
+#include "mongo/db/exec/projection_executor.h"
+#include "mongo/db/exec/projection_executor_builder.h"
+#include "mongo/db/matcher/expression_parser.h"
+#include "mongo/db/pipeline/expression_context_for_test.h"
+#include "mongo/db/query/projection_ast_util.h"
+#include "mongo/db/query/projection_parser.h"
+#include "mongo/db/query/projection_policies.h"
+#include "mongo/db/query/query_shape/serialization_options.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo {
+namespace {
+std::unique_ptr<projection_executor::ProjectionExecutor> compileProjection(BSONObj proj) {
+ auto expCtx = make_intrusive<ExpressionContextForTest>();
+ auto policies = ProjectionPolicies::findProjectionPolicies();
+ auto ast = projection_ast::parseAndAnalyze(expCtx, proj, policies);
+ return projection_executor::buildProjectionExecutor(
+ expCtx, &ast, policies, projection_executor::kDefaultBuilderParams);
+}
+std::unique_ptr<projection_executor::ProjectionExecutor> compileProjection(BSONObj proj,
+ BSONObj query) {
+ auto expCtx = make_intrusive<ExpressionContextForTest>();
+ auto match = uassertStatusOK(MatchExpressionParser::parse(query, expCtx));
+ auto policies = ProjectionPolicies::findProjectionPolicies();
+ auto ast = projection_ast::parseAndAnalyze(expCtx, proj, match.get(), query, policies);
+ auto exec = projection_executor::buildProjectionExecutor(
+ expCtx, &ast, policies, projection_executor::kDefaultBuilderParams);
+ return exec;
+}
+
+TEST(Redaction, ProjectionTest) {
+ SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST;
+ auto redactProj = [&](std::string obj) {
+ return compileProjection(fromjson(obj))->serializeTransformation(boost::none, options);
+ };
+
+ /// Inclusion projections
+
+ // Simple single inclusion
+ auto actual = redactProj("{a: 1}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":true})",
+ actual);
+
+ actual = redactProj("{a: true}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":true})",
+ actual);
+
+ // Dotted path
+ actual = redactProj("{\"a.b\": 1}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":{"HASH<b>":true}})",
+ actual);
+
+ // Two fields
+ actual = redactProj("{a: 1, b: 1}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":true,"HASH<b>":true})",
+ actual);
+
+ // Explicit _id: 1
+ actual = redactProj("{b: 1, _id: 1}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<b>":true})",
+ actual);
+
+ // Two nested fields
+ actual = redactProj("{\"b.d\": 1, \"b.c\": 1}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<b>":{"HASH<d>":true,"HASH<c>":true}})",
+ actual);
+
+ actual = redactProj("{\"b.d\": 1, a: 1, \"b.c\": 1}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({
+ "HASH<_id>": true,
+ "HASH<a>": true,
+ "HASH<b>": {
+ "HASH<d>": true,
+ "HASH<c>": true
+ }
+ })",
+ actual);
+
+ /// Exclusion projections
+
+ // Simple single exclusion
+ actual = redactProj("{a: 0}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<a>":false,"HASH<_id>":true})",
+ actual);
+
+ // Dotted path
+ actual = redactProj("{\"a.b\": 0}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<a>":{"HASH<b>":false},"HASH<_id>":true})",
+ actual);
+
+ // Two fields
+ actual = redactProj("{a: 0, b: 0}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<a>":false,"HASH<b>":false,"HASH<_id>":true})",
+ actual);
+
+ // Explicit _id: 0
+ actual = redactProj("{b: 0, _id: 0}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":false,"HASH<b>":false})",
+ actual);
+
+ // Two nested fields
+ actual = redactProj("{\"b.d\": 0, \"b.c\": 0}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<b>":{"HASH<d>":false,"HASH<c>":false},"HASH<_id>":true})",
+ actual);
+
+ actual = redactProj("{\"b.d\": 0, a: 0, \"b.c\": 0}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({
+ "HASH<a>": false,
+ "HASH<b>": {
+ "HASH<d>": false,
+ "HASH<c>": false
+ },
+ "HASH<_id>": true
+ })",
+ actual);
+
+ /// Add fields projection
+ actual = redactProj("{a: \"hi\"}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":"?string"})",
+ actual);
+
+ actual = redactProj("{a: '$field'}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":"$HASH<field>"})",
+ actual);
+
+ // Dotted path
+ actual = redactProj("{\"a.b\": \"hi\"}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":{"HASH<b>":"?string"}})",
+ actual);
+
+ // Two fields
+ actual = redactProj("{a: \"hi\", b: \"hello\"}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<a>":"?string","HASH<b>":"?string"})",
+ actual);
+
+ // Explicit _id: 0
+ actual = redactProj("{b: \"hi\", _id: \"hey\"}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<b>":"?string","HASH<_id>":"?string"})",
+ actual);
+
+ // Two nested fields
+ actual = redactProj("{\"b.d\": \"hello\", \"b.c\": \"world\"}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({"HASH<_id>":true,"HASH<b>":{"HASH<d>":"?string","HASH<c>":"?string"}})",
+ actual);
+
+ actual = redactProj("{\"b.d\": \"hello\", a: \"world\", \"b.c\": \"mongodb\"}");
+ ASSERT_DOCUMENT_EQ_AUTO( // NOLINT
+ R"({
+ "HASH<_id>": true,
+ "HASH<b>": {
+ "HASH<d>": "?string",
+ "HASH<c>": "?string"
+ },
+ "HASH<a>": "?string"
+ })",
+ actual);
+}
+} // namespace
+} // namespace mongo
diff --git a/src/mongo/db/exec/projection_node.cpp b/src/mongo/db/exec/projection_node.cpp
index 773730c33bc..6b053c4593f 100644
--- a/src/mongo/db/exec/projection_node.cpp
+++ b/src/mongo/db/exec/projection_node.cpp
@@ -48,7 +48,8 @@ void ProjectionNode::addProjectionForPath(const FieldPath& path) {
void ProjectionNode::_addProjectionForPath(const FieldPath& path) {
makeOptimizationsStale();
if (path.getPathLength() == 1) {
- _projectedFields.insert(path.fullPath());
+ auto it = _projectedFields.insert(_projectedFields.end(), path.fullPath());
+ _projectedFieldsSet.insert(StringData(*it));
return;
}
// FieldPath can't be empty, so it is safe to obtain the first path component here.
@@ -143,7 +144,7 @@ void ProjectionNode::applyProjections(const Document& inputDoc, MutableDocument*
while (it.more()) {
auto fieldName = it.fieldName();
- if (_projectedFields.find(fieldName) != _projectedFields.end()) {
+ if (_projectedFieldsSet.find(fieldName) != _projectedFieldsSet.end()) {
if (isIncl) {
outputProjectedField(fieldName, it.next().second, outputDoc);
} else {
@@ -274,25 +275,27 @@ void ProjectionNode::optimize() {
_maxFieldsToProject = maxFieldsToProject();
}
-Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain) const {
+Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain,
+ const SerializationOptions& options) const {
MutableDocument outputDoc;
- serialize(explain, &outputDoc);
+ serialize(explain, &outputDoc, options);
return outputDoc.freeze();
}
void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain,
- MutableDocument* output) const {
+ MutableDocument* output,
+ const SerializationOptions& options) const {
// Determine the boolean value for projected fields in the explain output.
const bool projVal = isIncluded();
// Always put "_id" first if it was projected (implicitly or explicitly).
- if (_projectedFields.find("_id") != _projectedFields.end()) {
- output->addField("_id", Value(projVal));
+ if (_projectedFieldsSet.find("_id") != _projectedFieldsSet.end()) {
+ output->addField(options.serializeFieldPath("_id"), Value(projVal));
}
for (auto&& projectedField : _projectedFields) {
if (projectedField != "_id") {
- output->addField(projectedField, Value(projVal));
+ output->addField(options.serializeFieldPathFromString(projectedField), Value(projVal));
}
}
@@ -300,13 +303,14 @@ void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explai
auto childIt = _children.find(field);
if (childIt != _children.end()) {
MutableDocument subDoc;
- childIt->second->serialize(explain, &subDoc);
- output->addField(field, subDoc.freezeToValue());
+ childIt->second->serialize(explain, &subDoc, options);
+ output->addField(options.serializeFieldPathFromString(field), subDoc.freezeToValue());
} else {
invariant(_policies.computedFieldsPolicy == ComputedFieldsPolicy::kAllowComputedFields);
auto expressionIt = _expressions.find(field);
invariant(expressionIt != _expressions.end());
- output->addField(field, expressionIt->second->serialize(static_cast<bool>(explain)));
+ output->addField(options.serializeFieldPathFromString(field),
+ expressionIt->second->serialize(options));
}
}
}
diff --git a/src/mongo/db/exec/projection_node.h b/src/mongo/db/exec/projection_node.h
index 073fe14ae4e..cd744ef4cac 100644
--- a/src/mongo/db/exec/projection_node.h
+++ b/src/mongo/db/exec/projection_node.h
@@ -29,8 +29,9 @@
#pragma once
-#include "mongo/db/exec/projection_executor.h"
+#include <list>
+#include "mongo/db/exec/projection_executor.h"
#include "mongo/db/query/projection_policies.h"
namespace mongo::projection_executor {
@@ -128,10 +129,12 @@ public:
void optimize();
- Document serialize(boost::optional<ExplainOptions::Verbosity> explain) const;
+ Document serialize(boost::optional<ExplainOptions::Verbosity> explain,
+ const SerializationOptions& options) const;
void serialize(boost::optional<ExplainOptions::Verbosity> explain,
- MutableDocument* output) const;
+ MutableDocument* output,
+ const SerializationOptions& options) const;
protected:
/**
@@ -165,7 +168,14 @@ protected:
StringMap<std::unique_ptr<ProjectionNode>> _children;
StringMap<boost::intrusive_ptr<Expression>> _expressions;
- StringSet _projectedFields;
+
+ // List of the projected fields in the order in which they were specified.
+ std::list<std::string> _projectedFields;
+
+ // Set of projected fields. Note that the _projectedFields list actually owns the strings, and
+ // this StringDataSet simply holds views of those strings.
+ StringDataSet _projectedFieldsSet;
+
ProjectionPolicies _policies;
std::string _pathToNode;
diff --git a/src/mongo/db/exec/sbe/stages/hash_agg.cpp b/src/mongo/db/exec/sbe/stages/hash_agg.cpp
index 99bcc9f11c3..2514dedf1b0 100644
--- a/src/mongo/db/exec/sbe/stages/hash_agg.cpp
+++ b/src/mongo/db/exec/sbe/stages/hash_agg.cpp
@@ -103,18 +103,26 @@ std::unique_ptr<PlanStage> HashAggStage::clone() const {
void HashAggStage::doSaveState(bool relinquishCursor) {
if (relinquishCursor) {
if (_rsCursor) {
- _rsCursor->save();
+ _recordStore->saveCursor(_opCtx, _rsCursor);
}
}
if (_rsCursor) {
_rsCursor->setSaveStorageCursorOnDetachFromOperationContext(!relinquishCursor);
}
+
+ if (_recordStore) {
+ _recordStore->saveState();
+ }
}
void HashAggStage::doRestoreState(bool relinquishCursor) {
invariant(_opCtx);
+ if (_recordStore) {
+ _recordStore->restoreState();
+ }
+
if (_rsCursor && relinquishCursor) {
- auto couldRestore = _rsCursor->restore();
+ auto couldRestore = _recordStore->restoreCursor(_opCtx, _rsCursor);
uassert(6196500, "HashAggStage could not restore cursor", couldRestore);
}
}
@@ -262,8 +270,7 @@ void HashAggStage::makeTemporaryRecordStore() {
"No storage engine so HashAggStage cannot spill to disk",
_opCtx->getServiceContext()->getStorageEngine());
assertIgnorePrepareConflictsBehavior(_opCtx);
- _recordStore = _opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore(
- _opCtx, KeyFormat::String);
+ _recordStore = std::make_unique<SpillingStore>(_opCtx);
_specificStats.usedDisk = true;
}
@@ -291,10 +298,10 @@ void HashAggStage::spillRowToDisk(const value::MaterializedRow& key,
if (collator) {
// The keystring cannot always be deserialized back to the original keys when a collation is
// in use, so we also store the unmodified key in the data part of the spilled record.
- upsertToRecordStore(_opCtx, _recordStore->rs(), rid, key, val, false /*update*/);
+ _recordStore->upsertToRecordStore(_opCtx, rid, key, val, false /*update*/);
} else {
auto typeBits = kb.getTypeBits();
- upsertToRecordStore(_opCtx, _recordStore->rs(), rid, val, typeBits, false /*update*/);
+ _recordStore->upsertToRecordStore(_opCtx, rid, val, typeBits, false /*update*/);
}
_specificStats.spilledRecords++;
@@ -414,7 +421,9 @@ void HashAggStage::open(bool reOpen) {
for (auto&& accessor : _outAggAccessors) {
accessor->setIndex(0);
}
- _rsCursor.reset();
+ if (_recordStore) {
+ _recordStore->resetCursor(_opCtx, _rsCursor);
+ }
_recordStore.reset();
_outKeyRowRecordStore = {0};
_outAggRowRecordStore = {0};
@@ -490,7 +499,7 @@ void HashAggStage::open(bool reOpen) {
_specificStats.spilledDataStorageSize = _recordStore->rs()->storageSize(_opCtx);
// Establish a cursor, positioned at the beginning of the record store.
- _rsCursor = _recordStore->rs()->getCursor(_opCtx);
+ _rsCursor = _recordStore->getCursor(_opCtx);
// Callers will be obtaining the results from the spill table, so set the
// 'SwitchAccessors' so that they refer to the rows recovered from the record store
@@ -675,6 +684,9 @@ void HashAggStage::close() {
trackClose();
_ht = boost::none;
+ if (_recordStore && _opCtx) {
+ _recordStore->resetCursor(_opCtx, _rsCursor);
+ }
_rsCursor.reset();
_recordStore.reset();
_outKeyRowRecordStore = {0};
diff --git a/src/mongo/db/exec/sbe/stages/hash_agg.h b/src/mongo/db/exec/sbe/stages/hash_agg.h
index 2f77e445883..91f91051363 100644
--- a/src/mongo/db/exec/sbe/stages/hash_agg.h
+++ b/src/mongo/db/exec/sbe/stages/hash_agg.h
@@ -31,6 +31,7 @@
#include "mongo/db/exec/sbe/expressions/expression.h"
#include "mongo/db/exec/sbe/stages/stages.h"
+#include "mongo/db/exec/sbe/util/spilling.h"
#include "mongo/db/exec/sbe/vm/vm.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/storage/temporary_record_store.h"
@@ -277,7 +278,7 @@ private:
internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.load();
// A record store which is instantiated and written to in the case of spilling.
- std::unique_ptr<TemporaryRecordStore> _recordStore;
+ std::unique_ptr<SpillingStore> _recordStore;
std::unique_ptr<SeekableRecordCursor> _rsCursor;
// A monotically increasing counter used to ensure uniqueness of 'RecordId' values. When
diff --git a/src/mongo/db/exec/sbe/stages/hash_lookup.cpp b/src/mongo/db/exec/sbe/stages/hash_lookup.cpp
index fbc8ff73058..02e95307c4b 100644
--- a/src/mongo/db/exec/sbe/stages/hash_lookup.cpp
+++ b/src/mongo/db/exec/sbe/stages/hash_lookup.cpp
@@ -187,6 +187,22 @@ value::SlotAccessor* HashLookupStage::getAccessor(CompileCtx& ctx, value::SlotId
return outerChild()->getAccessor(ctx, slot);
}
}
+void HashLookupStage::doSaveState(bool relinquishCursor) {
+ if (_recordStoreHt) {
+ _recordStoreHt->saveState();
+ }
+ if (_recordStoreBuf) {
+ _recordStoreBuf->saveState();
+ }
+}
+void HashLookupStage::doRestoreState(bool relinquishCursor) {
+ if (_recordStoreHt) {
+ _recordStoreHt->restoreState();
+ }
+ if (_recordStoreBuf) {
+ _recordStoreBuf->restoreState();
+ }
+}
void HashLookupStage::reset() {
_ht = boost::none;
@@ -259,7 +275,7 @@ void HashLookupStage::addHashTableEntry(value::SlotAccessor* keyAccessor, size_t
auto val = std::vector<size_t>{valueIndex};
auto [tagKey, valKey] = keyAccessor->getViewOfValue();
- spillIndicesToRecordStore(_recordStoreHt->rs(), tagKey, valKey, val);
+ spillIndicesToRecordStore(_recordStoreHt.get(), tagKey, valKey, val);
}
} else {
// The key is already present in '_ht' so the memory will only grow by one size_t. If we
@@ -281,7 +297,7 @@ void HashLookupStage::addHashTableEntry(value::SlotAccessor* keyAccessor, size_t
// Evict the hash table value.
_computedTotalMemUsage -= htIt->second.size() * sizeof(size_t);
htIt->second.push_back(valueIndex);
- spillIndicesToRecordStore(_recordStoreHt->rs(), tagKeyView, valKeyView, htIt->second);
+ spillIndicesToRecordStore(_recordStoreHt.get(), tagKeyView, valKeyView, htIt->second);
_ht->erase(htIt);
}
}
@@ -297,17 +313,15 @@ void HashLookupStage::makeTemporaryRecordStore() {
_opCtx->getServiceContext()->getStorageEngine());
assertIgnorePrepareConflictsBehavior(_opCtx);
- _recordStoreBuf = _opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore(
- _opCtx, KeyFormat::Long);
+ _recordStoreBuf = std::make_unique<SpillingStore>(_opCtx, KeyFormat::Long);
- _recordStoreHt = _opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore(
- _opCtx, KeyFormat::String);
+ _recordStoreHt = std::make_unique<SpillingStore>(_opCtx, KeyFormat::String);
_specificStats.usedDisk = true;
}
void HashLookupStage::spillBufferedValueToDisk(OperationContext* opCtx,
- RecordStore* rs,
+ SpillingStore* rs,
size_t bufferIdx,
const value::MaterializedRow& val) {
auto rid = getValueRecordId(bufferIdx);
@@ -315,15 +329,7 @@ void HashLookupStage::spillBufferedValueToDisk(OperationContext* opCtx,
BufBuilder buf;
val.serializeForSorter(buf);
- assertIgnorePrepareConflictsBehavior(opCtx);
- WriteUnitOfWork wuow(opCtx);
-
- auto status = rs->insertRecord(opCtx, rid, buf.buf(), buf.len(), Timestamp{});
- wuow.commit();
-
- tassert(6373906,
- str::stream() << "Failed to write to disk because " << status.getStatus().reason(),
- status.isOK());
+ rs->upsertToRecordStore(opCtx, rid, buf, false);
_specificStats.spilledBuffRecords++;
// Add size of record ID + size of buffer.
@@ -341,7 +347,7 @@ size_t HashLookupStage::bufferValueOrSpill(value::MaterializedRow& value) {
if (!hasSpilledBufToDisk()) {
makeTemporaryRecordStore();
}
- spillBufferedValueToDisk(_opCtx, _recordStoreBuf->rs(), bufferIndex, value);
+ spillBufferedValueToDisk(_opCtx, _recordStoreBuf.get(), bufferIndex, value);
}
_valueId++;
return bufferIndex;
@@ -427,7 +433,7 @@ void HashLookupStage::accumulateFromValueIndices(const C& bufferIndices) {
// We must shift the '_bufferIt' index by one when using it as a RecordId because a
// RecordId of 0 is invalid.
auto rid = getValueRecordId(_bufferIt);
- auto rsValue = readFromRecordStore(_opCtx, _recordStoreBuf->rs(), rid);
+ auto rsValue = _recordStoreBuf->readFromRecordStore(_opCtx, rid);
if (!rsValue) {
tasserted(6373900, "bufferIdx not found in record store");
}
@@ -443,7 +449,7 @@ void HashLookupStage::accumulateFromValueIndices(const C& bufferIndices) {
}
}
-void HashLookupStage::writeIndicesToRecordStore(RecordStore* rs,
+void HashLookupStage::writeIndicesToRecordStore(SpillingStore* rs,
value::TypeTags tagKey,
value::Value valKey,
const std::vector<size_t>& value,
@@ -458,7 +464,7 @@ void HashLookupStage::writeIndicesToRecordStore(RecordStore* rs,
key.reset(0, false, tagKey, valKey);
auto [rid, typeBits] = serializeKeyForRecordStore(key);
- upsertToRecordStore(_opCtx, rs, rid, buf, typeBits, update);
+ rs->upsertToRecordStore(_opCtx, rid, buf, typeBits, update);
if (!update) {
_specificStats.spilledHtRecords++;
// Add the size of key (which comprises of the memory usage for the key + its type bits),
@@ -471,7 +477,7 @@ void HashLookupStage::writeIndicesToRecordStore(RecordStore* rs,
}
boost::optional<std::vector<size_t>> HashLookupStage::readIndicesFromRecordStore(
- RecordStore* rs, value::TypeTags tagKey, value::Value valKey) {
+ SpillingStore* rs, value::TypeTags tagKey, value::Value valKey) {
_probeKey.reset(0, false, tagKey, valKey);
auto [rid, _] = serializeKeyForRecordStore(_probeKey);
@@ -490,7 +496,7 @@ boost::optional<std::vector<size_t>> HashLookupStage::readIndicesFromRecordStore
return boost::none;
}
-void HashLookupStage::spillIndicesToRecordStore(RecordStore* rs,
+void HashLookupStage::spillIndicesToRecordStore(SpillingStore* rs,
value::TypeTags tagKey,
value::Value valKey,
const std::vector<size_t>& value) {
@@ -545,7 +551,7 @@ PlanState HashLookupStage::getNext() {
normalizeStringIfCollator(tagElemView, valElemView);
auto indicesFromRS = readIndicesFromRecordStore(
- _recordStoreHt->rs(), tagElemCollView, valElemCollView);
+ _recordStoreHt.get(), tagElemCollView, valElemCollView);
if (indicesFromRS) {
indices.insert(indicesFromRS->begin(), indicesFromRS->end());
}
@@ -567,7 +573,7 @@ PlanState HashLookupStage::getNext() {
normalizeStringIfCollator(tagKeyView, valKeyView);
auto indicesFromRS = readIndicesFromRecordStore(
- _recordStoreHt->rs(), tagKeyCollView, valKeyCollView);
+ _recordStoreHt.get(), tagKeyCollView, valKeyCollView);
if (indicesFromRS) {
accumulateFromValueIndices(*indicesFromRS);
}
diff --git a/src/mongo/db/exec/sbe/stages/hash_lookup.h b/src/mongo/db/exec/sbe/stages/hash_lookup.h
index 2e3f0b34816..b312e0a68f4 100644
--- a/src/mongo/db/exec/sbe/stages/hash_lookup.h
+++ b/src/mongo/db/exec/sbe/stages/hash_lookup.h
@@ -33,6 +33,7 @@
#include "mongo/db/exec/sbe/expressions/expression.h"
#include "mongo/db/exec/sbe/stages/stages.h"
+#include "mongo/db/exec/sbe/util/spilling.h"
#include "mongo/db/exec/sbe/vm/vm.h"
#include "mongo/db/query/query_knobs_gen.h"
@@ -101,6 +102,10 @@ public:
std::vector<DebugPrinter::Block> debugPrint() const final;
size_t estimateCompileTimeSize() const final;
+protected:
+ void doSaveState(bool relinquishCursor) override;
+ void doRestoreState(bool relinquishCursor) override;
+
private:
using HashTableType = std::unordered_map<value::MaterializedRow, // NOLINT
std::vector<size_t>,
@@ -119,23 +124,23 @@ private:
// Spilling helpers.
void addHashTableEntry(value::SlotAccessor* keyAccessor, size_t valueIndex);
void spillBufferedValueToDisk(OperationContext* opCtx,
- RecordStore* rs,
+ SpillingStore* rs,
size_t bufferIdx,
const value::MaterializedRow&);
size_t bufferValueOrSpill(value::MaterializedRow& value);
void setInnerProjectSwitchAccessor(int idx);
- boost::optional<std::vector<size_t>> readIndicesFromRecordStore(RecordStore* rs,
+ boost::optional<std::vector<size_t>> readIndicesFromRecordStore(SpillingStore* rs,
value::TypeTags tagKey,
value::Value valKey);
- void writeIndicesToRecordStore(RecordStore* rs,
+ void writeIndicesToRecordStore(SpillingStore* rs,
value::TypeTags tagKey,
value::Value valKey,
const std::vector<size_t>& value,
bool update);
- void spillIndicesToRecordStore(RecordStore* rs,
+ void spillIndicesToRecordStore(SpillingStore* rs,
value::TypeTags tagKey,
value::Value valKey,
const std::vector<size_t>& value);
@@ -229,8 +234,8 @@ private:
// rows in '_buffer'.
long long _computedTotalMemUsage = 0;
- std::unique_ptr<TemporaryRecordStore> _recordStoreHt;
- std::unique_ptr<TemporaryRecordStore> _recordStoreBuf;
+ std::unique_ptr<SpillingStore> _recordStoreHt;
+ std::unique_ptr<SpillingStore> _recordStoreBuf;
HashLookupStats _specificStats;
};
diff --git a/src/mongo/db/exec/sbe/util/spilling.cpp b/src/mongo/db/exec/sbe/util/spilling.cpp
index 0f0cbb93d94..7675fad6846 100644
--- a/src/mongo/db/exec/sbe/util/spilling.cpp
+++ b/src/mongo/db/exec/sbe/util/spilling.cpp
@@ -29,6 +29,18 @@
#include "mongo/db/exec/sbe/util/spilling.h"
+#include "mongo/base/status.h"
+#include "mongo/base/status_with.h"
+#include "mongo/base/string_data.h"
+#include "mongo/bson/timestamp.h"
+#include "mongo/db/query/query_knobs_gen.h"
+#include "mongo/db/storage/record_data.h"
+#include "mongo/db/storage/recovery_unit.h"
+#include "mongo/db/storage/write_unit_of_work.h"
+#include "mongo/util/assert_util.h"
+#include "mongo/util/bufreader.h"
+#include "mongo/util/str.h"
+
namespace mongo {
namespace sbe {
@@ -57,32 +69,76 @@ KeyString::Value decodeKeyString(const RecordId& rid, KeyString::TypeBits typeBi
return kb.getValueCopy();
}
-boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& rid) {
- RecordData record;
- if (rs->findRecord(opCtx, rid, &record)) {
- auto valueReader = BufReader(record.data(), record.size());
- return value::MaterializedRow::deserializeForSorter(valueReader, {});
- }
- return boost::none;
+SpillingStore::SpillingStore(OperationContext* opCtx, KeyFormat format) {
+ _recordStore =
+ opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore(opCtx, format);
+
+ _spillingUnit = std::unique_ptr<RecoveryUnit>(
+ opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit());
+ _spillingUnit->setCacheMaxWaitTimeout(Milliseconds(internalQuerySpillingMaxWaitTimeout.load()));
+ _spillingState = WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork;
}
-static int upsertToRecordStore(
- OperationContext* opCtx, RecordStore* rs, const RecordId& key, BufBuilder& buf, bool update) {
+SpillingStore::~SpillingStore() {}
+int SpillingStore::upsertToRecordStore(OperationContext* opCtx,
+ const RecordId& recordKey,
+ const value::MaterializedRow& key,
+ const value::MaterializedRow& val,
+ bool update) {
+ BufBuilder buf;
+ key.serializeForSorter(buf);
+ val.serializeForSorter(buf);
+ return upsertToRecordStore(opCtx, recordKey, buf, update);
+}
+
+int SpillingStore::upsertToRecordStore(
+ OperationContext* opCtx,
+ const RecordId& key,
+ const value::MaterializedRow& val,
+ const KeyString::TypeBits& typeBits, // recover type of value.
+ bool update) {
+ BufBuilder bufValue;
+ val.serializeForSorter(bufValue);
+ // Append the 'typeBits' to the end of the val's buffer so the 'key' can be reconstructed when
+ // draining HashAgg.
+ bufValue.appendBuf(typeBits.getBuffer(), typeBits.getSize());
+
+ return upsertToRecordStore(opCtx, key, bufValue, update);
+}
+
+int SpillingStore::upsertToRecordStore(
+ OperationContext* opCtx,
+ const RecordId& key,
+ BufBuilder& buf,
+ const KeyString::TypeBits& typeBits, // recover type of value.
+ bool update) {
+ // Append the 'typeBits' to the end of the val's buffer so the 'key' can be reconstructed when
+ // draining HashAgg.
+ buf.appendBuf(typeBits.getBuffer(), typeBits.getSize());
+
+ return upsertToRecordStore(opCtx, key, buf, update);
+}
+
+int SpillingStore::upsertToRecordStore(OperationContext* opCtx,
+ const RecordId& key,
+ BufBuilder& buf,
+ bool update) {
assertIgnorePrepareConflictsBehavior(opCtx);
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
WriteUnitOfWork wuow(opCtx);
auto result = mongo::Status::OK();
if (update) {
- result = rs->updateRecord(opCtx, key, buf.buf(), buf.len());
+ result = rs()->updateRecord(opCtx, key, buf.buf(), buf.len());
} else {
- auto status = rs->insertRecord(opCtx, key, buf.buf(), buf.len(), Timestamp{});
+ auto status = rs()->insertRecord(opCtx, key, buf.buf(), buf.len(), Timestamp{});
result = status.getStatus();
}
wuow.commit();
+
if (!result.isOK()) {
tasserted(5843600, str::stream() << "Failed to write to disk because " << result.reason());
return 0;
@@ -90,42 +146,59 @@ static int upsertToRecordStore(
return buf.len();
}
-int upsertToRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& key,
- const value::MaterializedRow& val,
- const KeyString::TypeBits& typeBits, // recover type of value.
- bool update) {
- BufBuilder buf;
- val.serializeForSorter(buf);
- // Append the 'typeBits' to the end of the val's buffer so the 'key' can be reconstructed when
- // draining HashAgg.
- buf.appendBuf(typeBits.getBuffer(), typeBits.getSize());
- return upsertToRecordStore(opCtx, rs, key, buf, update);
+Status SpillingStore::insertRecords(OperationContext* opCtx,
+ std::vector<Record>* inOutRecords,
+ const std::vector<Timestamp>& timestamps) {
+ assertIgnorePrepareConflictsBehavior(opCtx);
+
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+ WriteUnitOfWork wuow(opCtx);
+ auto status = rs()->insertRecords(opCtx, inOutRecords, timestamps);
+ wuow.commit();
+
+ return status;
}
-int upsertToRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& recordKey,
- const value::MaterializedRow& key,
- const value::MaterializedRow& val,
- bool update) {
- BufBuilder buf;
- key.serializeForSorter(buf);
- val.serializeForSorter(buf);
- return upsertToRecordStore(opCtx, rs, recordKey, buf, update);
+boost::optional<value::MaterializedRow> SpillingStore::readFromRecordStore(OperationContext* opCtx,
+ const RecordId& rid) {
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+
+ RecordData record;
+ if (rs()->findRecord(opCtx, rid, &record)) {
+ auto valueReader = BufReader(record.data(), record.size());
+ return value::MaterializedRow::deserializeForSorter(valueReader, {});
+ }
+ return boost::none;
}
-int upsertToRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& key,
- BufBuilder& buf,
- const KeyString::TypeBits& typeBits, // recover type of value.
- bool update) {
- // Append the 'typeBits' to the end of the val's buffer so the 'key' can be reconstructed when
- // draining HashAgg.
- buf.appendBuf(typeBits.getBuffer(), typeBits.getSize());
- return upsertToRecordStore(opCtx, rs, key, buf, update);
+bool SpillingStore::findRecord(OperationContext* opCtx, const RecordId& loc, RecordData* out) {
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+
+ return rs()->findRecord(opCtx, loc, out);
}
+
+void SpillingStore::switchToSpilling(OperationContext* opCtx) {
+ invariant(!_originalUnit);
+ _originalUnit = opCtx->releaseRecoveryUnit();
+ _originalState = opCtx->setRecoveryUnit(std::move(_spillingUnit), _spillingState);
+}
+void SpillingStore::switchToOriginal(OperationContext* opCtx) {
+ invariant(!_spillingUnit);
+ _spillingUnit = opCtx->releaseRecoveryUnit();
+ _spillingState = opCtx->setRecoveryUnit(std::move(_originalUnit), _originalState);
+ invariant(!(_spillingUnit->getState() == RecoveryUnit::State::kInactiveInUnitOfWork ||
+ _spillingUnit->getState() == RecoveryUnit::State::kActive));
+}
+
+void SpillingStore::saveState() {
+ _spillingUnit->abandonSnapshot();
+}
+void SpillingStore::restoreState() {
+ // We do not have to do anything.
+}
+
} // namespace sbe
} // namespace mongo
diff --git a/src/mongo/db/exec/sbe/util/spilling.h b/src/mongo/db/exec/sbe/util/spilling.h
index 2d0eb98ec88..205d6f1a031 100644
--- a/src/mongo/db/exec/sbe/util/spilling.h
+++ b/src/mongo/db/exec/sbe/util/spilling.h
@@ -29,9 +29,14 @@
#pragma once
-#include "mongo/platform/basic.h"
+#include <boost/optional/optional.hpp>
+#include <utility>
+#include "mongo/bson/util/builder.h"
#include "mongo/db/exec/sbe/values/slot.h"
+#include "mongo/db/operation_context.h"
+#include "mongo/db/record_id.h"
+#include "mongo/db/storage/record_store.h"
#include "mongo/db/storage/temporary_record_store.h"
namespace mongo {
@@ -50,40 +55,104 @@ std::pair<RecordId, KeyString::TypeBits> encodeKeyString(KeyString::Builder&,
// Reconstructs the KeyString carried in RecordId using 'typeBits'.
KeyString::Value decodeKeyString(const RecordId& rid, KeyString::TypeBits typeBits);
-// Reads a materialized row from the record store.
-boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& rid);
-
-/**
- * Inserts or updates a key/value into 'rs'. The 'update' flag controls whether or not an update
- * will be performed. If a key/value pair is inserted into the 'rs' that already exists and
- * 'update' is false, this function will tassert.
- *
- * Returns the size of the new record in bytes, including the record id and value portions.
- */
-int upsertToRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& key,
- const value::MaterializedRow& val,
- const KeyString::TypeBits& typeBits,
- bool update);
/**
- * When a collator is provided, the key is encoded using the collator before being converted to a
- * record id. In this case, it is not possible to recover the key from the record id, thus we need
- * to store the original value of the key as well.
+ * SpillingStore is a wrapper around a temporary record store than maintains its own transaction as
+ * we do not want to intermingle operations running in the main query with spill reads and writes.
*/
-int upsertToRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& recordKey,
- const value::MaterializedRow& key,
- const value::MaterializedRow& val,
- bool update);
-int upsertToRecordStore(OperationContext* opCtx,
- RecordStore* rs,
- const RecordId& key,
- BufBuilder& buf,
- const KeyString::TypeBits& typeBits, // recover type of value.
- bool update);
+class SpillingStore {
+public:
+ SpillingStore(OperationContext* opCtx, KeyFormat format = KeyFormat::String);
+ ~SpillingStore();
+
+ /**
+ * When a collator is provided, the key is encoded using the collator before being converted to
+ * a record id. In this case, it is not possible to recover the key from the record id, thus we
+ * need to store the original value of the key as well.
+ */
+ int upsertToRecordStore(OperationContext* opCtx,
+ const RecordId& recordKey,
+ const value::MaterializedRow& key,
+ const value::MaterializedRow& val,
+ bool update);
+ /**
+ * Inserts or updates a key/value into 'rs'. The 'update' flag controls whether or not an update
+ * will be performed. If a key/value pair is inserted into the 'rs' that already exists and
+ * 'update' is false, this function will tassert.
+ *
+ * Returns the size of the new record in bytes, including the record id and value portions.
+ */
+ int upsertToRecordStore(OperationContext* opCtx,
+ const RecordId& key,
+ const value::MaterializedRow& val,
+ const KeyString::TypeBits& typeBits,
+ bool update);
+ int upsertToRecordStore(OperationContext* opCtx,
+ const RecordId& key,
+ BufBuilder& buf,
+ const KeyString::TypeBits& typeBits, // recover type of value.
+ bool update);
+ int upsertToRecordStore(OperationContext* opCtx,
+ const RecordId& key,
+ BufBuilder& buf,
+ bool update);
+
+
+ Status insertRecords(OperationContext* opCtx,
+ std::vector<Record>* inOutRecords,
+ const std::vector<Timestamp>& timestamps);
+
+ // Reads a materialized row from the record store.
+ boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* opCtx,
+ const RecordId& rid);
+
+ bool findRecord(OperationContext* opCtx, const RecordId& loc, RecordData* out);
+
+ auto rs() {
+ return _recordStore->rs();
+ }
+
+ auto getCursor(OperationContext* opCtx) {
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+ return rs()->getCursor(opCtx);
+ }
+
+ void resetCursor(OperationContext* opCtx, std::unique_ptr<SeekableRecordCursor>& cursor) {
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+ cursor.reset();
+ }
+
+ auto saveCursor(OperationContext* opCtx, std::unique_ptr<SeekableRecordCursor>& cursor) {
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+
+ return cursor->save();
+ }
+
+ auto restoreCursor(OperationContext* opCtx, std::unique_ptr<SeekableRecordCursor>& cursor) {
+ switchToSpilling(opCtx);
+ ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); });
+
+ return cursor->restore();
+ }
+
+ void saveState();
+ void restoreState();
+
+private:
+ void switchToSpilling(OperationContext* opCtx);
+ void switchToOriginal(OperationContext* opCtx);
+
+ std::unique_ptr<TemporaryRecordStore> _recordStore;
+
+ std::unique_ptr<RecoveryUnit> _originalUnit;
+ WriteUnitOfWork::RecoveryUnitState _originalState;
+
+ std::unique_ptr<RecoveryUnit> _spillingUnit;
+ WriteUnitOfWork::RecoveryUnitState _spillingState;
+
+ size_t _counter{0};
+};
} // namespace sbe
} // namespace mongo
diff --git a/src/mongo/db/exec/sbe/values/slot.cpp b/src/mongo/db/exec/sbe/values/slot.cpp
index 2dd622fcecb..cb53de849ac 100644
--- a/src/mongo/db/exec/sbe/values/slot.cpp
+++ b/src/mongo/db/exec/sbe/values/slot.cpp
@@ -271,7 +271,7 @@ static void serializeValue(BufBuilder& buf, TypeTags tag, Value val) {
case TypeTags::StringSmall: {
// Small strings cannot contain null bytes, so it is safe to serialize them as plain
// C-strings with a null terminator.
- buf.appendStr(getStringView(tag, val), true /* includeEndingNull */);
+ buf.appendCStr(getStringView(tag, val));
break;
}
case TypeTags::StringBig:
@@ -279,7 +279,7 @@ static void serializeValue(BufBuilder& buf, TypeTags tag, Value val) {
case TypeTags::bsonSymbol: {
auto sv = getStringOrSymbolView(tag, val);
buf.appendNum(static_cast<uint32_t>(sv.size()));
- buf.appendStr(sv, false /* includeEndingNull */);
+ buf.appendStrBytes(sv);
break;
}
case TypeTags::Array: {
@@ -309,7 +309,7 @@ static void serializeValue(BufBuilder& buf, TypeTags tag, Value val) {
auto obj = getObjectView(val);
buf.appendNum(obj->size());
for (size_t idx = 0; idx < obj->size(); ++idx) {
- buf.appendStr(obj->field(idx), true /* includeEndingNull */);
+ buf.appendCStr(obj->field(idx));
auto [tag, val] = obj->getAt(idx);
serializeValue(buf, tag, val);
}
@@ -352,27 +352,27 @@ static void serializeValue(BufBuilder& buf, TypeTags tag, Value val) {
}
case TypeTags::bsonRegex: {
auto regex = getBsonRegexView(val);
- buf.appendStr(regex.pattern, true /* includeEndingNull */);
- buf.appendStr(regex.flags, true /* includeEndingNull */);
+ buf.appendCStr(regex.pattern);
+ buf.appendCStr(regex.flags);
break;
}
case TypeTags::bsonJavascript: {
auto javascriptCode = getBsonJavascriptView(val);
buf.appendNum(static_cast<uint32_t>(javascriptCode.size()));
- buf.appendStr(javascriptCode, false /* includeEndingNull */);
+ buf.appendStrBytes(javascriptCode);
break;
}
case TypeTags::bsonDBPointer: {
auto dbptr = getBsonDBPointerView(val);
buf.appendNum(static_cast<uint32_t>(dbptr.ns.size()));
- buf.appendStr(dbptr.ns, false /* includeEndingNull */);
+ buf.appendStrBytes(dbptr.ns);
buf.appendBuf(dbptr.id, sizeof(ObjectIdType));
break;
}
case TypeTags::bsonCodeWScope: {
auto cws = getBsonCodeWScopeView(val);
buf.appendNum(static_cast<uint32_t>(cws.code.size()));
- buf.appendStr(cws.code, false /* includeEndingNull */);
+ buf.appendStrBytes(cws.code);
auto scopeLen = ConstDataView(cws.scope).read<LittleEndian<uint32_t>>();
buf.appendBuf(cws.scope, scopeLen);
break;
@@ -507,9 +507,10 @@ static void serializeValueIntoKeyString(KeyString::Builder& buf,
}
break;
}
+ case TypeTags::bsonObjectId:
case TypeTags::ObjectId: {
buf.appendBool(true);
- buf.appendBytes(getObjectIdView(val), sizeof(ObjectIdType));
+ buf.appendOID(OID::from(getRawPointerView(val)));
break;
}
case TypeTags::bsonObject: {
@@ -532,11 +533,6 @@ static void serializeValueIntoKeyString(KeyString::Builder& buf,
}
break;
}
- case TypeTags::bsonObjectId: {
- buf.appendBool(true);
- buf.appendOID(OID::from(getRawPointerView(val)));
- break;
- }
case TypeTags::bsonBinData: {
BufBuilder innerBinDataBuf;
innerBinDataBuf.appendUChar(static_cast<uint8_t>(tag));
diff --git a/src/mongo/db/exec/sbe/values/value_builder.h b/src/mongo/db/exec/sbe/values/value_builder.h
index 00333e9f824..53748519723 100644
--- a/src/mongo/db/exec/sbe/values/value_builder.h
+++ b/src/mongo/db/exec/sbe/values/value_builder.h
@@ -112,21 +112,21 @@ public:
} else {
appendValueBufferOffset(TypeTags::StringBig);
_valueBufferBuilder->appendNum(static_cast<int32_t>(in.size() + 1));
- _valueBufferBuilder->appendStr(in, true /* includeEndingNull */);
+ _valueBufferBuilder->appendStrBytesAndNul(in);
}
}
void append(const BSONSymbol& in) {
appendValueBufferOffset(TypeTags::bsonSymbol);
_valueBufferBuilder->appendNum(static_cast<int32_t>(in.symbol.size() + 1));
- _valueBufferBuilder->appendStr(in.symbol, true /* includeEndingNull */);
+ _valueBufferBuilder->appendStrBytesAndNul(in.symbol);
}
void append(const BSONCode& in) {
appendValueBufferOffset(TypeTags::bsonJavascript);
// Add one to account null byte at the end.
_valueBufferBuilder->appendNum(static_cast<uint32_t>(in.code.size() + 1));
- _valueBufferBuilder->appendStr(in.code, true /* includeEndingNull */);
+ _valueBufferBuilder->appendStrBytesAndNul(in.code);
}
void append(const BSONCodeWScope& in) {
@@ -134,7 +134,7 @@ public:
_valueBufferBuilder->appendNum(
static_cast<uint32_t>(4 + in.code.size() + 1 + in.scope.objsize()));
_valueBufferBuilder->appendNum(static_cast<int32_t>(in.code.size() + 1));
- _valueBufferBuilder->appendStr(in.code, true /* includeEndingNull */);
+ _valueBufferBuilder->appendStrBytesAndNul(in.code);
_valueBufferBuilder->appendBuf(in.scope.objdata(), in.scope.objsize());
}
@@ -147,14 +147,14 @@ public:
void append(const BSONRegEx& in) {
appendValueBufferOffset(TypeTags::bsonRegex);
- _valueBufferBuilder->appendStr(in.pattern, true /* includeEndingNull */);
- _valueBufferBuilder->appendStr(in.flags, true /* includeEndingNull */);
+ _valueBufferBuilder->appendCStr(in.pattern);
+ _valueBufferBuilder->appendCStr(in.flags);
}
void append(const BSONDBRef& in) {
appendValueBufferOffset(TypeTags::bsonDBPointer);
_valueBufferBuilder->appendNum(static_cast<int32_t>(in.ns.size() + 1));
- _valueBufferBuilder->appendStr(in.ns, true /* includeEndingNull */);
+ _valueBufferBuilder->appendStrBytesAndNul(in.ns);
_valueBufferBuilder->appendBuf(in.oid.view().view(), OID::kOIDSize);
}
diff --git a/src/mongo/db/exec/sbe/values/value_serialization_test.cpp b/src/mongo/db/exec/sbe/values/value_serialization_test.cpp
index 3be3212c627..ceacf610a4a 100644
--- a/src/mongo/db/exec/sbe/values/value_serialization_test.cpp
+++ b/src/mongo/db/exec/sbe/values/value_serialization_test.cpp
@@ -467,4 +467,17 @@ TEST_F(ValueSerializeForKeyString, RoundtripWideRow) {
}
runTest(row);
}
+
+// Test that roundtripping through KeyString works for ObjectIdType: ObjectId; bsonObjectId.
+TEST_F(ValueSerializeForKeyString, RoundtripObjectIdType) {
+ auto [objectIdTag, objectIdVal] = value::makeNewObjectId();
+
+ auto oid = OID::gen();
+ auto obj = BSON("" << oid);
+ auto oidStorage = obj.firstElement().value();
+
+ sbe::value::ValueGuard testDataGuard{objectIdTag, objectIdVal};
+ runTest({{objectIdTag, objectIdVal},
+ {value::TypeTags::bsonObjectId, value::bitcastFrom<const char*>(oidStorage)}});
+}
} // namespace mongo::sbe
diff --git a/src/mongo/db/exec/sbe/vm/arith.cpp b/src/mongo/db/exec/sbe/vm/arith.cpp
index 1d41ff59ce1..e6c9d380ffd 100644
--- a/src/mongo/db/exec/sbe/vm/arith.cpp
+++ b/src/mongo/db/exec/sbe/vm/arith.cpp
@@ -1027,7 +1027,7 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::genericLn(value::TypeT
if (!operand.isGreater(Decimal128::kNormalizedZero) && !operand.isNaN()) {
return {false, value::TypeTags::Nothing, 0};
}
- auto operandLn = operand.logarithm();
+ auto operandLn = operand.naturalLogarithm();
auto [tag, value] = value::makeCopyDecimal(operandLn);
return {true, tag, value};
diff --git a/src/mongo/db/exec/skip.cpp b/src/mongo/db/exec/skip.cpp
index d3d0fc48afd..755e9278fe0 100644
--- a/src/mongo/db/exec/skip.cpp
+++ b/src/mongo/db/exec/skip.cpp
@@ -47,7 +47,7 @@ SkipStage::SkipStage(ExpressionContext* expCtx,
long long toSkip,
WorkingSet* ws,
std::unique_ptr<PlanStage> child)
- : PlanStage(kStageType, expCtx), _ws(ws), _toSkip(toSkip) {
+ : PlanStage(kStageType, expCtx), _ws(ws), _leftToSkip(toSkip), _skipAmount(toSkip) {
_children.emplace_back(std::move(child));
}
@@ -63,9 +63,9 @@ PlanStage::StageState SkipStage::doWork(WorkingSetID* out) {
if (PlanStage::ADVANCED == status) {
// If we're still skipping results...
- if (_toSkip > 0) {
+ if (_leftToSkip > 0) {
// ...drop the result.
- --_toSkip;
+ --_leftToSkip;
_ws->free(id);
return PlanStage::NEED_TIME;
}
@@ -82,7 +82,7 @@ PlanStage::StageState SkipStage::doWork(WorkingSetID* out) {
unique_ptr<PlanStageStats> SkipStage::getStats() {
_commonStats.isEOF = isEOF();
- _specificStats.skip = _toSkip;
+ _specificStats.skip = _skipAmount;
unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_SKIP);
ret->specific = std::make_unique<SkipStats>(_specificStats);
ret->children.emplace_back(child()->getStats());
diff --git a/src/mongo/db/exec/skip.h b/src/mongo/db/exec/skip.h
index 24937662d02..5d0a764bd8a 100644
--- a/src/mongo/db/exec/skip.h
+++ b/src/mongo/db/exec/skip.h
@@ -66,8 +66,13 @@ public:
private:
WorkingSet* _ws;
- // We drop the first _toSkip results that we would have returned.
- long long _toSkip;
+ // The number of results left to skip. This number is decremented during query execution as we
+ // successfully skip a document.
+ long long _leftToSkip;
+
+ // Represents the number of results to skip. Unlike '_leftToSkip', this remains constant and
+ // is used when gathering statistics in explain.
+ const long long _skipAmount;
// Stats
SkipStats _specificStats;
diff --git a/src/mongo/db/exec/text_or.cpp b/src/mongo/db/exec/text_or.cpp
index a0ba3eb347e..f9e0745ca16 100644
--- a/src/mongo/db/exec/text_or.cpp
+++ b/src/mongo/db/exec/text_or.cpp
@@ -104,9 +104,7 @@ std::unique_ptr<PlanStageStats> TextOrStage::getStats() {
_commonStats.isEOF = isEOF();
if (_filter) {
- BSONObjBuilder bob;
- _filter->serialize(&bob);
- _commonStats.filter = bob.obj();
+ _commonStats.filter = _filter->serialize();
}
unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_TEXT_OR);
diff --git a/src/mongo/db/exec/upsert_stage.cpp b/src/mongo/db/exec/upsert_stage.cpp
index 212880f6af3..dfb54a99082 100644
--- a/src/mongo/db/exec/upsert_stage.cpp
+++ b/src/mongo/db/exec/upsert_stage.cpp
@@ -278,6 +278,8 @@ void UpsertStage::_generateNewDocumentFromSuppliedDoc(const FieldRefSet& immutab
suppliedDoc, write_ops::UpdateModification::ClassicTag{}, true /* isReplacement */),
{});
replacementDriver.setLogOp(false);
+ replacementDriver.setBypassEmptyTsReplacement(
+ static_cast<bool>(_params.request->getBypassEmptyTsReplacement()));
// We do not validate for storage, as we will validate the full document before inserting.
// However, we ensure that no immutable fields are modified.