diff options
Diffstat (limited to 'src/mongo/db/exec')
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. |
