diff options
| author | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-14 14:26:38 -0300 |
|---|---|---|
| committer | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-14 14:26:38 -0300 |
| commit | 294bc6ecabf14c09c9bc8644704921dcf97cb44e (patch) | |
| tree | 279b1e0bab53901a1647ac63c1c724f0f789a663 /src/mongo/db/exec | |
| parent | 70be7c27a251621187a1de533462ae2bb1e3bd39 (diff) | |
| parent | 1e917fd798aa25b7066d4b414b51184f13d5a092 (diff) | |
Update upstream source from tag 'upstream/6.0.10'debian/6.0.10-1
Update to upstream version '6.0.10'
with Debian dir 2d176fa254eee97b139f712fec5709641335a8c3
Diffstat (limited to 'src/mongo/db/exec')
69 files changed, 3302 insertions, 1054 deletions
diff --git a/src/mongo/db/exec/SConscript b/src/mongo/db/exec/SConscript index c2caf69f0f2..eac1c6a3105 100644 --- a/src/mongo/db/exec/SConscript +++ b/src/mongo/db/exec/SConscript @@ -84,6 +84,7 @@ sortExecutorEnv.Library( ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/sorter/sorter_idl', + '$BUILD_DIR/mongo/db/sorter/sorter_stats', ], ) @@ -108,7 +109,7 @@ env.Library( 'stagedebug_cmd.cpp' ], LIBDEPS=[ - "$BUILD_DIR/mongo/db/index/index_access_methods", + "$BUILD_DIR/mongo/db/index/index_access_method", "$BUILD_DIR/mongo/db/query_exec", ], LIBDEPS_PRIVATE=[ diff --git a/src/mongo/db/exec/add_fields_projection_executor.cpp b/src/mongo/db/exec/add_fields_projection_executor.cpp index a0fd7f08580..067059167f5 100644 --- a/src/mongo/db/exec/add_fields_projection_executor.cpp +++ b/src/mongo/db/exec/add_fields_projection_executor.cpp @@ -93,7 +93,7 @@ private: const BSONObj& _rawObj; // Tracks which paths we've seen to ensure no two paths conflict with each other. - std::set<std::string, PathPrefixComparator> _seenPaths; + OrderedPathSet _seenPaths; }; void ProjectionSpecValidator::uassertValid(const BSONObj& spec) { diff --git a/src/mongo/db/exec/add_fields_projection_executor.h b/src/mongo/db/exec/add_fields_projection_executor.h index c2c8e8481e3..12f7bbfe19b 100644 --- a/src/mongo/db/exec/add_fields_projection_executor.h +++ b/src/mongo/db/exec/add_fields_projection_executor.h @@ -112,7 +112,7 @@ public: } DocumentSource::GetModPathsReturn getModifiedPaths() const final { - std::set<std::string> computedPaths; + OrderedPathSet computedPaths; StringMap<std::string> renamedPaths; _root->reportComputedPaths(&computedPaths, &renamedPaths); return {DocumentSource::GetModPathsReturn::Type::kFiniteSet, diff --git a/src/mongo/db/exec/add_fields_projection_executor_test.cpp b/src/mongo/db/exec/add_fields_projection_executor_test.cpp index 007e5cc1ffb..0870c53751a 100644 --- a/src/mongo/db/exec/add_fields_projection_executor_test.cpp +++ b/src/mongo/db/exec/add_fields_projection_executor_test.cpp @@ -685,5 +685,62 @@ TEST(AddFieldsProjectionExecutorExecutionTest, DoNotExtractComputedProjectionWit addFields.serializeTransformation(boost::none)); } +TEST(AddFieldsProjectionExecutorExecutionTest, + ExtractComputedProjectionShouldNotHideDependentSubFields) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addFields(expCtx); + addFields.parse(BSON("obj" + << "$myMeta" + << "b" << BSON("$add" << BSON_ARRAY("$obj.a" << 1)))); + + const std::set<StringData> reservedNames{}; + auto [extractedAddFields, deleteFlag] = + addFields.extractComputedProjections("myMeta", "meta", reservedNames); + + ASSERT_EQ(extractedAddFields.nFields(), 0); + ASSERT_EQ(deleteFlag, false); + + auto expectedProjection = + Document(fromjson("{obj: '$myMeta', b: {$add: ['$obj.a', {$const: 1}]}}")); + ASSERT_DOCUMENT_EQ(expectedProjection, addFields.serializeTransformation(boost::none)); +} + +TEST(AddFieldsProjectionExecutorExecutionTest, + ExtractComputedProjectionShouldNotHideDependentFieldsWithDottedSibling) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addFields(expCtx); + addFields.parse(BSON("a" + << "$myMeta" + << "c.b" + << "$a.x")); + + const std::set<StringData> reservedNames{}; + auto [extractedAddFields, deleteFlag] = + addFields.extractComputedProjections("myMeta", "meta", reservedNames); + + ASSERT_EQ(extractedAddFields.nFields(), 0); + ASSERT_EQ(deleteFlag, false); + + auto expectedProjection = Document(fromjson("{a: '$myMeta', c: {b: '$a.x'}}")); + ASSERT_DOCUMENT_EQ(expectedProjection, addFields.serializeTransformation(boost::none)); +} + +TEST(AddFieldsProjectionExecutorExecutionTest, ExtractComputedProjectionShouldNotIncludeId) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addFields(expCtx); + addFields.parse(BSON("a" << BSON("$sum" << BSON_ARRAY("$myMeta" + << "$_id")))); + + const std::set<StringData> reservedNames{}; + auto [extractedAddFields, deleteFlag] = + addFields.extractComputedProjections("myMeta", "meta", reservedNames); + + ASSERT_EQ(extractedAddFields.nFields(), 0); + ASSERT_EQ(deleteFlag, false); + + auto expectedProjection = Document(fromjson("{a: {$sum: ['$myMeta', '$_id']}}")); + ASSERT_DOCUMENT_EQ(expectedProjection, addFields.serializeTransformation(boost::none)); +} + } // namespace } // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/batched_delete_stage.cpp b/src/mongo/db/exec/batched_delete_stage.cpp index 78bfd05e352..fb8be63cd8a 100644 --- a/src/mongo/db/exec/batched_delete_stage.cpp +++ b/src/mongo/db/exec/batched_delete_stage.cpp @@ -242,6 +242,16 @@ PlanStage::StageState BatchedDeleteStage::_deleteBatch(WorkingSetID* out) { wuow.commit(); } catch (const WriteConflictException&) { return _prepareToRetryDrainAfterWCE(out, recordsThatNoLongerMatch); + } catch (const ExceptionFor<ErrorCodes::StaleConfig>& ex) { + if (ex->getVersionReceived() == ChunkVersion::IGNORED() && ex->getCriticalSectionSignal()) { + // If ChunkVersion is IGNORED and we encountered a critical section, then yield, wait + // for critical section to finish and then we'll resume the write from the point we had + // left. We do this to prevent large multi-writes from repeatedly failing due to + // StaleConfig and exhausting the mongos retry attempts. + planExecutorShardingCriticalSectionFuture(opCtx()) = ex->getCriticalSectionSignal(); + return _prepareToRetryDrainAfterWCE(out, recordsThatNoLongerMatch); + } + throw; } incrementSSSMetricNoOverflow(batchedDeletesSSS.docs, docsDeleted); diff --git a/src/mongo/db/exec/bucket_unpacker.cpp b/src/mongo/db/exec/bucket_unpacker.cpp index bf2f1444fea..cacadfba722 100644 --- a/src/mongo/db/exec/bucket_unpacker.cpp +++ b/src/mongo/db/exec/bucket_unpacker.cpp @@ -34,6 +34,7 @@ #include "mongo/bson/util/bsoncolumn.h" #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_algo.h" +#include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_expr.h" #include "mongo/db/matcher/expression_geo.h" #include "mongo/db/matcher/expression_internal_bucket_geo_within.h" @@ -41,9 +42,14 @@ #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/matcher/expression_tree.h" #include "mongo/db/matcher/extensions_callback_noop.h" +#include "mongo/db/matcher/rewrite_expr.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/timeseries/timeseries_options.h" +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery + +#include "mongo/logv2/log.h" + namespace mongo { using IneligiblePredicatePolicy = BucketSpec::IneligiblePredicatePolicy; @@ -58,6 +64,9 @@ bool BucketSpec::fieldIsComputed(StringData field) const { namespace { +constexpr long long max32BitEpochMillis = + static_cast<long long>(std::numeric_limits<uint32_t>::max()) * 1000; + /** * Creates an ObjectId initialized with an appropriate timestamp corresponding to 'rhs' and * returns it as a Value. @@ -77,7 +86,7 @@ auto constructObjectIdValue(const BSONElement& rhs, int bucketMaxSpanSeconds) { oid.init(date, maxOrMin == OIDInit::max); return oid; }; - // Make an ObjectId cooresponding to a date value adjusted by the max bucket value for the + // Make an ObjectId corresponding to a date value adjusted by the max bucket value for the // time series view that this query operates on. This predicate can be used in a comparison // to gauge a max value for a given bucket, rather than a min value. auto makeMaxAdjustedDateOID = [&](auto&& date, auto&& maxOrMin) { @@ -86,9 +95,16 @@ auto constructObjectIdValue(const BSONElement& rhs, int bucketMaxSpanSeconds) { // Subtract max bucket range. return makeDateOID(date - Seconds{bucketMaxSpanSeconds}, maxOrMin); else - // Since we're out of range, just make a predicate that is true for all date types. - return makeDateOID(Date_t::min(), OIDInit::min); + // Since we're out of range, just make a predicate that is true for all dates. + // We'll never use an OID for a date < 0 due to OID range limitations, so we set the + // minimum date to 0. + return makeDateOID(Date_t::fromMillisSinceEpoch(0LL), OIDInit::min); }; + + // Because the OID timestamp is only 4 bytes, we can't convert larger dates + invariant(rhs.date().toMillisSinceEpoch() >= 0LL); + invariant(rhs.date().toMillisSinceEpoch() <= max32BitEpochMillis); + // An ObjectId consists of a 4-byte timestamp, as well as a unique value and a counter, thus // two ObjectIds initialized with the same date will have different values. To ensure that we // do not incorrectly include or exclude any buckets, depending on the operator we will @@ -130,9 +146,9 @@ std::unique_ptr<MatchExpression> makeOr(std::vector<std::unique_ptr<MatchExpress return std::make_unique<OrMatchExpression>(std::move(nontrivial)); } -std::unique_ptr<MatchExpression> handleIneligible(IneligiblePredicatePolicy policy, - const MatchExpression* matchExpr, - StringData message) { +BucketSpec::BucketPredicate handleIneligible(IneligiblePredicatePolicy policy, + const MatchExpression* matchExpr, + StringData message) { switch (policy) { case IneligiblePredicatePolicy::kError: uasserted( @@ -140,7 +156,7 @@ std::unique_ptr<MatchExpression> handleIneligible(IneligiblePredicatePolicy poli "Error translating non-metadata time-series predicate to operate on buckets: " + message + ": " + matchExpr->serialize().toString()); case IneligiblePredicatePolicy::kIgnore: - return nullptr; + return {}; } MONGO_UNREACHABLE_TASSERT(5916307); } @@ -204,40 +220,32 @@ std::unique_ptr<MatchExpression> createTypeEqualityPredicate( return makeOr(std::move(typeEqualityPredicates)); } -std::unique_ptr<MatchExpression> createComparisonPredicate( - const ComparisonMatchExpressionBase* matchExpr, +boost::optional<StringData> checkComparisonPredicateErrors( + const MatchExpression* matchExpr, + const StringData matchExprPath, + const BSONElement& matchExprData, const BucketSpec& bucketSpec, - int bucketMaxSpanSeconds, - ExpressionContext::CollationMatchesDefault collationMatchesDefault, - boost::intrusive_ptr<ExpressionContext> pExpCtx, - bool haveComputedMetaField, - bool includeMetaField, - bool assumeNoMixedSchemaData, - IneligiblePredicatePolicy policy) { + ExpressionContext::CollationMatchesDefault collationMatchesDefault) { using namespace timeseries; - const auto matchExprPath = matchExpr->path(); - const auto matchExprData = matchExpr->getData(); - // The control field's min and max are chosen using a field-order insensitive comparator, while // MatchExpressions use a comparator that treats field-order as significant. Because of this we // will not perform this optimization on queries with operands of compound types. if (matchExprData.type() == BSONType::Object || matchExprData.type() == BSONType::Array) - return handleIneligible(policy, matchExpr, "operand can't be an object or array"_sd); + return "operand can't be an object or array"_sd; // MatchExpressions have special comparison semantics regarding null, in that {$eq: null} will // match all documents where the field is either null or missing. Because this is different // from both the comparison semantics that InternalExprComparison expressions and the control's // min and max fields use, we will not perform this optimization on queries with null operands. if (matchExprData.type() == BSONType::jstNULL) - return handleIneligible(policy, matchExpr, "can't handle {$eq: null}"_sd); + return "can't handle {$eq: null}"_sd; // The control field's min and max are chosen based on the collation of the collection. If the // query's collation does not match the collection's collation and the query operand is a // string or compound type (skipped above) we will not perform this optimization. if (collationMatchesDefault == ExpressionContext::CollationMatchesDefault::kNo && matchExprData.type() == BSONType::String) { - return handleIneligible( - policy, matchExpr, "can't handle string comparison with a non-default collation"_sd); + return "can't handle string comparison with a non-default collation"_sd; } // This function only handles time and measurement predicates--not metadata. @@ -252,29 +260,63 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // We must avoid mapping predicates on fields computed via $addFields or a computed $project. if (bucketSpec.fieldIsComputed(matchExprPath.toString())) { - return handleIneligible(policy, matchExpr, "can't handle a computed field"); + return "can't handle a computed field"_sd; + } + + // We must avoid mapping predicates on fields removed by $project. + if (!determineIncludeField(matchExprPath, bucketSpec.behavior(), bucketSpec.fieldSet())) { + return "can't handle a field removed by projection"_sd; } const auto isTimeField = (matchExprPath == bucketSpec.timeField()); if (isTimeField && matchExprData.type() != BSONType::Date) { // Users are not allowed to insert non-date measurements into time field. So this query // would not match anything. We do not need to optimize for this case. - return handleIneligible( - policy, - matchExpr, - "This predicate will never be true, because the time field always contains a Date"); + return "This predicate will never be true, because the time field always contains a Date"_sd; } + return boost::none; +} + +std::unique_ptr<MatchExpression> createComparisonPredicate( + const ComparisonMatchExpressionBase* matchExpr, + const BucketSpec& bucketSpec, + int bucketMaxSpanSeconds, + ExpressionContext::CollationMatchesDefault collationMatchesDefault, + boost::intrusive_ptr<ExpressionContext> pExpCtx, + bool haveComputedMetaField, + bool includeMetaField, + bool assumeNoMixedSchemaData, + IneligiblePredicatePolicy policy) { + using namespace timeseries; + const auto matchExprPath = matchExpr->path(); + const auto matchExprData = matchExpr->getData(); + + const auto error = checkComparisonPredicateErrors( + matchExpr, matchExprPath, matchExprData, bucketSpec, collationMatchesDefault); + if (error) { + return handleIneligible(policy, matchExpr, *error).loosePredicate; + } + + const auto isTimeField = (matchExprPath == bucketSpec.timeField()); + auto minPath = std::string{kControlMinFieldNamePrefix} + matchExprPath; + const StringData minPathStringData(minPath); + auto maxPath = std::string{kControlMaxFieldNamePrefix} + matchExprPath; + const StringData maxPathStringData(maxPath); + BSONObj minTime; BSONObj maxTime; + bool dateIsExtended = false; if (isTimeField) { auto timeField = matchExprData.Date(); minTime = BSON("" << timeField - Seconds(bucketMaxSpanSeconds)); maxTime = BSON("" << timeField + Seconds(bucketMaxSpanSeconds)); - } - auto minPath = std::string{kControlMinFieldNamePrefix} + matchExprPath; - auto maxPath = std::string{kControlMaxFieldNamePrefix} + matchExprPath; + // The date is in the "extended" range if it doesn't fit into the bottom + // 32 bits. + long long timestamp = timeField.toMillisSinceEpoch(); + dateIsExtended = timestamp < 0LL || timestamp > max32BitEpochMillis; + } switch (matchExpr->matchType()) { case MatchExpression::EQ: @@ -282,8 +324,9 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // For $eq, make both a $lte against 'control.min' and a $gte predicate against // 'control.max'. // - // If the comparison is against the 'time' field, include a predicate against the _id - // field which is converted to the maximum for the corresponding range of ObjectIds and + // If the comparison is against the 'time' field and we haven't stored a time outside of + // the 32 bit range, include a predicate against the _id field which is converted to + // the maximum for the corresponding range of ObjectIds and // is adjusted by the max range for a bucket to approximate the max bucket value given // the min. Also include a predicate against the _id field which is converted to the // minimum for the range of ObjectIds corresponding to the given date. In @@ -293,60 +336,91 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // // The same procedure applies to aggregation expressions of the form // {$expr: {$eq: [...]}} that can be rewritten to use $_internalExprEq. - return isTimeField - ? makePredicate( - MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), - MatchExprPredicate<InternalExprGTEMatchExpression>(minPath, - minTime.firstElement()), - MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData), - MatchExprPredicate<InternalExprLTEMatchExpression>(maxPath, - maxTime.firstElement()), - MatchExprPredicate<LTEMatchExpression, Value>( - kBucketIdFieldName, - constructObjectIdValue<LTEMatchExpression>(matchExprData, - bucketMaxSpanSeconds)), - MatchExprPredicate<GTEMatchExpression, Value>( - kBucketIdFieldName, - constructObjectIdValue<GTEMatchExpression>(matchExprData, - bucketMaxSpanSeconds))) - : makeOr(makeVector<std::unique_ptr<MatchExpression>>( - makePredicate(MatchExprPredicate<InternalExprLTEMatchExpression>( - minPath, matchExprData), - MatchExprPredicate<InternalExprGTEMatchExpression>( - maxPath, matchExprData)), - createTypeEqualityPredicate( - pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + if (!isTimeField) { + return makeOr(makeVector<std::unique_ptr<MatchExpression>>( + makePredicate( + MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData)), + createTypeEqualityPredicate(pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + } else if (bucketSpec.usesExtendedRange()) { + return makePredicate( + MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprGTEMatchExpression>(minPath, + minTime.firstElement()), + MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData), + MatchExprPredicate<InternalExprLTEMatchExpression>(maxPath, + maxTime.firstElement())); + } else if (dateIsExtended) { + // Since by this point we know that no time value has been inserted which is + // outside the epoch range, we know that no document can meet this criteria + return std::make_unique<AlwaysFalseMatchExpression>(); + } else { + return makePredicate( + MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprGTEMatchExpression>(minPath, + minTime.firstElement()), + MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData), + MatchExprPredicate<InternalExprLTEMatchExpression>(maxPath, + maxTime.firstElement()), + MatchExprPredicate<LTEMatchExpression, Value>( + kBucketIdFieldName, + constructObjectIdValue<LTEMatchExpression>(matchExprData, + bucketMaxSpanSeconds)), + MatchExprPredicate<GTEMatchExpression, Value>( + kBucketIdFieldName, + constructObjectIdValue<GTEMatchExpression>(matchExprData, + bucketMaxSpanSeconds))); + } + MONGO_UNREACHABLE_TASSERT(6646903); case MatchExpression::GT: case MatchExpression::INTERNAL_EXPR_GT: // For $gt, make a $gt predicate against 'control.max'. In addition, if the comparison - // is against the 'time' field, include a predicate against the _id field which is - // converted to the maximum for the corresponding range of ObjectIds and is adjusted - // by the max range for a bucket to approximate the max bucket value given the min. In - // addition, we include a {'control.min' : {$gt: 'time - bucketMaxSpanSeconds'}} + // is against the 'time' field, and the collection doesn't contain times outside the + // 32 bit range, include a predicate against the _id field which is converted to the + // maximum for the corresponding range of ObjectIds and is adjusted by the max range + // for a bucket to approximate the max bucket value given the min. + // + // In addition, we include a {'control.min' : {$gt: 'time - bucketMaxSpanSeconds'}} // predicate which will be helpful in reducing bounds for index scans on 'time' field // and routing on mongos. // // The same procedure applies to aggregation expressions of the form // {$expr: {$gt: [...]}} that can be rewritten to use $_internalExprGt. - return isTimeField - ? makePredicate( - MatchExprPredicate<InternalExprGTMatchExpression>(maxPath, matchExprData), - MatchExprPredicate<InternalExprGTMatchExpression>(minPath, - minTime.firstElement()), - MatchExprPredicate<GTMatchExpression, Value>( - kBucketIdFieldName, - constructObjectIdValue<GTMatchExpression>(matchExprData, - bucketMaxSpanSeconds))) - : makeOr(makeVector<std::unique_ptr<MatchExpression>>( - std::make_unique<InternalExprGTMatchExpression>(maxPath, matchExprData), - createTypeEqualityPredicate( - pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + if (!isTimeField) { + return makeOr(makeVector<std::unique_ptr<MatchExpression>>( + std::make_unique<InternalExprGTMatchExpression>(maxPath, matchExprData), + createTypeEqualityPredicate(pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + } else if (bucketSpec.usesExtendedRange()) { + return makePredicate( + MatchExprPredicate<InternalExprGTMatchExpression>(maxPath, matchExprData), + MatchExprPredicate<InternalExprGTMatchExpression>(minPath, + minTime.firstElement())); + } else if (matchExprData.Date().toMillisSinceEpoch() < 0LL) { + // Since by this point we know that no time value has been inserted < 0, + // every document must meet this criteria + return std::make_unique<AlwaysTrueMatchExpression>(); + } else if (matchExprData.Date().toMillisSinceEpoch() > max32BitEpochMillis) { + // Since by this point we know that no time value has been inserted > + // max32BitEpochMillis, we know that no document can meet this criteria + return std::make_unique<AlwaysFalseMatchExpression>(); + } else { + return makePredicate( + MatchExprPredicate<InternalExprGTMatchExpression>(maxPath, matchExprData), + MatchExprPredicate<InternalExprGTMatchExpression>(minPath, + minTime.firstElement()), + MatchExprPredicate<GTMatchExpression, Value>( + kBucketIdFieldName, + constructObjectIdValue<GTMatchExpression>(matchExprData, + bucketMaxSpanSeconds))); + } + MONGO_UNREACHABLE_TASSERT(6646904); case MatchExpression::GTE: case MatchExpression::INTERNAL_EXPR_GTE: // For $gte, make a $gte predicate against 'control.max'. In addition, if the comparison - // is against the 'time' field, include a predicate against the _id field which is + // is against the 'time' field, and the collection doesn't contain times outside the + // 32 bit range, include a predicate against the _id field which is // converted to the minimum for the corresponding range of ObjectIds and is adjusted // by the max range for a bucket to approximate the max bucket value given the min. In // addition, we include a {'control.min' : {$gte: 'time - bucketMaxSpanSeconds'}} @@ -355,49 +429,83 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // // The same procedure applies to aggregation expressions of the form // {$expr: {$gte: [...]}} that can be rewritten to use $_internalExprGte. - return isTimeField - ? makePredicate( - MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData), - MatchExprPredicate<InternalExprGTEMatchExpression>(minPath, - minTime.firstElement()), - MatchExprPredicate<GTEMatchExpression, Value>( - kBucketIdFieldName, - constructObjectIdValue<GTEMatchExpression>(matchExprData, - bucketMaxSpanSeconds))) - : makeOr(makeVector<std::unique_ptr<MatchExpression>>( - std::make_unique<InternalExprGTEMatchExpression>(maxPath, matchExprData), - createTypeEqualityPredicate( - pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + if (!isTimeField) { + return makeOr(makeVector<std::unique_ptr<MatchExpression>>( + std::make_unique<InternalExprGTEMatchExpression>(maxPath, matchExprData), + createTypeEqualityPredicate(pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + } else if (bucketSpec.usesExtendedRange()) { + return makePredicate( + MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData), + MatchExprPredicate<InternalExprGTEMatchExpression>(minPath, + minTime.firstElement())); + } else if (matchExprData.Date().toMillisSinceEpoch() < 0LL) { + // Since by this point we know that no time value has been inserted < 0, + // every document must meet this criteria + return std::make_unique<AlwaysTrueMatchExpression>(); + } else if (matchExprData.Date().toMillisSinceEpoch() > max32BitEpochMillis) { + // Since by this point we know that no time value has been inserted > 0xffffffff, + // we know that no value can meet this criteria + return std::make_unique<AlwaysFalseMatchExpression>(); + } else { + return makePredicate( + MatchExprPredicate<InternalExprGTEMatchExpression>(maxPath, matchExprData), + MatchExprPredicate<InternalExprGTEMatchExpression>(minPath, + minTime.firstElement()), + MatchExprPredicate<GTEMatchExpression, Value>( + kBucketIdFieldName, + constructObjectIdValue<GTEMatchExpression>(matchExprData, + bucketMaxSpanSeconds))); + } + MONGO_UNREACHABLE_TASSERT(6646905); case MatchExpression::LT: case MatchExpression::INTERNAL_EXPR_LT: // For $lt, make a $lt predicate against 'control.min'. In addition, if the comparison // is against the 'time' field, include a predicate against the _id field which is - // converted to the minimum for the corresponding range of ObjectIds. In - // addition, we include a {'control.max' : {$lt: 'time + bucketMaxSpanSeconds'}} + // converted to the minimum for the corresponding range of ObjectIds, unless the + // collection contain extended range dates which won't fit int the 32 bits allocated + // for _id. + // + // In addition, we include a {'control.max' : {$lt: 'time + bucketMaxSpanSeconds'}} // predicate which will be helpful in reducing bounds for index scans on 'time' field // and routing on mongos. // // The same procedure applies to aggregation expressions of the form // {$expr: {$lt: [...]}} that can be rewritten to use $_internalExprLt. - return isTimeField - ? makePredicate( - MatchExprPredicate<InternalExprLTMatchExpression>(minPath, matchExprData), - MatchExprPredicate<InternalExprLTMatchExpression>(maxPath, - maxTime.firstElement()), - MatchExprPredicate<LTMatchExpression, Value>( - kBucketIdFieldName, - constructObjectIdValue<LTMatchExpression>(matchExprData, - bucketMaxSpanSeconds))) - : makeOr(makeVector<std::unique_ptr<MatchExpression>>( - std::make_unique<InternalExprLTMatchExpression>(minPath, matchExprData), - createTypeEqualityPredicate( - pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + if (!isTimeField) { + return makeOr(makeVector<std::unique_ptr<MatchExpression>>( + std::make_unique<InternalExprLTMatchExpression>(minPath, matchExprData), + createTypeEqualityPredicate(pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + } else if (bucketSpec.usesExtendedRange()) { + return makePredicate( + MatchExprPredicate<InternalExprLTMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprLTMatchExpression>(maxPath, + maxTime.firstElement())); + } else if (matchExprData.Date().toMillisSinceEpoch() < 0LL) { + // Since by this point we know that no time value has been inserted < 0, + // we know that no document can meet this criteria + return std::make_unique<AlwaysFalseMatchExpression>(); + } else if (matchExprData.Date().toMillisSinceEpoch() > max32BitEpochMillis) { + // Since by this point we know that no time value has been inserted > 0xffffffff + // every time value must be less than this value + return std::make_unique<AlwaysTrueMatchExpression>(); + } else { + return makePredicate( + MatchExprPredicate<InternalExprLTMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprLTMatchExpression>(maxPath, + maxTime.firstElement()), + MatchExprPredicate<LTMatchExpression, Value>( + kBucketIdFieldName, + constructObjectIdValue<LTMatchExpression>(matchExprData, + bucketMaxSpanSeconds))); + } + MONGO_UNREACHABLE_TASSERT(6646906); case MatchExpression::LTE: case MatchExpression::INTERNAL_EXPR_LTE: // For $lte, make a $lte predicate against 'control.min'. In addition, if the comparison - // is against the 'time' field, include a predicate against the _id field which is + // is against the 'time' field, and the collection doesn't contain times outside the + // 32 bit range, include a predicate against the _id field which is // converted to the maximum for the corresponding range of ObjectIds. In // addition, we include a {'control.max' : {$lte: 'time + bucketMaxSpanSeconds'}} // predicate which will be helpful in reducing bounds for index scans on 'time' field @@ -405,19 +513,34 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // // The same procedure applies to aggregation expressions of the form // {$expr: {$lte: [...]}} that can be rewritten to use $_internalExprLte. - return isTimeField - ? makePredicate( - MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), - MatchExprPredicate<InternalExprLTEMatchExpression>(maxPath, - maxTime.firstElement()), - MatchExprPredicate<LTEMatchExpression, Value>( - kBucketIdFieldName, - constructObjectIdValue<LTEMatchExpression>(matchExprData, - bucketMaxSpanSeconds))) - : makeOr(makeVector<std::unique_ptr<MatchExpression>>( - std::make_unique<InternalExprLTEMatchExpression>(minPath, matchExprData), - createTypeEqualityPredicate( - pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + if (!isTimeField) { + return makeOr(makeVector<std::unique_ptr<MatchExpression>>( + std::make_unique<InternalExprLTEMatchExpression>(minPath, matchExprData), + createTypeEqualityPredicate(pExpCtx, matchExprPath, assumeNoMixedSchemaData))); + } else if (bucketSpec.usesExtendedRange()) { + return makePredicate( + MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprLTEMatchExpression>(maxPath, + maxTime.firstElement())); + } else if (matchExprData.Date().toMillisSinceEpoch() < 0LL) { + // Since by this point we know that no time value has been inserted < 0, + // we know that no document can meet this criteria + return std::make_unique<AlwaysFalseMatchExpression>(); + } else if (matchExprData.Date().toMillisSinceEpoch() > max32BitEpochMillis) { + // Since by this point we know that no time value has been inserted > 0xffffffff + // every document must be less than this value + return std::make_unique<AlwaysTrueMatchExpression>(); + } else { + return makePredicate( + MatchExprPredicate<InternalExprLTEMatchExpression>(minPath, matchExprData), + MatchExprPredicate<InternalExprLTEMatchExpression>(maxPath, + maxTime.firstElement()), + MatchExprPredicate<LTEMatchExpression, Value>( + kBucketIdFieldName, + constructObjectIdValue<LTEMatchExpression>(matchExprData, + bucketMaxSpanSeconds))); + } + MONGO_UNREACHABLE_TASSERT(6646907); default: MONGO_UNREACHABLE_TASSERT(5348302); @@ -426,9 +549,108 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( MONGO_UNREACHABLE_TASSERT(5348303); } +std::unique_ptr<MatchExpression> createTightComparisonPredicate( + const ComparisonMatchExpressionBase* matchExpr, + const BucketSpec& bucketSpec, + ExpressionContext::CollationMatchesDefault collationMatchesDefault) { + using namespace timeseries; + const auto matchExprPath = matchExpr->path(); + const auto matchExprData = matchExpr->getData(); + + const auto error = checkComparisonPredicateErrors( + matchExpr, matchExprPath, matchExprData, bucketSpec, collationMatchesDefault); + if (error) { + return handleIneligible(BucketSpec::IneligiblePredicatePolicy::kIgnore, matchExpr, *error) + .loosePredicate; + } + + // We have to disable the tight predicate for the measurement field. There might be missing + // values in the measurements and the control fields ignore them on insertion. So we cannot use + // bucket min and max to determine the property of all events in the bucket. For measurement + // fields, there's a further problem that if the control field is an array, we cannot generate + // the tight predicate because the predicate will be implicitly mapped over the array elements. + if (matchExprPath != bucketSpec.timeField()) { + return handleIneligible(BucketSpec::IneligiblePredicatePolicy::kIgnore, + matchExpr, + "can't create tight predicate on non-time field") + .tightPredicate; + } + + auto minPath = std::string{kControlMinFieldNamePrefix} + matchExprPath; + const StringData minPathStringData(minPath); + auto maxPath = std::string{kControlMaxFieldNamePrefix} + matchExprPath; + const StringData maxPathStringData(maxPath); + + switch (matchExpr->matchType()) { + // All events satisfy $eq if bucket min and max both satisfy $eq. + case MatchExpression::EQ: + return makePredicate( + MatchExprPredicate<EqualityMatchExpression>(minPathStringData, matchExprData), + MatchExprPredicate<EqualityMatchExpression>(maxPathStringData, matchExprData)); + case MatchExpression::INTERNAL_EXPR_EQ: + return makePredicate( + MatchExprPredicate<InternalExprEqMatchExpression>(minPathStringData, matchExprData), + MatchExprPredicate<InternalExprEqMatchExpression>(maxPathStringData, + matchExprData)); + + // All events satisfy $gt if bucket min satisfy $gt. + case MatchExpression::GT: + return std::make_unique<GTMatchExpression>(minPathStringData, matchExprData); + case MatchExpression::INTERNAL_EXPR_GT: + return std::make_unique<InternalExprGTMatchExpression>(minPathStringData, + matchExprData); + + // All events satisfy $gte if bucket min satisfy $gte. + case MatchExpression::GTE: + return std::make_unique<GTEMatchExpression>(minPathStringData, matchExprData); + case MatchExpression::INTERNAL_EXPR_GTE: + return std::make_unique<InternalExprGTEMatchExpression>(minPathStringData, + matchExprData); + + // All events satisfy $lt if bucket max satisfy $lt. + case MatchExpression::LT: + return std::make_unique<LTMatchExpression>(maxPathStringData, matchExprData); + case MatchExpression::INTERNAL_EXPR_LT: + return std::make_unique<InternalExprLTMatchExpression>(maxPathStringData, + matchExprData); + + // All events satisfy $lte if bucket max satisfy $lte. + case MatchExpression::LTE: + return std::make_unique<LTEMatchExpression>(maxPathStringData, matchExprData); + case MatchExpression::INTERNAL_EXPR_LTE: + return std::make_unique<InternalExprLTEMatchExpression>(maxPathStringData, + matchExprData); + + default: + MONGO_UNREACHABLE_TASSERT(7026901); + } +} + +std::unique_ptr<MatchExpression> createTightExprComparisonPredicate( + const ExprMatchExpression* matchExpr, + const BucketSpec& bucketSpec, + ExpressionContext::CollationMatchesDefault collationMatchesDefault, + boost::intrusive_ptr<ExpressionContext> pExpCtx) { + using namespace timeseries; + auto rewriteMatchExpr = RewriteExpr::rewrite(matchExpr->getExpression(), pExpCtx->getCollator()) + .releaseMatchExpression(); + if (rewriteMatchExpr && + ComparisonMatchExpressionBase::isInternalExprComparison(rewriteMatchExpr->matchType())) { + auto compareMatchExpr = + checked_cast<const ComparisonMatchExpressionBase*>(rewriteMatchExpr.get()); + return createTightComparisonPredicate( + compareMatchExpr, bucketSpec, collationMatchesDefault); + } + + return handleIneligible(BucketSpec::IneligiblePredicatePolicy::kIgnore, + matchExpr, + "can't handle non-comparison $expr match expression") + .tightPredicate; +} + } // namespace -std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( +BucketSpec::BucketPredicate BucketSpec::createPredicatesOnBucketLevelField( const MatchExpression* matchExpr, const BucketSpec& bucketSpec, int bucketMaxSpanSeconds, @@ -443,7 +665,8 @@ std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( // If we have a leaf predicate on a meta field, we can map it to the bucket's meta field. // This includes comparisons such as $eq and $lte, as well as other non-comparison predicates - // such as $exists, $mod, or $elemMatch. + // such as $exists, or $mod. Unrenamable expressions can't be split into a whole bucket level + // filter, when we should return nullptr. // // Metadata predicates are partially handled earlier, by splitting the match expression into a // metadata-only part, and measurement/time-only part. However, splitting a $match into two @@ -461,39 +684,65 @@ std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( if (!includeMetaField) return handleIneligible(policy, matchExpr, "cannot handle an excluded meta field"); - auto result = matchExpr->shallowClone(); - expression::applyRenamesToExpression( - result.get(), - {{bucketSpec.metaField().get(), timeseries::kBucketMetaFieldName.toString()}}); - return result; + if (expression::hasOnlyRenameableMatchExpressionChildren(*matchExpr)) { + auto looseResult = matchExpr->shallowClone(); + expression::applyRenamesToExpression( + looseResult.get(), + {{bucketSpec.metaField().value(), timeseries::kBucketMetaFieldName.toString()}}); + auto tightResult = looseResult->shallowClone(); + return {std::move(looseResult), std::move(tightResult)}; + } else { + return {nullptr, nullptr}; + } } if (matchExpr->matchType() == MatchExpression::AND) { auto nextAnd = static_cast<const AndMatchExpression*>(matchExpr); - auto andMatchExpr = std::make_unique<AndMatchExpression>(); - + auto looseAndExpression = std::make_unique<AndMatchExpression>(); + auto tightAndExpression = std::make_unique<AndMatchExpression>(); for (size_t i = 0; i < nextAnd->numChildren(); i++) { - if (auto child = createPredicatesOnBucketLevelField(nextAnd->getChild(i), - bucketSpec, - bucketMaxSpanSeconds, - collationMatchesDefault, - pExpCtx, - haveComputedMetaField, - includeMetaField, - assumeNoMixedSchemaData, - policy)) { - andMatchExpr->add(std::move(child)); + auto child = createPredicatesOnBucketLevelField(nextAnd->getChild(i), + bucketSpec, + bucketMaxSpanSeconds, + collationMatchesDefault, + pExpCtx, + haveComputedMetaField, + includeMetaField, + assumeNoMixedSchemaData, + policy); + if (child.loosePredicate) { + looseAndExpression->add(std::move(child.loosePredicate)); + } + + if (tightAndExpression && child.tightPredicate) { + tightAndExpression->add(std::move(child.tightPredicate)); + } else { + // For tight expression, null means always false, we can short circuit here. + tightAndExpression = nullptr; } } - if (andMatchExpr->numChildren() == 1) { - return andMatchExpr->releaseChild(0); + + // For a loose predicate, if we are unable to generate an expression we can just treat it as + // always true or an empty AND. This is because we are trying to generate a predicate that + // will match the superset of our actual results. + std::unique_ptr<MatchExpression> looseExpression = nullptr; + if (looseAndExpression->numChildren() == 1) { + looseExpression = looseAndExpression->releaseChild(0); + } else if (looseAndExpression->numChildren() > 1) { + looseExpression = std::move(looseAndExpression); } - if (andMatchExpr->numChildren() > 0) { - return andMatchExpr; + + // For a tight predicate, if we are unable to generate an expression we can just treat it as + // always false. This is because we are trying to generate a predicate that will match the + // subset of our actual results. + std::unique_ptr<MatchExpression> tightExpression = nullptr; + if (tightAndExpression && tightAndExpression->numChildren() == 1) { + tightExpression = tightAndExpression->releaseChild(0); + } else { + tightExpression = std::move(tightAndExpression); } - // No error message here: an empty AND is valid. - return nullptr; + return {std::move(looseExpression), std::move(tightExpression)}; } else if (matchExpr->matchType() == MatchExpression::OR) { // Given {$or: [A, B]}, suppose A, B can be pushed down as A', B'. // If an event matches {$or: [A, B]} then either: @@ -501,9 +750,9 @@ std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( // - it matches B, which means any bucket containing it matches B' // So {$or: [A', B']} will capture all the buckets we need to satisfy {$or: [A, B]}. auto nextOr = static_cast<const OrMatchExpression*>(matchExpr); - auto result = std::make_unique<OrMatchExpression>(); + auto looseOrExpression = std::make_unique<OrMatchExpression>(); + auto tightOrExpression = std::make_unique<OrMatchExpression>(); - bool alwaysTrue = false; for (size_t i = 0; i < nextOr->numChildren(); i++) { auto child = createPredicatesOnBucketLevelField(nextOr->getChild(i), bucketSpec, @@ -514,51 +763,86 @@ std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( includeMetaField, assumeNoMixedSchemaData, policy); - if (child) { - result->add(std::move(child)); + if (looseOrExpression && child.loosePredicate) { + looseOrExpression->add(std::move(child.loosePredicate)); } else { - // Since this argument is always-true, the entire OR is always-true. - alwaysTrue = true; + // For loose expression, null means always true, we can short circuit here. + looseOrExpression = nullptr; + } - // Only short circuit if we're uninterested in reporting errors. - if (policy == IneligiblePredicatePolicy::kIgnore) - break; + // For tight predicate, we give a tighter bound so that all events in the bucket + // either all matches A or all matches B. + if (child.tightPredicate) { + tightOrExpression->add(std::move(child.tightPredicate)); } } - if (alwaysTrue) - return nullptr; - // No special case for an empty OR: returning nullptr would be incorrect because it - // means 'always-true', here. - return result; + // For a loose predicate, if we are unable to generate an expression we can just treat it as + // always true. This is because we are trying to generate a predicate that will match the + // superset of our actual results. + std::unique_ptr<MatchExpression> looseExpression = nullptr; + if (looseOrExpression && looseOrExpression->numChildren() == 1) { + looseExpression = looseOrExpression->releaseChild(0); + } else { + looseExpression = std::move(looseOrExpression); + } + + // For a tight predicate, if we are unable to generate an expression we can just treat it as + // always false or an empty OR. This is because we are trying to generate a predicate that + // will match the subset of our actual results. + std::unique_ptr<MatchExpression> tightExpression = nullptr; + if (tightOrExpression->numChildren() == 1) { + tightExpression = tightOrExpression->releaseChild(0); + } else if (tightOrExpression->numChildren() > 1) { + tightExpression = std::move(tightOrExpression); + } + + return {std::move(looseExpression), std::move(tightExpression)}; } else if (ComparisonMatchExpression::isComparisonMatchExpression(matchExpr) || ComparisonMatchExpressionBase::isInternalExprComparison(matchExpr->matchType())) { - return createComparisonPredicate( - checked_cast<const ComparisonMatchExpressionBase*>(matchExpr), - bucketSpec, - bucketMaxSpanSeconds, - collationMatchesDefault, - pExpCtx, - haveComputedMetaField, - includeMetaField, - assumeNoMixedSchemaData, - policy); + return { + createComparisonPredicate(checked_cast<const ComparisonMatchExpressionBase*>(matchExpr), + bucketSpec, + bucketMaxSpanSeconds, + collationMatchesDefault, + pExpCtx, + haveComputedMetaField, + includeMetaField, + assumeNoMixedSchemaData, + policy), + createTightComparisonPredicate( + checked_cast<const ComparisonMatchExpressionBase*>(matchExpr), + bucketSpec, + collationMatchesDefault)}; + } else if (matchExpr->matchType() == MatchExpression::EXPRESSION) { + return { + // The loose predicate will be pushed before the unpacking which will be inspected by + // the + // query planner. Since the classic planner doesn't handle the $expr expression, we + // don't + // generate the loose predicate. + nullptr, + createTightExprComparisonPredicate(checked_cast<const ExprMatchExpression*>(matchExpr), + bucketSpec, + collationMatchesDefault, + pExpCtx)}; } else if (matchExpr->matchType() == MatchExpression::GEO) { auto& geoExpr = static_cast<const GeoMatchExpression*>(matchExpr)->getGeoExpression(); if (geoExpr.getPred() == GeoExpression::WITHIN || geoExpr.getPred() == GeoExpression::INTERSECT) { - return std::make_unique<InternalBucketGeoWithinMatchExpression>( - geoExpr.getGeometryPtr(), geoExpr.getField()); + return {std::make_unique<InternalBucketGeoWithinMatchExpression>( + geoExpr.getGeometryPtr(), geoExpr.getField()), + nullptr}; } } else if (matchExpr->matchType() == MatchExpression::EXISTS) { if (assumeNoMixedSchemaData) { // We know that every field that appears in an event will also appear in the min/max. auto result = std::make_unique<AndMatchExpression>(); - result->add(std::make_unique<ExistsMatchExpression>( - std::string{timeseries::kControlMinFieldNamePrefix} + matchExpr->path())); - result->add(std::make_unique<ExistsMatchExpression>( - std::string{timeseries::kControlMaxFieldNamePrefix} + matchExpr->path())); - return result; + result->add(std::make_unique<ExistsMatchExpression>(StringData( + std::string{timeseries::kControlMinFieldNamePrefix} + matchExpr->path()))); + result->add(std::make_unique<ExistsMatchExpression>(StringData( + std::string{timeseries::kControlMaxFieldNamePrefix} + matchExpr->path()))); + return {std::move(result), nullptr}; } else { // At time of writing, we only pass 'kError' when creating a partial index, and // we know the collection will have no mixed-schema buckets by the time the index is @@ -567,7 +851,7 @@ std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( "Can't push down {$exists: true} when the collection may have mixed-schema " "buckets.", policy != IneligiblePredicatePolicy::kError); - return nullptr; + return {}; } } else if (matchExpr->matchType() == MatchExpression::MATCH_IN) { // {a: {$in: [X, Y]}} is equivalent to {$or: [ {a: X}, {a: Y} ]}. @@ -609,11 +893,11 @@ std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( } } if (alwaysTrue) - return nullptr; + return {}; // As above, no special case for an empty IN: returning nullptr would be incorrect because // it means 'always-true', here. - return result; + return {std::move(result), nullptr}; } return handleIneligible(policy, matchExpr, "can't handle this predicate"); } @@ -657,12 +941,10 @@ BSONObj BucketSpec::pushdownPredicate( BucketSpec{ tsOptions.getTimeField().toString(), metaField.map([](StringData s) { return s.toString(); }), - // Since we are operating on a collection, not a query-result, there are no - // inclusion/exclusion projections we need to apply to the buckets before - // unpacking. - {}, - // And there are no computed projections. - {}, + // Since we are operating on a collection, not a query-result, + // there are no inclusion/exclusion projections we need to apply + // to the buckets before unpacking. So we can use default values for the rest of + // the arguments. }, maxSpanSeconds, collationMatchesDefault, @@ -671,6 +953,7 @@ BSONObj BucketSpec::pushdownPredicate( includeMetaField, assumeNoMixedSchemaData, policy) + .loosePredicate : nullptr; BSONObjBuilder result; @@ -693,11 +976,15 @@ public: const Value& metaValue, bool includeTimeField, bool includeMetaField) = 0; + virtual bool getNext(BSONObjBuilder& builder, + const BucketSpec& spec, + const BSONElement& metaValue, + bool includeTimeField, + bool includeMetaField) = 0; virtual void extractSingleMeasurement(MutableDocument& measurement, int j, const BucketSpec& spec, const std::set<std::string>& unpackFieldsToIncludeExclude, - BucketUnpacker::Behavior behavior, const BSONObj& bucket, const Value& metaValue, bool includeTimeField, @@ -745,11 +1032,15 @@ public: const Value& metaValue, bool includeTimeField, bool includeMetaField) override; + bool getNext(BSONObjBuilder& builder, + const BucketSpec& spec, + const BSONElement& metaValue, + bool includeTimeField, + bool includeMetaField) override; void extractSingleMeasurement(MutableDocument& measurement, int j, const BucketSpec& spec, const std::set<std::string>& unpackFieldsToIncludeExclude, - BucketUnpacker::Behavior behavior, const BSONObj& bucket, const Value& metaValue, bool includeTimeField, @@ -761,7 +1052,7 @@ private: BSONObjIterator _timeFieldIter; // Iterators used to unpack the columns of the above bucket that are populated during the reset - // phase according to the provided 'Behavior' and 'BucketSpec'. + // phase according to the provided 'BucketSpec'. std::vector<std::pair<std::string, BSONObjIterator>> _fieldIters; }; @@ -831,12 +1122,37 @@ bool BucketUnpackerV1::getNext(MutableDocument& measurement, return _timeFieldIter.more(); } +bool BucketUnpackerV1::getNext(BSONObjBuilder& builder, + const BucketSpec& spec, + const BSONElement& metaValue, + bool includeTimeField, + bool includeMetaField) { + auto&& timeElem = _timeFieldIter.next(); + if (includeTimeField) { + builder.appendAs(timeElem, spec.timeField()); + } + + // Includes metaField when we're instructed to do so and metaField value exists. + if (includeMetaField && !metaValue.eoo()) { + builder.appendAs(metaValue, *spec.metaField()); + } + + const auto& currentIdx = timeElem.fieldNameStringData(); + for (auto&& [colName, colIter] : _fieldIters) { + if (auto&& elem = *colIter; colIter.more() && elem.fieldNameStringData() == currentIdx) { + builder.appendAs(elem, colName); + colIter.advance(elem); + } + } + + return _timeFieldIter.more(); +} + void BucketUnpackerV1::extractSingleMeasurement( MutableDocument& measurement, int j, const BucketSpec& spec, const std::set<std::string>& unpackFieldsToIncludeExclude, - BucketUnpacker::Behavior behavior, const BSONObj& bucket, const Value& metaValue, bool includeTimeField, @@ -851,7 +1167,7 @@ void BucketUnpackerV1::extractSingleMeasurement( for (auto&& dataElem : dataRegion) { auto colName = dataElem.fieldNameStringData(); - if (!determineIncludeField(colName, behavior, unpackFieldsToIncludeExclude)) { + if (!determineIncludeField(colName, spec.behavior(), unpackFieldsToIncludeExclude)) { continue; } auto value = dataElem[targetIdx]; @@ -879,11 +1195,15 @@ public: const Value& metaValue, bool includeTimeField, bool includeMetaField) override; + bool getNext(BSONObjBuilder& builder, + const BucketSpec& spec, + const BSONElement& metaValue, + bool includeTimeField, + bool includeMetaField) override; void extractSingleMeasurement(MutableDocument& measurement, int j, const BucketSpec& spec, const std::set<std::string>& unpackFieldsToIncludeExclude, - BucketUnpacker::Behavior behavior, const BSONObj& bucket, const Value& metaValue, bool includeTimeField, @@ -913,7 +1233,7 @@ private: ColumnStore _timeColumn; // Iterators used to unpack the columns of the above bucket that are populated during the reset - // phase according to the provided 'Behavior' and 'BucketSpec'. + // phase according to the provided 'BucketSpec'. std::vector<ColumnStore> _fieldColumns; // Element count @@ -968,12 +1288,43 @@ bool BucketUnpackerV2::getNext(MutableDocument& measurement, return _timeColumn.it != _timeColumn.end; } +bool BucketUnpackerV2::getNext(BSONObjBuilder& builder, + const BucketSpec& spec, + const BSONElement& metaValue, + bool includeTimeField, + bool includeMetaField) { + // Get element and increment iterator + const auto& timeElem = *_timeColumn.it; + if (includeTimeField) { + builder.appendAs(timeElem, spec.timeField()); + } + ++_timeColumn.it; + + // Includes metaField when we're instructed to do so and metaField value exists. + if (includeMetaField && !metaValue.eoo()) { + builder.appendAs(metaValue, *spec.metaField()); + } + + for (auto& fieldColumn : _fieldColumns) { + uassert(7026803, + "Bucket unexpectedly contained fewer values than count", + fieldColumn.it != fieldColumn.end); + const BSONElement& elem = *fieldColumn.it; + // EOO represents missing field + if (!elem.eoo()) { + builder.appendAs(elem, fieldColumn.column.name()); + } + ++fieldColumn.it; + } + + return _timeColumn.it != _timeColumn.end; +} + void BucketUnpackerV2::extractSingleMeasurement( MutableDocument& measurement, int j, const BucketSpec& spec, const std::set<std::string>& unpackFieldsToIncludeExclude, - BucketUnpacker::Behavior behavior, const BSONObj& bucket, const Value& metaValue, bool includeTimeField, @@ -1009,12 +1360,16 @@ std::size_t BucketUnpackerV2::numberOfFields() { BucketSpec::BucketSpec(const std::string& timeField, const boost::optional<std::string>& metaField, const std::set<std::string>& fields, - const std::set<std::string>& computedProjections) + Behavior behavior, + const std::set<std::string>& computedProjections, + bool usesExtendedRange) : _fieldSet(fields), + _behavior(behavior), _computedMetaProjFields(computedProjections), _timeField(timeField), _timeFieldHashed(FieldNameHasher().hashedFieldName(_timeField)), - _metaField(metaField) { + _metaField(metaField), + _usesExtendedRange(usesExtendedRange) { if (_metaField) { _metaFieldHashed = FieldNameHasher().hashedFieldName(*_metaField); } @@ -1022,10 +1377,12 @@ BucketSpec::BucketSpec(const std::string& timeField, BucketSpec::BucketSpec(const BucketSpec& other) : _fieldSet(other._fieldSet), + _behavior(other._behavior), _computedMetaProjFields(other._computedMetaProjFields), _timeField(other._timeField), _timeFieldHashed(HashedFieldName{_timeField, other._timeFieldHashed->hash()}), - _metaField(other._metaField) { + _metaField(other._metaField), + _usesExtendedRange(other._usesExtendedRange) { if (_metaField) { _metaFieldHashed = HashedFieldName{*_metaField, other._metaFieldHashed->hash()}; } @@ -1033,10 +1390,12 @@ BucketSpec::BucketSpec(const BucketSpec& other) BucketSpec::BucketSpec(BucketSpec&& other) : _fieldSet(std::move(other._fieldSet)), + _behavior(other._behavior), _computedMetaProjFields(std::move(other._computedMetaProjFields)), _timeField(std::move(other._timeField)), _timeFieldHashed(HashedFieldName{_timeField, other._timeFieldHashed->hash()}), - _metaField(std::move(other._metaField)) { + _metaField(std::move(other._metaField)), + _usesExtendedRange(other._usesExtendedRange) { if (_metaField) { _metaFieldHashed = HashedFieldName{*_metaField, other._metaFieldHashed->hash()}; } @@ -1045,6 +1404,7 @@ BucketSpec::BucketSpec(BucketSpec&& other) BucketSpec& BucketSpec::operator=(const BucketSpec& other) { if (&other != this) { _fieldSet = other._fieldSet; + _behavior = other._behavior; _computedMetaProjFields = other._computedMetaProjFields; _timeField = other._timeField; _timeFieldHashed = HashedFieldName{_timeField, other._timeFieldHashed->hash()}; @@ -1052,6 +1412,7 @@ BucketSpec& BucketSpec::operator=(const BucketSpec& other) { if (_metaField) { _metaFieldHashed = HashedFieldName{*_metaField, other._metaFieldHashed->hash()}; } + _usesExtendedRange = other._usesExtendedRange; } return *this; } @@ -1093,8 +1454,8 @@ BucketUnpacker::BucketUnpacker(BucketUnpacker&& other) = default; BucketUnpacker::~BucketUnpacker() = default; BucketUnpacker& BucketUnpacker::operator=(BucketUnpacker&& rhs) = default; -BucketUnpacker::BucketUnpacker(BucketSpec spec, Behavior unpackerBehavior) { - setBucketSpecAndBehavior(std::move(spec), unpackerBehavior); +BucketUnpacker::BucketUnpacker(BucketSpec spec) { + setBucketSpec(std::move(spec)); } void BucketUnpacker::addComputedMetaProjFields(const std::vector<StringData>& computedFieldNames) { @@ -1103,7 +1464,7 @@ void BucketUnpacker::addComputedMetaProjFields(const std::vector<StringData>& co // If we're already specifically including fields, we need to add the computed fields to // the included field set to indicate they're in the output doc. - if (_unpackerBehavior == BucketUnpacker::Behavior::kInclude) { + if (_spec.behavior() == BucketSpec::Behavior::kInclude) { _spec.addIncludeExcludeField(field); } else { // Since exclude is applied after addComputedMetaProjFields, we must erase the new field @@ -1144,6 +1505,25 @@ Document BucketUnpacker::getNext() { return measurement.freeze(); } +BSONObj BucketUnpacker::getNextBson() { + tassert(7026800, "'getNextBson()' requires the bucket to be owned", _bucket.isOwned()); + tassert(7026801, "'getNextBson()' was called after the bucket has been exhausted", hasNext()); + tassert(7026802, + "'getNextBson()' cannot return max and min time as metadata", + !_includeMaxTimeAsMetadata && !_includeMinTimeAsMetadata); + + BSONObjBuilder builder; + _hasNext = _unpackingImpl->getNext( + builder, _spec, _metaBSONElem, _includeTimeField, _includeMetaField); + + // Add computed meta projections. + for (auto&& name : _spec.computedMetaProjFields()) { + builder.appendAs(_computedMetaProjections[name], name); + } + + return builder.obj(); +} + Document BucketUnpacker::extractSingleMeasurement(int j) { tassert(5422101, "'extractSingleMeasurment' expects j to be greater than or equal to zero and less than " @@ -1155,7 +1535,6 @@ Document BucketUnpacker::extractSingleMeasurement(int j) { j, _spec, fieldsToIncludeExcludeDuringUnpack(), - _unpackerBehavior, _bucket, _metaValue, _includeTimeField, @@ -1169,9 +1548,10 @@ Document BucketUnpacker::extractSingleMeasurement(int j) { return measurement.freeze(); } -void BucketUnpacker::reset(BSONObj&& bucket) { +void BucketUnpacker::reset(BSONObj&& bucket, bool bucketMatchedQuery) { _unpackingImpl.reset(); _bucket = std::move(bucket); + _bucketMatchedQuery = bucketMatchedQuery; uassert(5346510, "An empty bucket cannot be unpacked", !_bucket.isEmpty()); auto&& dataRegion = _bucket.getField(timeseries::kBucketDataFieldName).Obj(); @@ -1186,7 +1566,8 @@ void BucketUnpacker::reset(BSONObj&& bucket) { "The $_internalUnpackBucket stage requires the data region to have a timeField object", timeFieldElem); - _metaValue = Value{_bucket[timeseries::kBucketMetaFieldName]}; + _metaBSONElem = _bucket[timeseries::kBucketMetaFieldName]; + _metaValue = Value{_metaBSONElem}; if (_spec.metaField()) { // The spec indicates that there might be a metadata region. Missing metadata in // measurements is expressed with missing metadata in a bucket. But we disallow undefined @@ -1268,10 +1649,10 @@ void BucketUnpacker::reset(BSONObj&& bucket) { continue; } - // Includes a field when '_unpackerBehavior' is 'kInclude' and it's found in 'fieldSet' or - // _unpackerBehavior is 'kExclude' and it's not found in 'fieldSet'. + // Includes a field when '_spec.behavior()' is 'kInclude' and it's found in 'fieldSet' or + // _spec.behavior() is 'kExclude' and it's not found in 'fieldSet'. if (determineIncludeField( - colName, _unpackerBehavior, fieldsToIncludeExcludeDuringUnpack())) { + colName, _spec.behavior(), fieldsToIncludeExcludeDuringUnpack())) { _unpackingImpl->addField(elem); } } @@ -1322,7 +1703,7 @@ int BucketUnpacker::computeMeasurementCount(const BSONObj& bucket, StringData ti } void BucketUnpacker::determineIncludeTimeField() { - const bool isInclude = _unpackerBehavior == BucketUnpacker::Behavior::kInclude; + const bool isInclude = _spec.behavior() == BucketSpec::Behavior::kInclude; const bool fieldSetContainsTime = _spec.fieldSet().find(_spec.timeField()) != _spec.fieldSet().end(); @@ -1342,22 +1723,21 @@ void BucketUnpacker::eraseMetaFromFieldSetAndDetermineIncludeMeta() { } else if (auto itr = _spec.fieldSet().find(*_spec.metaField()); itr != _spec.fieldSet().end()) { _spec.removeIncludeExcludeField(*_spec.metaField()); - _includeMetaField = _unpackerBehavior == BucketUnpacker::Behavior::kInclude; + _includeMetaField = _spec.behavior() == BucketSpec::Behavior::kInclude; } else { - _includeMetaField = _unpackerBehavior == BucketUnpacker::Behavior::kExclude; + _includeMetaField = _spec.behavior() == BucketSpec::Behavior::kExclude; } } void BucketUnpacker::eraseExcludedComputedMetaProjFields() { - if (_unpackerBehavior == BucketUnpacker::Behavior::kExclude) { + if (_spec.behavior() == BucketSpec::Behavior::kExclude) { for (const auto& field : _spec.fieldSet()) { _spec.eraseFromComputedMetaProjFields(field); } } } -void BucketUnpacker::setBucketSpecAndBehavior(BucketSpec&& bucketSpec, Behavior behavior) { - _unpackerBehavior = behavior; +void BucketUnpacker::setBucketSpec(BucketSpec&& bucketSpec) { _spec = std::move(bucketSpec); eraseMetaFromFieldSetAndDetermineIncludeMeta(); @@ -1383,7 +1763,7 @@ const std::set<std::string>& BucketUnpacker::fieldsToIncludeExcludeDuringUnpack( _unpackFieldsToIncludeExclude = std::set<std::string>(); const auto& metaProjFields = _spec.computedMetaProjFields(); - if (_unpackerBehavior == BucketUnpacker::Behavior::kInclude) { + if (_spec.behavior() == BucketSpec::Behavior::kInclude) { // For include, we unpack fieldSet - metaProjFields. for (auto&& field : _spec.fieldSet()) { if (metaProjFields.find(field) == metaProjFields.cend()) { diff --git a/src/mongo/db/exec/bucket_unpacker.h b/src/mongo/db/exec/bucket_unpacker.h index 287bd9f2540..29f2f1f30d2 100644 --- a/src/mongo/db/exec/bucket_unpacker.h +++ b/src/mongo/db/exec/bucket_unpacker.h @@ -54,11 +54,18 @@ namespace mongo { */ class BucketSpec { public: + // When unpackin buckets with kInclude we must produce measurements that contain the + // set of fields. Otherwise, if the kExclude option is used, the measurements will include the + // set difference between all fields in the bucket and the provided fields. + enum class Behavior { kInclude, kExclude }; + BucketSpec() = default; BucketSpec(const std::string& timeField, const boost::optional<std::string>& metaField, const std::set<std::string>& fields = {}, - const std::set<std::string>& computedProjections = {}); + Behavior behavior = Behavior::kExclude, + const std::set<std::string>& computedProjections = {}, + bool usesExtendedRange = false); BucketSpec(const BucketSpec&); BucketSpec(BucketSpec&&); @@ -92,6 +99,14 @@ public: return _fieldSet; } + void setBehavior(Behavior behavior) { + _behavior = behavior; + } + + Behavior behavior() const { + return _behavior; + } + void addComputedMetaProjFields(const StringData& field) { _computedMetaProjFields.emplace(field); } @@ -104,6 +119,14 @@ public: _computedMetaProjFields.erase(field); } + void setUsesExtendedRange(bool usesExtendedRange) { + _usesExtendedRange = usesExtendedRange; + } + + bool usesExtendedRange() const { + return _usesExtendedRange; + } + // Returns whether 'field' depends on a pushed down $addFields or computed $project. bool fieldIsComputed(StringData field) const; @@ -118,27 +141,45 @@ public: kError, }; + struct BucketPredicate { + // A loose predicate is a predicate which returns true when any measures of a bucket + // matches. + std::unique_ptr<MatchExpression> loosePredicate; + + // A tight predicate is a predicate which returns true when all measures of a bucket + // matches. + std::unique_ptr<MatchExpression> tightPredicate; + }; + /** - * Takes a predicate after $_internalUnpackBucket on a bucketed field as an argument and - * attempts to map it to a new predicate on the 'control' field. For example, the predicate - * {a: {$gt: 5}} will generate the predicate {control.max.a: {$_internalExprGt: 5}}, which will - * be added before the $_internalUnpackBucket stage. + * Takes a predicate after $_internalUnpackBucket as an argument and attempts to rewrite it as + * new predicates on the 'control' field. There will be a 'loose' predicate that will match if + * some of the event field matches, also a 'tight' predicate that will match if all of the event + * field matches. * - * If the original predicate is on the bucket's timeField we may also create a new predicate - * on the '_id' field to assist in index utilization. For example, the predicate - * {time: {$lt: new Date(...)}} will generate the following predicate: + * For example, the event level predicate {a: {$gt: 5}} will generate the loose predicate + * {control.max.a: {$_internalExprGt: 5}}. The loose predicate will be added before the + * $_internalUnpackBucket stage to filter out buckets with no match. + * + * Ideally, we'd like to add a tight predicate such as {control.min.a: {$_internalExprGt: 5}} to + * evaluate the filter on bucket level to avoid unnecessary event level evaluation. However, a + * bucket might contain events with missing fields that are skipped when computing the controls, + * so in reality we only add a tight predicate on timeField which is required to exist. + * + * If the original predicate is on the bucket's timeField we may also create a new loose + * predicate on the '_id' field (as it incorporates min time for the bucket) to assist in index + * utilization. For example, the predicate {time: {$lt: new Date(...)}} will generate the + * following predicate: * {$and: [ * {_id: {$lt: ObjectId(...)}}, * {control.min.time: {$_internalExprLt: new Date(...)}} * ]} * - * If the provided predicate is ineligible for this mapping, the function will return a nullptr. - * This should be interpreted as an always-true predicate. - * - * When using IneligiblePredicatePolicy::kIgnore, if the predicate can't be pushed down, it - * returns null. When using IneligiblePredicatePolicy::kError it raises a user error. + * If the provided predicate is ineligible for this mapping and using + * IneligiblePredicatePolicy::kIgnore, both loose and tight predicates will be set to nullptr. + * When using IneligiblePredicatePolicy::kError it raises a user error. */ - static std::unique_ptr<MatchExpression> createPredicatesOnBucketLevelField( + static BucketPredicate createPredicatesOnBucketLevelField( const MatchExpression* matchExpr, const BucketSpec& bucketSpec, int bucketMaxSpanSeconds, @@ -184,6 +225,7 @@ public: private: // The set of field names in the data region that should be included or excluded. std::set<std::string> _fieldSet; + Behavior _behavior = Behavior::kExclude; // Set of computed meta field projection names. Added at the end of materialized // measurements. @@ -194,6 +236,7 @@ private: boost::optional<std::string> _metaField = boost::none; boost::optional<HashedFieldName> _metaFieldHashed = boost::none; + bool _usesExtendedRange = false; }; /** @@ -201,10 +244,6 @@ private: */ class BucketUnpacker { public: - // When BucketUnpacker is created with kInclude it must produce measurements that contain the - // set of fields. Otherwise, if the kExclude option is used, the measurements will include the - // set difference between all fields in the bucket and the provided fields. - enum class Behavior { kInclude, kExclude }; /** * Returns the number of measurements in the bucket in O(1) time. */ @@ -214,7 +253,7 @@ public: static const std::set<StringData> reservedBucketFieldNames; BucketUnpacker(); - BucketUnpacker(BucketSpec spec, Behavior unpackerBehavior); + BucketUnpacker(BucketSpec spec); BucketUnpacker(const BucketUnpacker& other) = delete; BucketUnpacker(BucketUnpacker&& other); ~BucketUnpacker(); @@ -228,6 +267,11 @@ public: Document getNext(); /** + * Similar to the previous method, but return a BSON object instead. + */ + BSONObj getNextBson(); + + /** * This method will extract the j-th measurement from the bucket. A precondition of this method * is that j >= 0 && j <= the number of measurements within the underlying bucket. */ @@ -246,7 +290,6 @@ public: */ BucketUnpacker copy() const { BucketUnpacker unpackerCopy; - unpackerCopy._unpackerBehavior = _unpackerBehavior; unpackerCopy._spec = _spec; unpackerCopy._includeMetaField = _includeMetaField; unpackerCopy._includeTimeField = _includeTimeField; @@ -256,10 +299,10 @@ public: /** * This resets the unpacker to prepare to unpack a new bucket described by the given document. */ - void reset(BSONObj&& bucket); + void reset(BSONObj&& bucket, bool bucketMatchedQuery = false); - Behavior behavior() const { - return _unpackerBehavior; + BucketSpec::Behavior behavior() const { + return _spec.behavior(); } const BucketSpec& bucketSpec() const { @@ -270,6 +313,10 @@ public: return _bucket; } + bool bucketMatchedQuery() const { + return _bucketMatchedQuery; + } + bool includeMetaField() const { return _includeMetaField; } @@ -306,7 +353,7 @@ public: return std::string{timeseries::kControlMaxFieldNamePrefix} + field; } - void setBucketSpecAndBehavior(BucketSpec&& bucketSpec, Behavior behavior); + void setBucketSpec(BucketSpec&& bucketSpec); void setIncludeMinTimeAsMetadata(); void setIncludeMaxTimeAsMetadata(); @@ -331,12 +378,14 @@ private: void eraseExcludedComputedMetaProjFields(); BucketSpec _spec; - Behavior _unpackerBehavior; std::unique_ptr<UnpackingImpl> _unpackingImpl; bool _hasNext = false; + // A flag used to mark that the entire bucket matches the following $match predicate. + bool _bucketMatchedQuery = false; + // A flag used to mark that the timestamp value should be materialized in measurements. bool _includeTimeField{false}; @@ -357,6 +406,8 @@ private: // measurement. Value _metaValue; + BSONElement _metaBSONElem; + // Since the bucket min time is the same across all materialized measurements, we can cache the // value in the reset phase and use it to materialize as a metadata field in each measurement // if required by the pipeline. @@ -383,9 +434,9 @@ private: * Determines if an arbitrary field should be included in the materialized measurements. */ inline bool determineIncludeField(StringData fieldName, - BucketUnpacker::Behavior unpackerBehavior, + BucketSpec::Behavior unpackerBehavior, const std::set<std::string>& unpackFieldsToIncludeExclude) { - const bool isInclude = unpackerBehavior == BucketUnpacker::Behavior::kInclude; + const bool isInclude = unpackerBehavior == BucketSpec::Behavior::kInclude; const bool unpackFieldsContains = unpackFieldsToIncludeExclude.find(fieldName.toString()) != unpackFieldsToIncludeExclude.cend(); return isInclude == unpackFieldsContains; diff --git a/src/mongo/db/exec/bucket_unpacker_test.cpp b/src/mongo/db/exec/bucket_unpacker_test.cpp index 8ee0f4e05f5..9eecea624c5 100644 --- a/src/mongo/db/exec/bucket_unpacker_test.cpp +++ b/src/mongo/db/exec/bucket_unpacker_test.cpp @@ -57,12 +57,12 @@ public: * before actually doing any unpacking. */ BucketUnpacker makeBucketUnpacker(std::set<std::string> fields, - BucketUnpacker::Behavior behavior, + BucketSpec::Behavior behavior, BSONObj bucket, boost::optional<std::string> metaFieldName = boost::none) { - auto spec = BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields)}; - - BucketUnpacker unpacker{std::move(spec), behavior}; + auto spec = + BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields), behavior}; + BucketUnpacker unpacker{std::move(spec)}; unpacker.reset(std::move(bucket)); return unpacker; } @@ -72,12 +72,13 @@ public: * the given 'bucket'. Asserts that 'reset()' throws the given 'errorCode'. */ void assertUnpackerThrowsCode(std::set<std::string> fields, - BucketUnpacker::Behavior behavior, + BucketSpec::Behavior behavior, BSONObj bucket, boost::optional<std::string> metaFieldName, int errorCode) { - auto spec = BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields)}; - BucketUnpacker unpacker{std::move(spec), behavior}; + auto spec = + BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields), behavior}; + BucketUnpacker unpacker{std::move(spec)}; ASSERT_THROWS_CODE(unpacker.reset(std::move(bucket)), AssertionException, errorCode); } @@ -181,7 +182,7 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeAllMeasurementFields) { "a:{'0':1, '1':2}, b:{'1':1}}}"); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); @@ -205,7 +206,7 @@ TEST_F(BucketUnpackerTest, ExcludeASingleField) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); @@ -233,7 +234,7 @@ TEST_F(BucketUnpackerTest, EmptyIncludeGetsEmptyMeasurements) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); @@ -259,7 +260,7 @@ TEST_F(BucketUnpackerTest, EmptyExcludeMaterializesAllFields) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -287,7 +288,7 @@ TEST_F(BucketUnpackerTest, SparseColumnsWhereOneColumnIsExhaustedBeforeTheOther) auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -314,7 +315,7 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeWithDollarPrefix) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -341,7 +342,7 @@ TEST_F(BucketUnpackerTest, BucketsWithMetadataOnly) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -367,7 +368,7 @@ TEST_F(BucketUnpackerTest, UnorderedRowKeysDoesntAffectMaterialization) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -397,7 +398,7 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadata) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -424,7 +425,7 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadataUnorderedKey auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -452,7 +453,7 @@ TEST_F(BucketUnpackerTest, ExcludedMetaFieldDoesntMaterializeMetadataWhenBucketH auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -479,7 +480,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnUndefinedMeta) { auto test = [&](BSONObj bucket) { assertUnpackerThrowsCode(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString(), 5369600); @@ -499,7 +500,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnUnexpectedMeta) { auto test = [&](BSONObj bucket) { assertUnpackerThrowsCode(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), boost::none /* no metaField provided */, 5369601); @@ -518,7 +519,7 @@ TEST_F(BucketUnpackerTest, NullMetaInBucketMaterializesAsNull) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -550,7 +551,7 @@ TEST_F(BucketUnpackerTest, GetNextHandlesMissingMetaInBucket) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -579,7 +580,7 @@ TEST_F(BucketUnpackerTest, EmptyDataRegionInBucketIsTolerated) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker( - fields, BucketUnpacker::Behavior::kExclude, bucket, kUserDefinedMetaName.toString()); + fields, BucketSpec::Behavior::kExclude, bucket, kUserDefinedMetaName.toString()); ASSERT_FALSE(unpacker.hasNext()); }; @@ -591,7 +592,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnEmptyBucket) { auto bucket = Document{}; assertUnpackerThrowsCode(std::move(fields), - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, bucket.toBson(), kUserDefinedMetaName.toString(), 5346510); @@ -609,48 +610,52 @@ TEST_F(BucketUnpackerTest, EraseMetaFromFieldSetAndDetermineIncludeMeta) { } })"); auto unpacker = makeBucketUnpacker(empFields, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); // Tests a spec with 'metaField' in include list. std::set<std::string> fields{kUserDefinedMetaName.toString()}; - auto specWithMetaInclude = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; + auto specWithMetaInclude = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(fields), + BucketSpec::Behavior::kInclude}; // This calls eraseMetaFromFieldSetAndDetermineIncludeMeta. - unpacker.setBucketSpecAndBehavior(std::move(specWithMetaInclude), - BucketUnpacker::Behavior::kInclude); + unpacker.setBucketSpec(std::move(specWithMetaInclude)); ASSERT_TRUE(unpacker.includeMetaField()); ASSERT_EQ(unpacker.bucketSpec().fieldSet().count(kUserDefinedMetaName.toString()), 0); std::set<std::string> fieldsNoMetaInclude{"foo"}; auto specWithFooInclude = BucketSpec{kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), - std::move(fieldsNoMetaInclude)}; + std::move(fieldsNoMetaInclude), + BucketSpec::Behavior::kInclude}; std::set<std::string> fieldsNoMetaExclude{"foo"}; auto specWithFooExclude = BucketSpec{kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), - std::move(fieldsNoMetaExclude)}; + std::move(fieldsNoMetaExclude), + BucketSpec::Behavior::kExclude}; - unpacker.setBucketSpecAndBehavior(std::move(specWithFooExclude), - BucketUnpacker::Behavior::kExclude); + unpacker.setBucketSpec(std::move(specWithFooExclude)); ASSERT_TRUE(unpacker.includeMetaField()); - unpacker.setBucketSpecAndBehavior(std::move(specWithFooInclude), - BucketUnpacker::Behavior::kInclude); + unpacker.setBucketSpec(std::move(specWithFooInclude)); ASSERT_FALSE(unpacker.includeMetaField()); // Tests a spec with 'metaField' not in exclude list. std::set<std::string> excludeFields{}; - auto specMetaExclude = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(excludeFields)}; + auto specMetaExclude = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(excludeFields), + BucketSpec::Behavior::kExclude}; + auto specMetaInclude = specMetaExclude; - unpacker.setBucketSpecAndBehavior(std::move(specMetaExclude), - BucketUnpacker::Behavior::kExclude); + specMetaInclude.setBehavior(BucketSpec::Behavior::kInclude); + + unpacker.setBucketSpec(std::move(specMetaExclude)); ASSERT_TRUE(unpacker.includeMetaField()); - unpacker.setBucketSpecAndBehavior(std::move(specMetaInclude), - BucketUnpacker::Behavior::kInclude); + unpacker.setBucketSpec(std::move(specMetaInclude)); ASSERT_FALSE(unpacker.includeMetaField()); } @@ -665,21 +670,25 @@ TEST_F(BucketUnpackerTest, DetermineIncludeTimeField) { })"); std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()}; auto unpacker = makeBucketUnpacker(unpackerFields, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); std::set<std::string> includeFields{kUserDefinedTimeName.toString()}; - auto includeSpec = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(includeFields)}; + auto includeSpec = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(includeFields), + BucketSpec::Behavior::kInclude}; // This calls determineIncludeTimeField. - unpacker.setBucketSpecAndBehavior(std::move(includeSpec), BucketUnpacker::Behavior::kInclude); + unpacker.setBucketSpec(std::move(includeSpec)); ASSERT_TRUE(unpacker.includeTimeField()); std::set<std::string> excludeFields{kUserDefinedTimeName.toString()}; - auto excludeSpec = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(excludeFields)}; - unpacker.setBucketSpecAndBehavior(std::move(excludeSpec), BucketUnpacker::Behavior::kExclude); + auto excludeSpec = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(excludeFields), + BucketSpec::Behavior::kExclude}; + unpacker.setBucketSpec(std::move(excludeSpec)); ASSERT_FALSE(unpacker.includeTimeField()); } @@ -694,24 +703,26 @@ TEST_F(BucketUnpackerTest, DetermineIncludeFieldIncludeMode) { {"data", Document{}}} .toBson(); - auto spec = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; + auto spec = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(fields), + BucketSpec::Behavior::kInclude}; BucketUnpacker includeUnpacker; - includeUnpacker.setBucketSpecAndBehavior(std::move(spec), BucketUnpacker::Behavior::kInclude); + includeUnpacker.setBucketSpec(std::move(spec)); // Need to call reset so that the private method calculateFieldsToIncludeExcludeDuringUnpack() // is called, and _unpackFieldsToIncludeExclude gets filled with fields. includeUnpacker.reset(std::move(bucket)); // Now the spec knows which fields to include/exclude. ASSERT_TRUE(determineIncludeField(kUserDefinedTimeName, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, includeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_TRUE(determineIncludeField(includedMeasurementField, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, includeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_FALSE(determineIncludeField(excludedMeasurementField, - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, includeUnpacker.fieldsToIncludeExcludeDuringUnpack())); } @@ -726,21 +737,23 @@ TEST_F(BucketUnpackerTest, DetermineIncludeFieldExcludeMode) { {"data", Document{}}} .toBson(); - auto spec = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; + auto spec = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(fields), + BucketSpec::Behavior::kExclude}; BucketUnpacker excludeUnpacker; - excludeUnpacker.setBucketSpecAndBehavior(std::move(spec), BucketUnpacker::Behavior::kExclude); + excludeUnpacker.setBucketSpec(std::move(spec)); excludeUnpacker.reset(std::move(bucket)); ASSERT_FALSE(determineIncludeField(kUserDefinedTimeName, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, excludeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_FALSE(determineIncludeField(includedMeasurementField, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, excludeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_TRUE(determineIncludeField(excludedMeasurementField, - BucketUnpacker::Behavior::kExclude, + BucketSpec::Behavior::kExclude, excludeUnpacker.fieldsToIncludeExcludeDuringUnpack())); } @@ -758,9 +771,11 @@ auto expectedTimestampObjSize(int32_t rowKeyOffset, int32_t n) { TEST_F(BucketUnpackerTest, ExtractSingleMeasurement) { std::set<std::string> fields{ "_id", kUserDefinedMetaName.toString(), kUserDefinedTimeName.toString(), "a", "b"}; - auto spec = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; - auto unpacker = BucketUnpacker{std::move(spec), BucketUnpacker::Behavior::kInclude}; + auto spec = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(fields), + BucketSpec::Behavior::kInclude}; + auto unpacker = BucketUnpacker{std::move(spec)}; auto d1 = dateFromISOString("2020-02-17T00:00:00.000Z").getValue(); auto d2 = dateFromISOString("2020-02-17T01:00:00.000Z").getValue(); @@ -803,9 +818,11 @@ TEST_F(BucketUnpackerTest, ExtractSingleMeasurement) { TEST_F(BucketUnpackerTest, ExtractSingleMeasurementSparse) { std::set<std::string> fields{ "_id", kUserDefinedMetaName.toString(), kUserDefinedTimeName.toString(), "a", "b"}; - auto spec = BucketSpec{ - kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; - auto unpacker = BucketUnpacker{std::move(spec), BucketUnpacker::Behavior::kInclude}; + auto spec = BucketSpec{kUserDefinedTimeName.toString(), + kUserDefinedMetaName.toString(), + std::move(fields), + BucketSpec::Behavior::kInclude}; + auto unpacker = BucketUnpacker{std::move(spec)}; auto d1 = dateFromISOString("2020-02-17T00:00:00.000Z").getValue(); auto d2 = dateFromISOString("2020-02-17T01:00:00.000Z").getValue(); @@ -892,7 +909,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountLess) { auto modifiedCompressedBucket = modifyCompressedBucketElementCount(*compressedBucket, -1); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -927,7 +944,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMore) { auto modifiedCompressedBucket = modifyCompressedBucketElementCount(*compressedBucket, 1); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -962,7 +979,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMissing) { auto modifiedCompressedBucket = modifyCompressedBucketElementCount(*compressedBucket, 0); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -999,7 +1016,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchDataField) { modifyCompressedBucketRemoveLastInField(*compressedBucket, "a"_sd); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -1034,7 +1051,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchTimeField) { modifyCompressedBucketRemoveLastInField(*compressedBucket, "time"_sd); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketUnpacker::Behavior::kInclude, + BucketSpec::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); diff --git a/src/mongo/db/exec/collection_scan.cpp b/src/mongo/db/exec/collection_scan.cpp index 506ce18d1ce..f8550dbe81d 100644 --- a/src/mongo/db/exec/collection_scan.cpp +++ b/src/mongo/db/exec/collection_scan.cpp @@ -185,9 +185,26 @@ PlanStage::StageState CollectionScan::doWork(WorkingSetID* out) { << "attempting to resume no longer exists in the collection. " << "recordId: " << recordIdToSeek); } - } - return PlanStage::NEED_TIME; + if (_params.resumeAfterRecordId && !_params.resumeAfterRecordId->isNull()) { + invariant(!_params.tailable); + invariant(_lastSeenId.isNull()); + // Seek to where we are trying to resume the scan from. Signal a KeyNotFound + // error if the record no longer exists. + // + // Note that we want to return the record *after* this one since we have already + // returned this one prior to the resume. + auto& recordIdToSeek = *_params.resumeAfterRecordId; + if (!_cursor->seekExact(recordIdToSeek)) { + uasserted(ErrorCodes::KeyNotFound, + str::stream() + << "Failed to resume collection scan: the recordId from " + "which we are " + << "attempting to resume no longer exists in the collection. " + << "recordId: " << recordIdToSeek); + } + } + } } if (_lastSeenId.isNull() && _params.direction == CollectionScanParams::FORWARD && @@ -354,16 +371,24 @@ PlanStage::StageState CollectionScan::returnIfMatches(WorkingSetMember* member, // In the future, we could change seekNear() to always return a record after minRecord in the // direction of the scan. However, tailable scans depend on the current behavior in order to // mark their position for resuming the tailable scan later on. - if (!beforeStartOfRange(_params, *member) && Filter::passes(member, _filter)) { - if (_params.stopApplyingFilterAfterFirstMatch) { - _filter = nullptr; - } - *out = memberID; - return PlanStage::ADVANCED; - } else { + if (beforeStartOfRange(_params, *member)) { + _workingSet->free(memberID); + return PlanStage::NEED_TIME; + } + + if (!Filter::passes(member, _filter)) { _workingSet->free(memberID); + if (_params.shouldReturnEofOnFilterMismatch) { + _commonStats.isEOF = true; + return PlanStage::IS_EOF; + } return PlanStage::NEED_TIME; } + if (_params.stopApplyingFilterAfterFirstMatch) { + _filter = nullptr; + } + *out = memberID; + return PlanStage::ADVANCED; } bool CollectionScan::isEOF() { diff --git a/src/mongo/db/exec/collection_scan_common.h b/src/mongo/db/exec/collection_scan_common.h index ba5559a4491..5770943229c 100644 --- a/src/mongo/db/exec/collection_scan_common.h +++ b/src/mongo/db/exec/collection_scan_common.h @@ -111,6 +111,10 @@ struct CollectionScanParams { // Whether or not to wait for oplog visibility on oplog collection scans. bool shouldWaitForOplogVisibility = false; + + // Whether or not to return EOF and stop further scanning once MatchExpression evaluates to + // false. Can only be set to true if the MatchExpression is present. + bool shouldReturnEofOnFilterMismatch = false; }; } // namespace mongo diff --git a/src/mongo/db/exec/delete_stage.cpp b/src/mongo/db/exec/delete_stage.cpp index 83941b91c4e..581ef2ef294 100644 --- a/src/mongo/db/exec/delete_stage.cpp +++ b/src/mongo/db/exec/delete_stage.cpp @@ -178,23 +178,38 @@ PlanStage::StageState DeleteStage::doWork(WorkingSetID* out) { bool writeToOrphan = false; if (!_params->isExplain && !_params->fromMigrate) { - const auto action = _preWriteFilter.computeAction(member->doc.value()); - if (action == write_stage_common::PreWriteFilter::Action::kSkip) { - LOGV2_DEBUG(5983201, - 3, - "Skipping delete operation to orphan document to prevent a wrong change " - "stream event", - "namespace"_attr = collection()->ns(), - "record"_attr = member->doc.value()); - return PlanStage::NEED_TIME; - } else if (action == write_stage_common::PreWriteFilter::Action::kWriteAsFromMigrate) { - LOGV2_DEBUG(6184700, - 3, - "Marking delete operation to orphan document with the fromMigrate flag " - "to prevent a wrong change stream event", - "namespace"_attr = collection()->ns(), - "record"_attr = member->doc.value()); - writeToOrphan = true; + try { + const auto action = _preWriteFilter.computeAction(member->doc.value()); + if (action == write_stage_common::PreWriteFilter::Action::kSkip) { + LOGV2_DEBUG( + 5983201, + 3, + "Skipping delete operation to orphan document to prevent a wrong change " + "stream event", + "namespace"_attr = collection()->ns(), + "record"_attr = member->doc.value()); + return PlanStage::NEED_TIME; + } else if (action == write_stage_common::PreWriteFilter::Action::kWriteAsFromMigrate) { + LOGV2_DEBUG(6184700, + 3, + "Marking delete operation to orphan document with the fromMigrate flag " + "to prevent a wrong change stream event", + "namespace"_attr = collection()->ns(), + "record"_attr = member->doc.value()); + writeToOrphan = true; + } + } catch (const ExceptionFor<ErrorCodes::StaleConfig>& ex) { + if (ex->getVersionReceived() == ChunkVersion::IGNORED() && + ex->getCriticalSectionSignal()) { + // If ChunkVersion is IGNORED and we encountered a critical section, then yield, + // wait for the critical section to finish and then we'll resume the write from the + // point we had left. We do this to prevent large multi-writes from repeatedly + // failing due to StaleConfig and exhausting the mongos retry attempts. + planExecutorShardingCriticalSectionFuture(opCtx()) = ex->getCriticalSectionSignal(); + memberFreer.dismiss(); // Keep this member around so we can retry deleting it. + return prepareToRetryWSM(id, out); + } + throw; } } @@ -235,6 +250,18 @@ PlanStage::StageState DeleteStage::doWork(WorkingSetID* out) { } catch (const WriteConflictException&) { memberFreer.dismiss(); // Keep this member around so we can retry deleting it. return prepareToRetryWSM(id, out); + } catch (const ExceptionFor<ErrorCodes::StaleConfig>& ex) { + if (ex->getVersionReceived() == ChunkVersion::IGNORED() && + ex->getCriticalSectionSignal()) { + // If ChunkVersion is IGNORED and we encountered a critical section, then yield, + // wait for the critical section to finish and then we'll resume the write from the + // point we had left. We do this to prevent large multi-writes from repeatedly + // failing due to StaleConfig and exhausting the mongos retry attempts. + planExecutorShardingCriticalSectionFuture(opCtx()) = ex->getCriticalSectionSignal(); + memberFreer.dismiss(); // Keep this member around so we can retry deleting it. + return prepareToRetryWSM(id, out); + } + throw; } } _specificStats.docsDeleted += _params->numStatsForDoc ? _params->numStatsForDoc(bsonObjDoc) : 1; diff --git a/src/mongo/db/exec/document_value/document.cpp b/src/mongo/db/exec/document_value/document.cpp index dd833b3c28f..4488af81f3b 100644 --- a/src/mongo/db/exec/document_value/document.cpp +++ b/src/mongo/db/exec/document_value/document.cpp @@ -88,8 +88,10 @@ const StringDataSet Document::allMetadataFieldNames{Document::metaFieldTextScore Document::metaFieldGeoNearPoint, Document::metaFieldSearchScore, Document::metaFieldSearchHighlights, + Document::metaFieldSearchSortValues, Document::metaFieldIndexKey, - Document::metaFieldSearchScoreDetails}; + Document::metaFieldSearchScoreDetails, + Document::metaFieldVectorSearchScore}; DocumentStorageIterator::DocumentStorageIterator(DocumentStorage* storage, BSONObjIterator bsonIt) : _bsonIt(std::move(bsonIt)), @@ -130,7 +132,7 @@ bool DocumentStorageIterator::shouldSkipDeleted() { // If we strip the metadata see if a field name matches the known list. All metadata fields // start with '$' so optimize for a quick bailout. - if (_storage->stripMetadata() && fieldName[0] == '$' && + if (_storage->bsonHasMetadata() && fieldName[0] == '$' && Document::allMetadataFieldNames.contains(fieldName)) { return true; } @@ -343,8 +345,8 @@ void DocumentStorage::reserveFields(size_t expectedFields) { } intrusive_ptr<DocumentStorage> DocumentStorage::clone() const { - auto out = - make_intrusive<DocumentStorage>(_bson, _stripMetadata, _modified, _numBytesFromBSONInCache); + auto out = make_intrusive<DocumentStorage>( + _bson, _bsonHasMetadata, _modified, _numBytesFromBSONInCache); if (_cache) { // Make a copy of the buffer with the fields. @@ -373,6 +375,7 @@ intrusive_ptr<DocumentStorage> DocumentStorage::clone() const { out->_haveLazyLoadedMetadata = _haveLazyLoadedMetadata; out->_metadataFields = _metadataFields; + out->_snapshottedSize = _snapshottedSize; return out; } @@ -389,11 +392,12 @@ DocumentStorage::~DocumentStorage() { } } -void DocumentStorage::reset(const BSONObj& bson, bool stripMetadata) { +void DocumentStorage::reset(const BSONObj& bson, bool bsonHasMetadata) { _bson = bson; _numBytesFromBSONInCache = 0; - _stripMetadata = stripMetadata; + _bsonHasMetadata = bsonHasMetadata; _modified = false; + _snapshottedSize = 0; // Clean cache. for (auto it = iteratorCacheOnly(); !it.atEnd(); it.advance()) { @@ -420,6 +424,8 @@ void DocumentStorage::loadLazyMetadata() const { return; } + bool oldModified = _metadataFields.isModified(); + BSONObjIterator it(_bson); while (it.more()) { BSONElement elem(it.next()); @@ -460,10 +466,15 @@ void DocumentStorage::loadLazyMetadata() const { _metadataFields.setIndexKey(elem.Obj()); } else if (fieldName == Document::metaFieldSearchScoreDetails) { _metadataFields.setSearchScoreDetails(elem.Obj()); + } else if (fieldName == Document::metaFieldSearchSortValues) { + _metadataFields.setSearchSortValues(elem.Obj()); + } else if (fieldName == Document::metaFieldVectorSearchScore) { + _metadataFields.setVectorSearchScore(elem.Double()); } } } + _metadataFields.setModified(oldModified); _haveLazyLoadedMetadata = true; } @@ -513,22 +524,11 @@ void Document::toBson(BSONObjBuilder* builder, size_t recursionLevel) const { } } -BSONObj Document::toBson() const { - if (!storage().isModified() && !storage().stripMetadata()) { - return storage().bsonObj(); - } - - BSONObjBuilder bb; - toBson(&bb); - return bb.obj(); -} - boost::optional<BSONObj> Document::toBsonIfTriviallyConvertible() const { - if (!storage().isModified() && !storage().stripMetadata()) { + if (isTriviallyConvertible()) { return storage().bsonObj(); - } else { - return boost::none; } + return boost::none; } constexpr StringData Document::metaFieldTextScore; @@ -539,35 +539,41 @@ constexpr StringData Document::metaFieldGeoNearPoint; constexpr StringData Document::metaFieldSearchScore; constexpr StringData Document::metaFieldSearchHighlights; constexpr StringData Document::metaFieldSearchScoreDetails; +constexpr StringData Document::metaFieldSearchSortValues; +constexpr StringData Document::metaFieldVectorSearchScore; -BSONObj Document::toBsonWithMetaData() const { - BSONObjBuilder bb; - toBson(&bb); +void Document::toBsonWithMetaData(BSONObjBuilder* builder) const { + toBson(builder); if (!metadata()) { - return bb.obj(); + return; } if (metadata().hasTextScore()) - bb.append(metaFieldTextScore, metadata().getTextScore()); + builder->append(metaFieldTextScore, metadata().getTextScore()); if (metadata().hasRandVal()) - bb.append(metaFieldRandVal, metadata().getRandVal()); + builder->append(metaFieldRandVal, metadata().getRandVal()); if (metadata().hasSortKey()) - bb.append(metaFieldSortKey, - DocumentMetadataFields::serializeSortKey(metadata().isSingleElementKey(), - metadata().getSortKey())); + builder->append(metaFieldSortKey, + DocumentMetadataFields::serializeSortKey(metadata().isSingleElementKey(), + metadata().getSortKey())); if (metadata().hasGeoNearDistance()) - bb.append(metaFieldGeoNearDistance, metadata().getGeoNearDistance()); + builder->append(metaFieldGeoNearDistance, metadata().getGeoNearDistance()); if (metadata().hasGeoNearPoint()) - metadata().getGeoNearPoint().addToBsonObj(&bb, metaFieldGeoNearPoint); + metadata().getGeoNearPoint().addToBsonObj(builder, metaFieldGeoNearPoint); if (metadata().hasSearchScore()) - bb.append(metaFieldSearchScore, metadata().getSearchScore()); + builder->append(metaFieldSearchScore, metadata().getSearchScore()); if (metadata().hasSearchHighlights()) - metadata().getSearchHighlights().addToBsonObj(&bb, metaFieldSearchHighlights); + metadata().getSearchHighlights().addToBsonObj(builder, metaFieldSearchHighlights); if (metadata().hasIndexKey()) - bb.append(metaFieldIndexKey, metadata().getIndexKey()); + builder->append(metaFieldIndexKey, metadata().getIndexKey()); if (metadata().hasSearchScoreDetails()) - bb.append(metaFieldSearchScoreDetails, metadata().getSearchScoreDetails()); - return bb.obj(); + builder->append(metaFieldSearchScoreDetails, metadata().getSearchScoreDetails()); + if (metadata().hasSearchSortValues()) { + builder->append(metaFieldSearchSortValues, metadata().getSearchSortValues()); + } + if (metadata().hasVectorSearchScore()) { + builder->append(metaFieldVectorSearchScore, metadata().getVectorSearchScore()); + } } Document Document::fromBsonWithMetaData(const BSONObj& bson) { @@ -704,31 +710,17 @@ const Value Document::getNestedField(const FieldPath& path, vector<Position>* po return getNestedFieldHelper(*this, path, positions, 0); } -size_t Document::getApproximateSizeWithoutBackingBSON() const { - size_t size = sizeof(Document); - if (!_storage) - return size; - - size += sizeof(DocumentStorage); - size += storage().allocatedBytes(); - - for (auto it = storage().iteratorCacheOnly(); !it.atEnd(); it.advance()) { - size += it->val.getApproximateSize(); - size -= sizeof(Value); // already accounted for above - } - - // The metadata also occupies space in the document storage that's pre-allocated. - size += storage().getMetadataApproximateSize(); - - return size; +size_t Document::getApproximateSize() const { + return sizeof(Document) + storage().snapshottedApproximateSize(); } -size_t Document::getApproximateSize() const { - return getApproximateSizeWithoutBackingBSON() + storage().bsonObjSize(); +size_t Document::getCurrentApproximateSize() const { + return sizeof(Document) + storage().currentApproximateSize(); } size_t Document::memUsageForSorter() const { - return getApproximateSizeWithoutBackingBSON() + storage().nonCachedBsonObjSize(); + return storage().currentApproximateSize() - storage().bsonObjSize() + + storage().nonCachedBsonObjSize(); } void Document::hash_combine(size_t& seed, diff --git a/src/mongo/db/exec/document_value/document.h b/src/mongo/db/exec/document_value/document.h index 8fcfb28dd8b..062778628a0 100644 --- a/src/mongo/db/exec/document_value/document.h +++ b/src/mongo/db/exec/document_value/document.h @@ -100,7 +100,9 @@ public: static constexpr StringData metaFieldSearchScore = "$searchScore"_sd; static constexpr StringData metaFieldSearchHighlights = "$searchHighlights"_sd; static constexpr StringData metaFieldSearchScoreDetails = "$searchScoreDetails"_sd; + static constexpr StringData metaFieldSearchSortValues = "$searchSortValues"_sd; static constexpr StringData metaFieldIndexKey = "$indexKey"_sd; + static constexpr StringData metaFieldVectorSearchScore = "$vectorSearchScore"_sd; static const StringDataSet allMetadataFieldNames; @@ -191,7 +193,8 @@ public: /** * Get the approximate size of the Document, plus its underlying storage and sub-values. Returns - * size in bytes. + * size in bytes. The return value of this function is snapshotted. All subsequent calls of this + * method will return the same value. * * Note: Some memory may be shared with other Documents or between fields within a single * Document so this can overestimate usage. @@ -202,6 +205,11 @@ public: size_t getApproximateSize() const; /** + * Same as 'getApproximateSize()', but this method re-computes the size on every call. + */ + size_t getCurrentApproximateSize() const; + + /** * Return the approximate amount of space used by metadata. */ size_t getMetadataApproximateSize() const { @@ -253,12 +261,38 @@ public: void hash_combine(size_t& seed, const StringData::ComparatorInterface* stringComparator) const; /** + * Returns true, if this document is trivially convertible to BSON, meaning the underlying + * storage is already in BSON format and there are no damages. + */ + bool isTriviallyConvertible() const { + return !storage().isModified() && !storage().bsonHasMetadata(); + } + + /** + * Returns true, if this document is trivially convertible to BSON with metadata, meaning the + * underlying storage is already in BSON format and there are no damages. + */ + bool isTriviallyConvertibleWithMetadata() const { + return !storage().isModified() && !storage().isMetadataModified(); + } + + /** * Serializes this document to the BSONObj under construction in 'builder'. Metadata is not * included. Throws a AssertionException if 'recursionLevel' exceeds the maximum allowable * depth. */ void toBson(BSONObjBuilder* builder, size_t recursionLevel = 1) const; - BSONObj toBson() const; + + template <typename BSONTraits = BSONObj::DefaultSizeTrait> + BSONObj toBson() const { + if (isTriviallyConvertible()) { + return storage().bsonObj(); + } + + BSONObjBuilder bb; + toBson(&bb); + return bb.obj<BSONTraits>(); + } /** * Serializes this document iff the conversion is "trivial," meaning that the underlying storage @@ -272,7 +306,18 @@ public: /** * Like the 'toBson()' method, but includes metadata as top-level fields. */ - BSONObj toBsonWithMetaData() const; + void toBsonWithMetaData(BSONObjBuilder* builder) const; + + template <typename BSONTraits = BSONObj::DefaultSizeTrait> + BSONObj toBsonWithMetaData() const { + if (isTriviallyConvertibleWithMetadata()) { + return storage().bsonObj(); + } + + BSONObjBuilder bb; + toBsonWithMetaData(&bb); + return bb.obj<BSONTraits>(); + } /** * Like Document(BSONObj) but treats top-level fields with special names as metadata. @@ -370,12 +415,6 @@ private: getNestedFieldNonCachingHelper(const FieldPath& dottedField, size_t level) const; boost::intrusive_ptr<const DocumentStorage> _storage; - - /** - * Returns the approximate size of this `Document` instance without considering the size of its - * backing BSON object. - */ - size_t getApproximateSizeWithoutBackingBSON() const; }; // @@ -511,13 +550,11 @@ public: } /** - * Replace the current base Document with bson. - * - * The paramater 'stripMetadata' controls whether we strip the metadata fields from the - * underlying bson when converting the document object back to bson. + * Replace the current base Document with the BSON object. Setting 'bsonHasMetadata' to true + * signals that the BSON object contains metadata fields. */ - void reset(const BSONObj& bson, bool stripMetadata) { - storage().reset(bson, stripMetadata); + void reset(const BSONObj& bson, bool bsonHasMetadata) { + storage().reset(bson, bsonHasMetadata); } /** Add the given field to the Document. @@ -654,6 +691,7 @@ public: * TODO: there are some optimizations that may make sense at freeze time. */ Document freeze() { + resetSnapshottedApproximateSize(); // This essentially moves _storage into a new Document by way of temp. Document ret; boost::intrusive_ptr<const DocumentStorage> temp(storagePtr(), /*inc_ref_count=*/false); @@ -672,8 +710,12 @@ public: * Note that unlike freeze(), this indicates intention to continue * modifying this document. The returned Document will not observe * future changes to this MutableDocument. + * + * Note that the computed snapshotted approximate size of the Document + * is not preserved across calls. */ Document peek() { + resetSnapshottedApproximateSize(); return Document(storagePtr()); } @@ -695,13 +737,13 @@ public: storage().makeOwned(); } - /** Create a new document storage with the BSON object. - * - * The optional paramater 'stripMetadata' controls whether we strip the metadata fields (the - * complete list is in Document::allMetadataFieldNames). + /** + * Creates a new document storage with the BSON object. Setting 'bsonHasMetadata' to true + * signals that the BSON object contains metadata fields (the complete list is in + * Document::allMetadataFieldNames). */ - DocumentStorage& newStorageWithBson(const BSONObj& bson, bool stripMetadata) { - reset(make_intrusive<DocumentStorage>(bson, stripMetadata, false, 0)); + DocumentStorage& newStorageWithBson(const BSONObj& bson, bool bsonHasMetadata) { + reset(make_intrusive<DocumentStorage>(bson, bsonHasMetadata, false, 0)); return const_cast<DocumentStorage&>(*storagePtr()); } @@ -739,12 +781,19 @@ private: MutableValue getNestedFieldHelper(const FieldPath& dottedField, size_t level); MutableValue getNestedFieldHelper(const std::vector<Position>& positions, size_t level); - // this should only be called by storage methods and peek/freeze + // this should only be called by storage methods and peek/freeze/resetsnapshottedApproximateSize const DocumentStorage* storagePtr() const { dassert(!_storage || typeid(*_storage) == typeid(const DocumentStorage)); return static_cast<const DocumentStorage*>(_storage); } + void resetSnapshottedApproximateSize() { + auto mutableStorage = const_cast<DocumentStorage*>(storagePtr()); + if (mutableStorage) { + mutableStorage->resetSnapshottedApproximateSize(); + } + } + // These are both const to prevent modifications bypassing storage() method. // They always point to NULL or an object with dynamic type DocumentStorage. const RefCountable* _storageHolder; // Only used in constructors and destructor diff --git a/src/mongo/db/exec/document_value/document_internal.h b/src/mongo/db/exec/document_value/document_internal.h index 1387ca908b4..f21f3d40cc9 100644 --- a/src/mongo/db/exec/document_value/document_internal.h +++ b/src/mongo/db/exec/document_value/document_internal.h @@ -359,11 +359,11 @@ public: /** * Construct a storage from the BSON. The BSON is lazily processed as fields are requested from - * the document. If we know that the BSON does not contain any metadata fields we can set the - * 'stripMetadata' flag to false that will speed up the field iteration. + * the document. If we know that the BSON contains metadata fields we can set the + * 'bsonHasMetadata' flag to true. */ DocumentStorage(const BSONObj& bson, - bool stripMetadata, + bool bsonHasMetadata, bool modified, uint32_t numBytesFromBSONInCache) : _cache(nullptr), @@ -373,12 +373,12 @@ public: _hashTabMask(0), _bson(bson), _numBytesFromBSONInCache(numBytesFromBSONInCache), - _stripMetadata(stripMetadata), + _bsonHasMetadata(bsonHasMetadata), _modified(modified) {} ~DocumentStorage(); - void reset(const BSONObj& bson, bool stripMetadata); + void reset(const BSONObj& bson, bool bsonHasMetadata); /** * Populates the cache by recursively walking the underlying BSON. @@ -551,7 +551,7 @@ public: * WorkingSetMember. */ const DocumentMetadataFields& metadata() const { - if (_stripMetadata) { + if (_bsonHasMetadata) { loadLazyMetadata(); } return _metadataFields; @@ -589,8 +589,8 @@ public: return _firstElement ? _firstElement->plusBytes(_usedBytes) : nullptr; } - auto stripMetadata() const { - return _stripMetadata; + auto bsonHasMetadata() const { + return _bsonHasMetadata; } Position constructInCache(const BSONElement& elem); @@ -599,10 +599,37 @@ public: return _modified; } + auto isMetadataModified() const { + return _metadataFields.isModified(); + } + auto bsonObj() const { return _bson; } + size_t currentApproximateSize() const { + size_t size = sizeof(DocumentStorage) + allocatedBytes() + getMetadataApproximateSize() + + bsonObjSize(); + + for (auto it = iteratorCacheOnly(); !it.atEnd(); it.advance()) { + size += it->val.getApproximateSize() - sizeof(Value); + } + + return size; + } + + size_t snapshottedApproximateSize() const { + if (_snapshottedSize == 0) { + const_cast<DocumentStorage*>(this)->_snapshottedSize = currentApproximateSize(); + } + + return _snapshottedSize; + } + + void resetSnapshottedApproximateSize() { + _snapshottedSize = 0; + } + private: /// Returns the position of the named field in the cache or Position() template <typename T> @@ -685,22 +712,23 @@ private: // whole backing BSON, but only the portion of backing BSON that's not already in the cache. uint32_t _numBytesFromBSONInCache = 0; - // If '_stripMetadata' is true, tracks whether or not the metadata has been lazy-loaded from the - // backing '_bson' object. If so, then no attempt will be made to load the metadata again, even - // if the metadata has been released by a call to 'releaseMetadata()'. + // Tracks whether or not the metadata has been lazy-loaded from the backing '_bson' object. If + // so, then no attempt will be made to load the metadata again, even if the metadata has been + // released by a call to 'releaseMetadata()'. mutable bool _haveLazyLoadedMetadata = false; mutable DocumentMetadataFields _metadataFields; - // The storage constructed from a BSON value may contain metadata. When we process the BSON we - // have to move the metadata to the MetadataFields object. If we know that the BSON does not - // have any metadata we can set _stripMetadata to false that will speed up the iteration. - bool _stripMetadata{false}; + // True if this storage was constructed from BSON with metadata. Serializing this object using + // the 'toBson()' method will omit (strip) the metadata fields. + bool _bsonHasMetadata{false}; // This flag is set to true anytime the storage returns a mutable field. It is used to optimize // a conversion to BSON; i.e. if there are not any modifications we can directly return _bson. bool _modified{false}; + size_t _snapshottedSize{0}; + // Defined in document.cpp static const DocumentStorage kEmptyDoc; diff --git a/src/mongo/db/exec/document_value/document_metadata_fields.cpp b/src/mongo/db/exec/document_value/document_metadata_fields.cpp index 90e15ef5c2a..0532ed2e8cb 100644 --- a/src/mongo/db/exec/document_value/document_metadata_fields.cpp +++ b/src/mongo/db/exec/document_value/document_metadata_fields.cpp @@ -46,6 +46,7 @@ DocumentMetadataFields::DocumentMetadataFields(const DocumentMetadataFields& oth DocumentMetadataFields& DocumentMetadataFields::operator=(const DocumentMetadataFields& other) { _holder = other._holder ? std::make_unique<MetadataHolder>(*other._holder) : nullptr; + _modified = true; return *this; } @@ -54,6 +55,7 @@ DocumentMetadataFields::DocumentMetadataFields(DocumentMetadataFields&& other) DocumentMetadataFields& DocumentMetadataFields::operator=(DocumentMetadataFields&& other) { _holder = std::move(other._holder); + _modified = true; return *this; } @@ -91,6 +93,12 @@ void DocumentMetadataFields::mergeWith(const DocumentMetadataFields& other) { if (!hasTimeseriesBucketMaxTime() && other.hasTimeseriesBucketMaxTime()) { setTimeseriesBucketMaxTime(other.getTimeseriesBucketMaxTime()); } + if (!hasSearchSortValues() && other.hasSearchSortValues()) { + setSearchSortValues(other.getSearchSortValues()); + } + if (!hasVectorSearchScore() && other.hasVectorSearchScore()) { + setVectorSearchScore(other.getVectorSearchScore()); + } } void DocumentMetadataFields::copyFrom(const DocumentMetadataFields& other) { @@ -127,6 +135,12 @@ void DocumentMetadataFields::copyFrom(const DocumentMetadataFields& other) { if (other.hasTimeseriesBucketMaxTime()) { setTimeseriesBucketMaxTime(other.getTimeseriesBucketMaxTime()); } + if (other.hasSearchSortValues()) { + setSearchSortValues(other.getSearchSortValues()); + } + if (other.hasVectorSearchScore()) { + setVectorSearchScore(other.getVectorSearchScore()); + } } size_t DocumentMetadataFields::getApproximateSize() const { @@ -148,7 +162,7 @@ size_t DocumentMetadataFields::getApproximateSize() const { size -= sizeof(_holder->searchHighlights); size += _holder->indexKey.objsize(); size += _holder->searchScoreDetails.objsize(); - + size += _holder->searchSortValues.objsize(); return size; } @@ -204,6 +218,14 @@ void DocumentMetadataFields::serializeForSorter(BufBuilder& buf) const { buf.appendNum(static_cast<char>(MetaType::kTimeseriesBucketMaxTime + 1)); buf.appendNum(getTimeseriesBucketMaxTime().toMillisSinceEpoch()); } + if (hasSearchSortValues()) { + buf.appendNum(static_cast<char>(MetaType::kSearchSortValues + 1)); + getSearchSortValues().appendSelfToBufBuilder(buf); + } + if (hasVectorSearchScore()) { + buf.appendNum(static_cast<char>(MetaType::kVectorSearchScore + 1)); + buf.appendNum(getVectorSearchScore()); + } buf.appendNum(static_cast<char>(0)); } @@ -236,9 +258,16 @@ void DocumentMetadataFields::deserializeForSorter(BufReader& buf, DocumentMetada out->setSearchScoreDetails( BSONObj::deserializeForSorter(buf, BSONObj::SorterDeserializeSettings())); } else if (marker == static_cast<char>(MetaType::kTimeseriesBucketMinTime) + 1) { - out->setTimeseriesBucketMinTime(Date_t::fromMillisSinceEpoch(buf.read<long long>())); + out->setTimeseriesBucketMinTime( + Date_t::fromMillisSinceEpoch(buf.read<LittleEndian<long long>>())); } else if (marker == static_cast<char>(MetaType::kTimeseriesBucketMaxTime) + 1) { - out->setTimeseriesBucketMaxTime(Date_t::fromMillisSinceEpoch(buf.read<long long>())); + out->setTimeseriesBucketMaxTime( + Date_t::fromMillisSinceEpoch(buf.read<LittleEndian<long long>>())); + } else if (marker == static_cast<char>(MetaType::kSearchSortValues) + 1) { + out->setSearchSortValues( + BSONObj::deserializeForSorter(buf, BSONObj::SorterDeserializeSettings())); + } else if (marker == static_cast<char>(MetaType::kVectorSearchScore) + 1) { + out->setVectorSearchScore(buf.read<LittleEndian<double>>()); } else { uasserted(28744, "Unrecognized marker, unable to deserialize buffer"); } @@ -298,6 +327,10 @@ const char* DocumentMetadataFields::typeNameToDebugString(DocumentMetadataFields return "timeseries bucket min time"; case DocumentMetadataFields::kTimeseriesBucketMaxTime: return "timeseries bucket max time"; + case DocumentMetadataFields::kSearchSortValues: + return "$search sort values"; + case DocumentMetadataFields::kVectorSearchScore: + return "$vectorSearch distance"; default: MONGO_UNREACHABLE; } diff --git a/src/mongo/db/exec/document_value/document_metadata_fields.h b/src/mongo/db/exec/document_value/document_metadata_fields.h index 12932c29686..e8451677ea7 100644 --- a/src/mongo/db/exec/document_value/document_metadata_fields.h +++ b/src/mongo/db/exec/document_value/document_metadata_fields.h @@ -67,6 +67,8 @@ public: kSearchScoreDetails, kTimeseriesBucketMinTime, kTimeseriesBucketMaxTime, + kSearchSortValues, + kVectorSearchScore, // New fields must be added before the kNumFields sentinel. kNumFields @@ -148,11 +150,7 @@ public: } void setTextScore(double score) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kTextScore); + _setCommon(MetaType::kTextScore); _holder->textScore = score; } @@ -166,11 +164,7 @@ public: } void setRandVal(double val) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kRandVal); + _setCommon(MetaType::kRandVal); _holder->randVal = val; } @@ -184,11 +178,7 @@ public: } void setSortKey(Value sortKey, bool isSingleElementKey) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kSortKey); + _setCommon(MetaType::kSortKey); _holder->isSingleElementKey = isSingleElementKey; _holder->sortKey = std::move(sortKey); } @@ -207,11 +197,7 @@ public: } void setGeoNearDistance(double dist) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kGeoNearDist); + _setCommon(MetaType::kGeoNearDist); _holder->geoNearDistance = dist; } @@ -225,11 +211,7 @@ public: } void setGeoNearPoint(Value point) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kGeoNearPoint); + _setCommon(MetaType::kGeoNearPoint); _holder->geoNearPoint = std::move(point); } @@ -243,11 +225,7 @@ public: } void setSearchScore(double score) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kSearchScore); + _setCommon(MetaType::kSearchScore); _holder->searchScore = score; } @@ -261,11 +239,7 @@ public: } void setSearchHighlights(Value highlights) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kSearchHighlights); + _setCommon(MetaType::kSearchHighlights); _holder->searchHighlights = highlights; } @@ -279,11 +253,7 @@ public: } void setIndexKey(BSONObj indexKey) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kIndexKey); + _setCommon(MetaType::kIndexKey); _holder->indexKey = indexKey.getOwned(); } @@ -297,11 +267,7 @@ public: } void setRecordId(RecordId rid) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - - _holder->metaFields.set(MetaType::kRecordId); + _setCommon(MetaType::kRecordId); _holder->recordId = rid; } @@ -315,10 +281,7 @@ public: } void setSearchScoreDetails(BSONObj details) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - _holder->metaFields.set(MetaType::kSearchScoreDetails); + _setCommon(MetaType::kSearchScoreDetails); _holder->searchScoreDetails = details.getOwned(); } @@ -327,15 +290,14 @@ public: } Date_t getTimeseriesBucketMinTime() const { - invariant(hasTimeseriesBucketMinTime()); + tassert(6850100, + "Document must have timeseries bucket min time metadata field set", + hasTimeseriesBucketMinTime()); return _holder->timeseriesBucketMinTime; } void setTimeseriesBucketMinTime(Date_t time) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - _holder->metaFields.set(MetaType::kTimeseriesBucketMinTime); + _setCommon(MetaType::kTimeseriesBucketMinTime); _holder->timeseriesBucketMinTime = time; } @@ -344,20 +306,69 @@ public: } Date_t getTimeseriesBucketMaxTime() const { - invariant(hasTimeseriesBucketMaxTime()); + tassert(6850101, + "Document must have timeseries bucket max time metadata field set", + hasTimeseriesBucketMaxTime()); return _holder->timeseriesBucketMaxTime; } void setTimeseriesBucketMaxTime(Date_t time) { - if (!_holder) { - _holder = std::make_unique<MetadataHolder>(); - } - _holder->metaFields.set(MetaType::kTimeseriesBucketMaxTime); + _setCommon(MetaType::kTimeseriesBucketMaxTime); _holder->timeseriesBucketMaxTime = time; } + + bool hasSearchSortValues() const { + return _holder && _holder->metaFields.test(MetaType::kSearchSortValues); + } + + BSONObj getSearchSortValues() const { + tassert(7320401, "Document must have $searchSortValues set", hasSearchSortValues()); + return _holder->searchSortValues; + } + + void setSearchSortValues(BSONObj vals) { + _setCommon(MetaType::kSearchSortValues); + _holder->searchSortValues = vals.getOwned(); + } + + bool hasVectorSearchScore() const { + return _holder && _holder->metaFields.test(MetaType::kVectorSearchScore); + } + + double getVectorSearchScore() const { + tassert(7828400, "vectorSearchScore must be present in metadata", hasVectorSearchScore()); + return _holder->vectorSearchScore; + } + + void setVectorSearchScore(double vectorSearchScore) { + _setCommon(MetaType::kVectorSearchScore); + _holder->vectorSearchScore = vectorSearchScore; + } + void serializeForSorter(BufBuilder& buf) const; + bool isModified() const { + return _modified; + } + + /** + * Sets the 'modified' flag to the given value. Necessary for implementing a lazy load + * optimization for the contained mutable 'DocumentMetadataFields' instance inside the + * 'Document' class. + */ + void setModified(bool newValue) { + _modified = newValue; + } + private: + inline void _setCommon(MetaType mt) { + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + _holder->metaFields.set(mt); + _modified = true; + } + // A simple data struct housing all possible metadata fields. struct MetadataHolder { std::bitset<MetaType::kNumFields> metaFields; @@ -379,10 +390,17 @@ private: BSONObj searchScoreDetails; Date_t timeseriesBucketMinTime; Date_t timeseriesBucketMaxTime; + BSONObj searchSortValues; + double vectorSearchScore{0.0}; }; // Null until the first setter is called, at which point a MetadataHolder struct is allocated. std::unique_ptr<MetadataHolder> _holder; + + // This flag is set to true anytime a 'DocumentMetadataFields' instance is modified. It is used + // to optimize document conversion to BSON with metadata; i.e. if there are no modifications we + // can directly return the underlying BSON. + bool _modified{false}; }; using QueryMetadataBitSet = std::bitset<DocumentMetadataFields::MetaType::kNumFields>; diff --git a/src/mongo/db/exec/document_value/document_metadata_fields_test.cpp b/src/mongo/db/exec/document_value/document_metadata_fields_test.cpp index 5515009b2bd..23ab2277c96 100644 --- a/src/mongo/db/exec/document_value/document_metadata_fields_test.cpp +++ b/src/mongo/db/exec/document_value/document_metadata_fields_test.cpp @@ -34,6 +34,7 @@ #include "mongo/db/exec/document_value/document_metadata_fields.h" #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/unittest/bson_test_util.h" +#include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" namespace mongo { @@ -50,6 +51,8 @@ TEST(DocumentMetadataFieldsTest, AllMetadataRoundtripsThroughSerialization) { metadata.setIndexKey(BSON("b" << 1)); metadata.setSearchScoreDetails(BSON("scoreDetails" << "foo")); + metadata.setSearchSortValues(BSON("a" << 1)); + metadata.setVectorSearchScore(7.6); BufBuilder builder; metadata.serializeForSorter(builder); @@ -69,6 +72,8 @@ TEST(DocumentMetadataFieldsTest, AllMetadataRoundtripsThroughSerialization) { ASSERT_BSONOBJ_EQ(deserialized.getSearchScoreDetails(), BSON("scoreDetails" << "foo")); + ASSERT_BSONOBJ_EQ(deserialized.getSearchSortValues(), BSON("a" << 1)); + ASSERT_EQ(deserialized.getVectorSearchScore(), 7.6); } TEST(DocumentMetadataFieldsTest, HasMethodsReturnFalseForEmptyMetadata) { @@ -83,6 +88,8 @@ TEST(DocumentMetadataFieldsTest, HasMethodsReturnFalseForEmptyMetadata) { ASSERT_FALSE(metadata.hasSearchHighlights()); ASSERT_FALSE(metadata.hasIndexKey()); ASSERT_FALSE(metadata.hasSearchScoreDetails()); + ASSERT_FALSE(metadata.hasSearchSortValues()); + ASSERT_FALSE(metadata.hasVectorSearchScore()); } TEST(DocumentMetadataFieldsTest, HasMethodsReturnTrueForInitializedMetadata) { @@ -125,6 +132,14 @@ TEST(DocumentMetadataFieldsTest, HasMethodsReturnTrueForInitializedMetadata) { metadata.setSearchScoreDetails(BSON("scoreDetails" << "foo")); ASSERT_TRUE(metadata.hasSearchScoreDetails()); + + ASSERT_FALSE(metadata.hasSearchSortValues()); + metadata.setSearchSortValues(BSON("a" << 1)); + ASSERT_TRUE(metadata.hasSearchSortValues()); + + ASSERT_FALSE(metadata.hasVectorSearchScore()); + metadata.setVectorSearchScore(7.6); + ASSERT_TRUE(metadata.hasVectorSearchScore()); } TEST(DocumentMetadataFieldsTest, MoveConstructor) { @@ -139,6 +154,8 @@ TEST(DocumentMetadataFieldsTest, MoveConstructor) { metadata.setIndexKey(BSON("b" << 1)); metadata.setSearchScoreDetails(BSON("scoreDetails" << "foo")); + metadata.setSearchSortValues(BSON("a" << 1)); + metadata.setVectorSearchScore(7.6); DocumentMetadataFields moveConstructed(std::move(metadata)); ASSERT_TRUE(moveConstructed); @@ -154,6 +171,8 @@ TEST(DocumentMetadataFieldsTest, MoveConstructor) { ASSERT_BSONOBJ_EQ(moveConstructed.getSearchScoreDetails(), BSON("scoreDetails" << "foo")); + ASSERT_BSONOBJ_EQ(moveConstructed.getSearchSortValues(), BSON("a" << 1)); + ASSERT_EQ(moveConstructed.getVectorSearchScore(), 7.6); ASSERT_FALSE(metadata); // NOLINT(bugprone-use-after-move) } @@ -170,6 +189,8 @@ TEST(DocumentMetadataFieldsTest, MoveAssignmentOperator) { metadata.setIndexKey(BSON("b" << 1)); metadata.setSearchScoreDetails(BSON("scoreDetails" << "foo")); + metadata.setSearchSortValues(BSON("a" << 1)); + metadata.setVectorSearchScore(7.6); DocumentMetadataFields moveAssigned; moveAssigned.setTextScore(12.3); @@ -188,6 +209,8 @@ TEST(DocumentMetadataFieldsTest, MoveAssignmentOperator) { ASSERT_BSONOBJ_EQ(moveAssigned.getSearchScoreDetails(), BSON("scoreDetails" << "foo")); + ASSERT_BSONOBJ_EQ(moveAssigned.getSearchSortValues(), BSON("a" << 1)); + ASSERT_EQ(moveAssigned.getVectorSearchScore(), 7.6); ASSERT_FALSE(metadata); // NOLINT(bugprone-use-after-move) } @@ -241,6 +264,8 @@ TEST(DocumentMetadataFieldsTest, MergeWithOnlyCopiesMetadataThatDestinationDoesN ASSERT_FALSE(destination.hasSearchHighlights()); ASSERT_FALSE(destination.hasIndexKey()); ASSERT_FALSE(destination.hasSearchScoreDetails()); + ASSERT_FALSE(destination.hasSearchSortValues()); + ASSERT_FALSE(destination.hasVectorSearchScore()); } TEST(DocumentMetadataFieldsTest, CopyFromCopiesAllMetadataThatSourceHas) { @@ -266,6 +291,173 @@ TEST(DocumentMetadataFieldsTest, CopyFromCopiesAllMetadataThatSourceHas) { ASSERT_FALSE(destination.hasSearchHighlights()); ASSERT_FALSE(destination.hasIndexKey()); ASSERT_FALSE(destination.hasSearchScoreDetails()); + ASSERT_FALSE(destination.hasSearchSortValues()); + ASSERT_FALSE(destination.hasVectorSearchScore()); +} + +TEST(DocumentMetadataFieldsTest, GetTimeseriesBucketMinTimeExists) { + DocumentMetadataFields source; + Date_t time; + source.setTimeseriesBucketMinTime(time); + ASSERT_EQ(source.getTimeseriesBucketMinTime(), time); +} + +TEST(DocumentMetadataFieldsTest, GetTimeseriesBucketMaxTimeExists) { + DocumentMetadataFields source; + Date_t time; + source.setTimeseriesBucketMaxTime(time); + ASSERT_EQ(source.getTimeseriesBucketMaxTime(), time); +} + +TEST(DocumentMetadataFieldsTest, MetadataIsMarkedModifiedOnSetMetadataField) { + // Test setting metadata fields directly. + + auto testFieldSetter = [](std::function<void(DocumentMetadataFields&)> invokeSetter) { + DocumentMetadataFields metadata; + ASSERT_FALSE(metadata.isModified()); + invokeSetter(metadata); + ASSERT_TRUE(metadata.isModified()); + }; + + testFieldSetter([](DocumentMetadataFields& md) { md.setTextScore(10.0); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setRandVal(20.0); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setSortKey(Value(30), true); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setGeoNearDistance(40.0); }); + testFieldSetter( + [](DocumentMetadataFields& md) { md.setGeoNearPoint(Value{BSON_ARRAY(1 << 2)}); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setSearchScore(50.0); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setSearchHighlights(Value{"foo"_sd}); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setIndexKey(BSON("b" << 1)); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setRecordId(RecordId{6}); }); + testFieldSetter([](DocumentMetadataFields& md) { + md.setSearchScoreDetails(BSON("scoreDetails" + << "foo")); + }); + testFieldSetter([](DocumentMetadataFields& md) { md.setTimeseriesBucketMinTime(Date_t()); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setTimeseriesBucketMaxTime(Date_t()); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setSearchSortValues(BSON("a" << 1)); }); + testFieldSetter([](DocumentMetadataFields& md) { md.setVectorSearchScore(60.0); }); +} + +TEST(DocumentMetadataFieldsTest, MetadataIsConstructedUnmodified) { + // We consider new instances as unmodified for all constructors (default, copy, move) even when + // the original metadata has the 'modified' flag set. + + // Testing the default constructor. + DocumentMetadataFields metadata1; + ASSERT_FALSE(metadata1.isModified()); + metadata1.setTextScore(10.0); + ASSERT_TRUE(metadata1.isModified()); + + // Testing the copy-constructor. + DocumentMetadataFields metadata2(metadata1); + ASSERT_FALSE(metadata2.isModified()); + + // Testing the move-constructor. + DocumentMetadataFields metadata3(std::move(metadata1)); + ASSERT_FALSE(metadata3.isModified()); +} + +TEST(DocumentMetadataFieldsTest, CopyAssignmentIsModification) { + // We consider copy-assignment as modification even when the instances are equal and the copied + // metadata does not have the 'modified' flag set. + + DocumentMetadataFields metadata1; + ASSERT_FALSE(metadata1.isModified()); + + DocumentMetadataFields metadata2; + ASSERT_FALSE(metadata2.isModified()); + + // Testing copy-assignment to empty object (metadata2 == metadata1). + metadata2 = metadata1; + ASSERT_TRUE(metadata2.isModified()); + + // Testing copy-assignment in a more common case (metadata1 is empty, metadata2 is not empty). + metadata2.setTextScore(10.0); + metadata2.setModified(false); + ASSERT_FALSE(metadata2.isModified()); + metadata1 = metadata2; + ASSERT_TRUE(metadata1.isModified()); + ASSERT_EQ(metadata2.getTextScore(), metadata1.getTextScore()); +} + +TEST(DocumentMetadataFieldsTest, MoveAssignmentIsModification) { + // We consider move-assignment as modification even when the instances are equal and the moved + // metadata does not have the 'modified' flag set. + + DocumentMetadataFields metadata1; + ASSERT_FALSE(metadata1.isModified()); + + DocumentMetadataFields metadata2; + ASSERT_FALSE(metadata2.isModified()); + + // Testing move-assignment to empty object (metadata2 == metadata1). + metadata2 = std::move(metadata1); + ASSERT_TRUE(metadata2.isModified()); + + // Testing move-assignment in a more common case (metadata3 is empty, metadata2 is not empty). + metadata2.setTextScore(10.0); + metadata2.setModified(false); + ASSERT_FALSE(metadata2.isModified()); + + DocumentMetadataFields metadata3; + ASSERT_FALSE(metadata3.isModified()); + + metadata3 = std::move(metadata2); + ASSERT_TRUE(metadata3.isModified()); + ASSERT_EQ(10.0, metadata3.getTextScore()); +} + +TEST(DocumentMetadataFieldsTest, MetadataIsMarkedModifiedOnCopyFrom) { + DocumentMetadataFields metadata1; + metadata1.setRandVal(20.0); + ASSERT_TRUE(metadata1.isModified()); + + // Testing 'setModified(false)'. + metadata1.setModified(false); + ASSERT_FALSE(metadata1.isModified()); + + // Calling 'copyFrom(metadata1)' modifies metadata2 even when metadata2 == metadata1 and + // metadata1 is not marked as modified. + DocumentMetadataFields metadata2(metadata1); + ASSERT_FALSE(metadata2.isModified()); + metadata2.copyFrom(metadata1); + ASSERT_TRUE(metadata2.isModified()); +} + +TEST(DocumentMetadataFieldsTest, MetadataIsMarkedModifiedOnMergeWith) { + DocumentMetadataFields metadata1; + metadata1.setRandVal(20.0); + ASSERT_TRUE(metadata1.isModified()); + + metadata1.setModified(false); + ASSERT_FALSE(metadata1.isModified()); + + // Calling 'mergeWith(metadata1)' modifies metadata2 only when metadata1 has fields not set in + // metadata1. + DocumentMetadataFields metadata2(metadata1); + ASSERT_FALSE(metadata2.isModified()); + metadata2.mergeWith(metadata1); + ASSERT_FALSE(metadata2.isModified()); + + DocumentMetadataFields metadata3; + ASSERT_FALSE(metadata3.isModified()); + metadata3.mergeWith(metadata2); + ASSERT_TRUE(metadata3.isModified()); +} + +DEATH_TEST_REGEX(DocumentMetadataFieldsTest, + GetTimeseriesBucketMinTimeDoesntExist, + "Tripwire assertion.*6850100") { + DocumentMetadataFields source; + source.getTimeseriesBucketMinTime(); +} + +DEATH_TEST_REGEX(DocumentMetadataFieldsTest, + GetTimeseriesBucketMaxTimeDoesntExist, + "Tripwire assertion.*6850101") { + DocumentMetadataFields source; + source.getTimeseriesBucketMaxTime(); } } // namespace mongo 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 421da707001..ecc550f6ee0 100644 --- a/src/mongo/db/exec/document_value/document_value_test.cpp +++ b/src/mongo/db/exec/document_value/document_value_test.cpp @@ -46,6 +46,16 @@ #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; @@ -355,6 +365,46 @@ TEST(DocumentGetFieldNonCaching, TraverseArray) { checkArrayTagIsReturned(); } +TEST(DocumentSize, ApproximateSizeIsSnapshotted) { + const auto rawBson = BSON("field" + << "value"); + const Document document{rawBson}; + const auto noCacheSize = document.getApproximateSize(); + + // Force the cache construction, making the total size of the 'Document' bigger. + // 'getApproximateSize()' must still return the same value. + document.fillCache(); + const auto fullCacheSizeSnapshot = document.getApproximateSize(); + const auto fullCacheSizeCurrent = document.getCurrentApproximateSize(); + ASSERT_EQ(noCacheSize, fullCacheSizeSnapshot); + ASSERT_LT(noCacheSize, fullCacheSizeCurrent); +} + +TEST(DocumentSize, ApproximateSizeDuringBuildIsUpdated) { + MutableDocument builder; + builder.addField("a1", Value(1)); + builder.addField("a2", mongo::Value(2)); + builder.addField("a3", mongo::Value(3)); + auto middleBuildSize = builder.getApproximateSize(); + + builder.addField("a4", Value(4)); + builder.addField("a5", mongo::Value(5)); + builder.addField("a6", mongo::Value(6)); + auto peekSize = builder.peek().getApproximateSize(); + + builder.addField("a7", Value(7)); + builder.addField("a8", mongo::Value(8)); + builder.addField("a9", mongo::Value(9)); + auto beforeFreezeSize = builder.getApproximateSize(); + + Document result = builder.freeze(); + auto frozenSize = result.getApproximateSize(); + + ASSERT_LT(middleBuildSize, peekSize); + ASSERT_LT(peekSize, beforeFreezeSize); + ASSERT_EQ(beforeFreezeSize, frozenSize); +} + /** Add Document fields. */ class AddField { public: @@ -705,6 +755,22 @@ public: BSONObjBuilder objBuilder; BSONArrayBuilder arrBuilder; }; + +TEST(DocumentTest, ToBsonSizeTraits) { + constexpr size_t longStringLength = 9 * 1024 * 1024; + static_assert(longStringLength <= BSONObjMaxInternalSize && + 2 * longStringLength > BSONObjMaxInternalSize && + 2 * longStringLength <= BufferMaxSize); + std::string longString(longStringLength, 'A'); + MutableDocument md; + md.addField("a", Value(longString)); + ASSERT_DOES_NOT_THROW(md.peek().toBson()); + md.addField("b", Value(longString)); + ASSERT_THROWS_CODE(md.peek().toBson(), DBException, ErrorCodes::BSONObjectTooLarge); + ASSERT_THROWS_CODE( + md.peek().toBson<BSONObj::DefaultSizeTrait>(), DBException, ErrorCodes::BSONObjectTooLarge); + ASSERT_DOES_NOT_THROW(md.peek().toBson<BSONObj::LargeSizeTrait>()); +} } // namespace Document namespace MetaFields { @@ -840,7 +906,8 @@ TEST(MetaFields, CopyMetadataFromCopiesAllMetadata) { << "foo" << "h" << 1 << "$indexKey" << BSON("y" << 1) << "$searchScoreDetails" << BSON("scoreDetails" - << "foo"))); + << "foo") + << "$searchSortValues" << BSON("a" << 1) << "$vectorSearchScore" << 6.7)); MutableDocument destination{}; destination.copyMetaDataFrom(source); @@ -857,6 +924,8 @@ TEST(MetaFields, CopyMetadataFromCopiesAllMetadata) { ASSERT_BSONOBJ_EQ(result.metadata().getSearchScoreDetails(), BSON("scoreDetails" << "foo")); + ASSERT_BSONOBJ_EQ(result.metadata().getSearchSortValues(), BSON("a" << 1)); + ASSERT_EQ(result.metadata().getVectorSearchScore(), 6.7); } class SerializationTest : public unittest::Test { @@ -877,6 +946,8 @@ protected: ASSERT_EQ(output.metadata().hasSearchScore(), input.metadata().hasSearchScore()); ASSERT_EQ(output.metadata().hasSearchHighlights(), input.metadata().hasSearchHighlights()); ASSERT_EQ(output.metadata().hasIndexKey(), input.metadata().hasIndexKey()); + ASSERT_EQ(output.metadata().hasVectorSearchScore(), + input.metadata().hasVectorSearchScore()); if (input.metadata().hasTextScore()) { ASSERT_EQ(output.metadata().getTextScore(), input.metadata().getTextScore()); } @@ -897,6 +968,10 @@ protected: ASSERT_BSONOBJ_EQ(output.metadata().getSearchScoreDetails(), input.metadata().getSearchScoreDetails()); } + if (input.metadata().hasVectorSearchScore()) { + ASSERT_EQ(output.metadata().getVectorSearchScore(), + input.metadata().getVectorSearchScore()); + } ASSERT(output.toBson().binaryEqual(input.toBson())); } @@ -911,6 +986,7 @@ TEST_F(SerializationTest, MetaSerializationNoVals) { << "def"_sd)); docBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); + docBuilder.metadata().setVectorSearchScore(40.0); assertRoundTrips(docBuilder.freeze()); } @@ -925,6 +1001,7 @@ TEST_F(SerializationTest, MetaSerializationWithVals) { docBuilder.metadata().setIndexKey(BSON("key" << 42)); docBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); + docBuilder.metadata().setVectorSearchScore(40.0); assertRoundTrips(docBuilder.freeze()); } @@ -947,6 +1024,8 @@ TEST(MetaFields, ToAndFromBson) { << "def"_sd)); docBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); + docBuilder.metadata().setSearchSortValues(BSON("a" << 42)); + docBuilder.metadata().setVectorSearchScore(40.0); Document doc = docBuilder.freeze(); BSONObj obj = doc.toBsonWithMetaData(); ASSERT_EQ(10.0, obj[Document::metaFieldTextScore].Double()); @@ -958,6 +1037,8 @@ TEST(MetaFields, ToAndFromBson) { ASSERT_BSONOBJ_EQ(obj[Document::metaFieldSearchScoreDetails].Obj(), BSON("scoreDetails" << "foo")); + ASSERT_BSONOBJ_EQ(BSON("a" << 42), obj[Document::metaFieldSearchSortValues].Obj()); + ASSERT_EQ(40.0, obj[Document::metaFieldVectorSearchScore].Double()); Document fromBson = Document::fromBsonWithMetaData(obj); ASSERT_TRUE(fromBson.metadata().hasTextScore()); ASSERT_TRUE(fromBson.metadata().hasRandVal()); @@ -966,6 +1047,110 @@ TEST(MetaFields, ToAndFromBson) { ASSERT_BSONOBJ_EQ(BSON("scoreDetails" << "foo"), fromBson.metadata().getSearchScoreDetails()); + ASSERT_BSONOBJ_EQ(BSON("a" << 42), fromBson.metadata().getSearchSortValues()); + ASSERT_EQ(40.0, fromBson.metadata().getVectorSearchScore()); +} + +TEST(MetaFields, ToAndFromBsonTrivialConvertibility) { + Value sortKey{Document{{"token"_sd, "SOMENCODEDATA"_sd}}}; + // Create a document with a backing BSONObj and separate metadata. + auto origObjNoMetadata = BSON("a" << 42); + ASSERT_FALSE(origObjNoMetadata.hasField(Document::metaFieldSortKey)); + + MutableDocument docBuilder; + docBuilder.reset(origObjNoMetadata, false); + docBuilder.metadata().setSortKey(sortKey, true); + Document docWithSeparateBsonAndMetadata = docBuilder.freeze(); + + BSONObj origObjWithMetadata = docWithSeparateBsonAndMetadata.toBsonWithMetaData(); + ASSERT_TRUE(origObjWithMetadata.hasField(Document::metaFieldSortKey)); + Document restoredDocWithMetadata = Document::fromBsonWithMetaData(origObjWithMetadata); + ASSERT_DOCUMENT_EQ(docWithSeparateBsonAndMetadata, restoredDocWithMetadata); + + // Test the 'isTriviallyConvertible()' function. + // The original document is trivially convertible without metadata because the metadata was + // added to the document separately from the backing BSON object. + ASSERT_TRUE(docWithSeparateBsonAndMetadata.isTriviallyConvertible()); + // The original document is NOT trivially convertible with metadata because the metadata was + // added to the document and does not exist in the BSONObj. + ASSERT_FALSE(docWithSeparateBsonAndMetadata.isTriviallyConvertibleWithMetadata()); + // The restored document is trivially convertible with metadata because the underlying BSONObj + // contains the metadata serialized from the original document. + ASSERT_TRUE(restoredDocWithMetadata.isTriviallyConvertibleWithMetadata()); + // The restored document is NOT trivially convertible without metadata because the metadata + // fields need to be stripped from the underlying BSONObj. + ASSERT_FALSE(restoredDocWithMetadata.isTriviallyConvertible()); + + // Test that the conversion with metadata 'origObjWithMetadata' -> 'restoredDocWithMetadata' -> + // 'restoredObjWithMetadata' is trivial because the backing BSON already contains metadata and + // neither the metadata nor the non-metadata fields have been modified. + BSONObj restoredObjWithMetadata = restoredDocWithMetadata.toBsonWithMetaData(); + ASSERT_TRUE(restoredObjWithMetadata.hasField(Document::metaFieldSortKey)); + // Test that 'restoredObjWithMetadata' is referring to the exact same memory location as + // 'origObjWithMetadata', i.e. both objdata() and objsize() match. + ASSERT_EQ(origObjWithMetadata.objdata(), restoredObjWithMetadata.objdata()); + ASSERT_EQ(origObjWithMetadata.objsize(), restoredObjWithMetadata.objsize()); + + // Test that the conversion without metadata 'origObjWithMetadata' -> 'restoredDocWithMetadata' + // -> 'strippedRestoredObj' is NOT trivial because the backing BSON has metadata that must be + // omitted during serialization. + BSONObj strippedRestoredObj = restoredDocWithMetadata.toBson(); + ASSERT_FALSE(strippedRestoredObj.hasField(Document::metaFieldSortKey)); + // 'restoredDocWithMetadata' is trivially convertible with metadata and converting it to BSON + // without metadata will return a new BSON object. + ASSERT_TRUE(origObjNoMetadata.binaryEqual(strippedRestoredObj)); + ASSERT_NE(origObjNoMetadata.objdata(), strippedRestoredObj.objdata()); + + // Test that the conversion without metadata 'origObjNoMetadata' -> + // 'docWithSeparateBsonAndMetadata' -> 'restoredObjNoMetadata' is trivial because + // 'origObjNoMetadata' does not contain any metadata. + BSONObj restoredObjNoMetadata = docWithSeparateBsonAndMetadata.toBson(); + ASSERT_FALSE(restoredObjNoMetadata.hasField(Document::metaFieldSortKey)); + // Test that 'restoredObjNoMetadata' is referring to the exact same memory location as + // 'origObjNoMetadata', i.e. both objdata() and objsize() match. + ASSERT_EQ(origObjNoMetadata.objdata(), restoredObjNoMetadata.objdata()); + ASSERT_EQ(origObjNoMetadata.objsize(), restoredObjNoMetadata.objsize()); +} + +TEST(MetaFields, TrivialConvertibilityBsonWithoutMetadata) { + // Test that an unmodified document without metadata is trivially convertible to BSON with and + // without metadata. + auto bsonWithoutMetadata = BSON("a" << 42); + Document doc(bsonWithoutMetadata); + ASSERT_TRUE(doc.isTriviallyConvertible()); + ASSERT_TRUE(doc.isTriviallyConvertibleWithMetadata()); +} + +TEST(MetaFields, TrivialConvertibilityNoBson) { + // A Document created with no backing BSON is not trivially convertible. + auto docNoBson = Document{{"a", 42}}; + ASSERT_FALSE(docNoBson.isTriviallyConvertible()); + ASSERT_FALSE(docNoBson.isTriviallyConvertibleWithMetadata()); + + // An empty Document is trivially convertible, since the default BSONObj is also empty. + auto emptyDoc = Document{}; + ASSERT_TRUE(emptyDoc.isTriviallyConvertible()); + ASSERT_TRUE(emptyDoc.isTriviallyConvertibleWithMetadata()); +} + +TEST(MetaFields, TrivialConvertibilityModified) { + // Modifying a document with a backing BSON renders it not trivially convertible. + MutableDocument mutDocModified(Document(BSON("a" << 42))); + mutDocModified.addField("b", Value(43)); + auto modifiedDoc = mutDocModified.freeze(); + ASSERT_FALSE(modifiedDoc.isTriviallyConvertible()); + ASSERT_FALSE(modifiedDoc.isTriviallyConvertibleWithMetadata()); +} + +TEST(MetaFields, TrivialConvertibilityMetadataModified) { + // Modifying the metadata of a document with a backing BSON renders it not trivially convertible + // with metadata. + MutableDocument mutDocModifiedMd( + Document::fromBsonWithMetaData(BSON(Document::metaFieldTextScore << 10.0))); + mutDocModifiedMd.metadata().setRandVal(20.0); + auto modifiedMdDoc = mutDocModifiedMd.freeze(); + ASSERT_FALSE(modifiedMdDoc.isTriviallyConvertible()); + ASSERT_FALSE(modifiedMdDoc.isTriviallyConvertibleWithMetadata()); } TEST(MetaFields, MetaFieldsIncludedInDocumentApproximateSize) { @@ -983,8 +1168,10 @@ TEST(MetaFields, MetaFieldsIncludedInDocumentApproximateSize) { const size_t bigMetadataDocSize = doc2.getApproximateSize(); ASSERT_GT(bigMetadataDocSize, smallMetadataDocSize); - // Do a sanity check on the amount of space taken by metadata in document 2. - ASSERT_LT(doc2.getMetadataApproximateSize(), 300U); + // Do a sanity check on the amount of space taken by metadata in document 2. Note that the size + // of certain data types may vary on different build variants, so we cannot assert on the exact + // size. + ASSERT_LT(doc2.getMetadataApproximateSize(), 400U); Document emptyDoc; ASSERT_LT(emptyDoc.getMetadataApproximateSize(), 100U); diff --git a/src/mongo/db/exec/document_value/value.cpp b/src/mongo/db/exec/document_value/value.cpp index 248514180f0..245ec950a0b 100644 --- a/src/mongo/db/exec/document_value/value.cpp +++ b/src/mongo/db/exec/document_value/value.cpp @@ -45,6 +45,7 @@ #include "mongo/db/query/datetime/date_time_support.h" #include "mongo/platform/decimal128.h" #include "mongo/util/hex.h" +#include "mongo/util/murmur3.h" #include "mongo/util/represent_as.h" #include "mongo/util/str.h" @@ -935,7 +936,7 @@ void Value::hash_combine(size_t& seed, case Code: case Symbol: { StringData sd = getRawData(); - MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); + seed = murmur3<sizeof(size_t)>(sd, seed); break; } @@ -944,7 +945,7 @@ void Value::hash_combine(size_t& seed, if (stringComparator) { stringComparator->hash_combine(seed, sd); } else { - MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); + seed = murmur3<sizeof(size_t)>(sd, seed); } break; } @@ -968,14 +969,14 @@ void Value::hash_combine(size_t& seed, case BinData: { StringData sd = getRawData(); - MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); + seed = murmur3<sizeof(size_t)>(sd, seed); boost::hash_combine(seed, _storage.binDataType()); break; } case RegEx: { StringData sd = getRawData(); - MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); + seed = murmur3<sizeof(size_t)>(sd, seed); break; } diff --git a/src/mongo/db/exec/document_value/value_comparator_test.cpp b/src/mongo/db/exec/document_value/value_comparator_test.cpp index 963a29bc11a..ac854fdadbe 100644 --- a/src/mongo/db/exec/document_value/value_comparator_test.cpp +++ b/src/mongo/db/exec/document_value/value_comparator_test.cpp @@ -312,5 +312,33 @@ TEST(ValueComparatorTest, HashingCodeShouldNotRespectCollation) { ASSERT_NE(comparator.hash(val1), comparator.hash(val2)); } +// This test was originally designed to reproduce SERVER-78126. +TEST(ValueComparatorTest, ArraysDifferingByOneStringShouldHaveDifferentHashes) { + const ValueComparator comparator{}; + const Value val1{std::vector<Value>{Value{std::string{"a"}}, Value{std::string{"x"}}}}; + const Value val2{std::vector<Value>{Value{std::string{"b"}}, Value{std::string{"x"}}}}; + ASSERT_NE(comparator.compare(val1, val2), 0); + ASSERT_NE(comparator.hash(val1), comparator.hash(val2)); +} + +TEST(ValueComparatorTest, ArraysDifferingByOneStringShouldHaveDifferentHashesWithCollation) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); + const ValueComparator comparator{&collator}; + const Value val1{std::vector<Value>{Value{std::string{"abc"}}, Value{std::string{"xyz"}}}}; + const Value val2{std::vector<Value>{Value{std::string{"bcd"}}, Value{std::string{"xyz"}}}}; + ASSERT_NE(comparator.compare(val1, val2), 0); + ASSERT_NE(comparator.hash(val1), comparator.hash(val2)); +} + +TEST(ValueComparatorTest, ObjectsDifferingByOneStringShouldHaveDifferentHashes) { + const ValueComparator comparator{}; + const Value val1( + Document({{"foo"_sd, Value{std::string{"abc"}}}, {"bar"_sd, Value{std::string{"xyz"}}}})); + const Value val2( + Document({{"foo"_sd, Value{std::string{"def"}}}, {"bar"_sd, Value{std::string{"xyz"}}}})); + ASSERT_NE(comparator.compare(val1, val2), 0); + ASSERT_NE(comparator.hash(val1), comparator.hash(val2)); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/exec/exclusion_projection_executor.h b/src/mongo/db/exec/exclusion_projection_executor.h index a9c1fade72d..eb75f86fa20 100644 --- a/src/mongo/db/exec/exclusion_projection_executor.h +++ b/src/mongo/db/exec/exclusion_projection_executor.h @@ -149,7 +149,7 @@ public: return {DocumentSource::GetModPathsReturn::Type::kAllPaths, {}, {}}; } - std::set<std::string> modifiedPaths; + OrderedPathSet modifiedPaths; _root->reportProjectedPaths(&modifiedPaths); return {DocumentSource::GetModPathsReturn::Type::kFiniteSet, std::move(modifiedPaths), {}}; } diff --git a/src/mongo/db/exec/exclusion_projection_executor_test.cpp b/src/mongo/db/exec/exclusion_projection_executor_test.cpp index 983c5e1995a..c3866ef865f 100644 --- a/src/mongo/db/exec/exclusion_projection_executor_test.cpp +++ b/src/mongo/db/exec/exclusion_projection_executor_test.cpp @@ -363,7 +363,8 @@ TEST(ExclusionProjectionExecutionTest, ShouldEvaluateMetaExpressions) { "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}}")); + "l: {$meta: 'searchScoreDetails'}, " + "m: {$meta: 'vectorSearchScore'}}")); MutableDocument inputDocBuilder(Document{{"a", 1}, {"b", 2}}); inputDocBuilder.metadata().setTextScore(0.0); @@ -377,6 +378,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldEvaluateMetaExpressions) { inputDocBuilder.metadata().setSortKey(Value{Document{{"bar", 8}}}, true); inputDocBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); + inputDocBuilder.metadata().setVectorSearchScore(9.0); Document inputDoc = inputDocBuilder.freeze(); auto result = exclusion->applyTransformation(inputDoc); @@ -384,7 +386,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldEvaluateMetaExpressions) { ASSERT_DOCUMENT_EQ(result, Document{fromjson("{b: 2, c: 0.0, d: 1.0, e: 2.0, f: 'foo', g: 3.0, " "h: [4, 5], i: 6, j: {foo: 7}, k: [{bar: 8}]," - "l: {scoreDetails: 'foo'}}")}); + "l: {scoreDetails: 'foo'}, m: 9.0}")}); } TEST(ExclusionProjectionExecutionTest, ShouldAddMetaExpressionsToDependencies) { @@ -398,18 +400,20 @@ TEST(ExclusionProjectionExecutionTest, ShouldAddMetaExpressionsToDependencies) { "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}}")); + "l: {$meta: 'searchScoreDetails'}}, " + "m: {$meta: 'vectorSearchScore'}")); DepsTracker deps; exclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 0UL); - // We do not add the dependencies for searchScore, searchHighlights, or searchScoreDetails - // because those values are not stored in the collection (or in mongod at all). + // We do not add the dependencies for searchScore, searchHighlights, searchScoreDetails, or + // distance because those values are not stored in the collection (or in mongod at all). ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kSearchScore]); ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kSearchHighlights]); ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kSearchScoreDetails]); + ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kVectorSearchScore]); ASSERT_TRUE(deps.metadataDeps()[DocumentMetadataFields::kTextScore]); ASSERT_TRUE(deps.metadataDeps()[DocumentMetadataFields::kRandVal]); diff --git a/src/mongo/db/exec/inclusion_projection_executor.cpp b/src/mongo/db/exec/inclusion_projection_executor.cpp index d7384be8109..c2a759be998 100644 --- a/src/mongo/db/exec/inclusion_projection_executor.cpp +++ b/src/mongo/db/exec/inclusion_projection_executor.cpp @@ -99,6 +99,50 @@ boost::intrusive_ptr<Expression> substituteInExpr(boost::intrusive_ptr<Expressio } return ex; }; + +/** + * Returns a vector of top-level dependencies where each index i in the vector corresponds to the + * dependencies from the ith expression according to 'orderToProcess'. + */ +std::vector<OrderedPathSet> getTopLevelDeps( + const std::vector<std::string>& orderToProcess, + const StringMap<boost::intrusive_ptr<Expression>>& expressions, + const StringMap<std::unique_ptr<ProjectionNode>>& children) { + std::vector<OrderedPathSet> topLevelDeps; + for (const auto& field : orderToProcess) { + DepsTracker deps; + if (auto exprIt = expressions.find(field); exprIt != expressions.end()) { + exprIt->second->addDependencies(&deps); + } else { + // Each expression in orderToProcess should either be in expressions or children. + auto childIt = children.find(field); + tassert(6657000, "Unable to calculate dependencies", childIt != children.end()); + childIt->second->reportDependencies(&deps); + } + + OrderedPathSet ops{deps.fields.begin(), deps.fields.end()}; + topLevelDeps.push_back( + DepsTracker::simplifyDependencies(ops, DepsTracker::TruncateToRootLevel::yes)); + } + return topLevelDeps; +} + +/** + * Returns whether or not there is an expression in the projection which depends on 'field' other + * than the expression which computes 'field'. For example, given field "a" and projection + * {a: "$b", c: {$sum: ["$a", 5]}}, return true. Given field "a" and projection + * {a: {$sum: ["$a", 5]}, c: "$b"}, return false. 'field' should be a top level path. + */ +bool computedExprDependsOnField(const std::vector<OrderedPathSet>& topLevelDeps, + const std::string& field, + const size_t fieldIndex) { + for (size_t i = 0; i < topLevelDeps.size(); i++) { + if (i != fieldIndex && topLevelDeps[i].count(field) > 0) { + return true; + } + } + return false; +} } // namespace std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( @@ -109,8 +153,8 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( return {BSONObj{}, false}; } - DepsTracker allDeps; - reportDependencies(&allDeps); + std::vector<OrderedPathSet> topLevelDeps = + getTopLevelDeps(_orderToProcessAdditionsAndChildren, _expressions, _children); // Auxiliary vector with extracted computed projections: <name, expression, replacement // strategy>. If the replacement strategy flag is true, the expression is replaced with a @@ -118,19 +162,15 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( std::vector<std::tuple<StringData, boost::intrusive_ptr<Expression>, bool>> addFieldsExpressions; bool replaceWithProjField = true; - for (auto&& field : _orderToProcessAdditionsAndChildren) { + for (size_t i = 0; i < _orderToProcessAdditionsAndChildren.size(); i++) { + auto&& field = _orderToProcessAdditionsAndChildren[i]; + if (reservedNames.count(field) > 0) { // Do not pushdown computed projection with reserved name. replaceWithProjField = false; continue; } - if (allDeps.fields.count(field) > 0) { - // Do not extract a computed projection if its name is the same as a dependent field. If - // the extracted $addFields were to be placed before this projection, the dependency - // with the common name would be shadowed by the computed projection. - replaceWithProjField = false; - continue; - } + auto expressionIt = _expressions.find(field); if (expressionIt == _expressions.end()) { // After seeing the first dotted path expression we need to replace computed @@ -138,13 +178,17 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( replaceWithProjField = false; continue; } - DepsTracker deps; - expressionIt->second->addDependencies(&deps); - auto topLevelFieldNames = - deps.toProjectionWithoutMetadata(DepsTracker::TruncateToRootLevel::yes) - .getFieldNames<std::set<std::string>>(); - topLevelFieldNames.erase("_id"); + // Do not extract a computed projection if it is computing a value that other fields in the + // same projection depend on. If the extracted $addFields were to be placed before this + // projection, the dependency with the common name would be shadowed by the computed + // projection. + if (computedExprDependsOnField(topLevelDeps, field, i)) { + replaceWithProjField = false; + continue; + } + + const auto& topLevelFieldNames = topLevelDeps[i]; if (topLevelFieldNames.size() == 1 && topLevelFieldNames.count(oldName.toString()) == 1) { // Substitute newName for oldName in the expression. StringMap<std::string> renames; @@ -197,35 +241,34 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInAddFields( return {BSONObj{}, false}; } - DepsTracker allDeps; - reportDependencies(&allDeps); + std::vector<OrderedPathSet> topLevelDeps = + getTopLevelDeps(_orderToProcessAdditionsAndChildren, _expressions, _children); // Auxiliary vector with extracted computed projections: <name, expression>. // To preserve the original fields order, only projections at the beginning of the // _orderToProcessAdditionsAndChildren list can be extracted for pushdown. std::vector<std::pair<StringData, boost::intrusive_ptr<Expression>>> addFieldsExpressions; - for (auto&& field : _orderToProcessAdditionsAndChildren) { + for (size_t i = 0; i < _orderToProcessAdditionsAndChildren.size(); i++) { + auto&& field = _orderToProcessAdditionsAndChildren[i]; // Do not extract for pushdown computed projection with reserved name. if (reservedNames.count(field) > 0) { break; } - if (allDeps.fields.count(field) > 0) { - // Do not extract a computed projection if its name is the same as a dependent field. If - // the extracted $addFields were to be placed before this $addFields, the dependency - // with the common name would be shadowed by the computed projection. - break; - } + auto expressionIt = _expressions.find(field); if (expressionIt == _expressions.end()) { break; } - DepsTracker deps; - expressionIt->second->addDependencies(&deps); - auto topLevelFieldNames = - deps.toProjectionWithoutMetadata(DepsTracker::TruncateToRootLevel::yes) - .getFieldNames<std::set<std::string>>(); - topLevelFieldNames.erase("_id"); + // Do not extract a computed projection if it is computing a value that other fields in the + // same projection depend on. If the extracted $addFields were to be placed before this + // projection, the dependency with the common name would be shadowed by the computed + // projection. + if (computedExprDependsOnField(topLevelDeps, field, i)) { + break; + } + + auto& topLevelFieldNames = topLevelDeps[i]; if (topLevelFieldNames.size() == 1 && topLevelFieldNames.count(oldName.toString()) == 1) { // Substitute newName for oldName in the expression. StringMap<std::string> renames; diff --git a/src/mongo/db/exec/inclusion_projection_executor.h b/src/mongo/db/exec/inclusion_projection_executor.h index a0429ff924f..f0dd72aae80 100644 --- a/src/mongo/db/exec/inclusion_projection_executor.h +++ b/src/mongo/db/exec/inclusion_projection_executor.h @@ -237,10 +237,10 @@ public: return {DocumentSource::GetModPathsReturn::Type::kAllPaths, {}, {}}; } - std::set<std::string> preservedPaths; + OrderedPathSet preservedPaths; _root->reportProjectedPaths(&preservedPaths); - std::set<std::string> computedPaths; + OrderedPathSet computedPaths; StringMap<std::string> renamedPaths; _root->reportComputedPaths(&computedPaths, &renamedPaths); diff --git a/src/mongo/db/exec/inclusion_projection_executor_test.cpp b/src/mongo/db/exec/inclusion_projection_executor_test.cpp index 84ed31a9bb4..b31899043ca 100644 --- a/src/mongo/db/exec/inclusion_projection_executor_test.cpp +++ b/src/mongo/db/exec/inclusion_projection_executor_test.cpp @@ -814,18 +814,20 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}}")); + "l: {$meta: 'searchScoreDetails'}}, " + "m: {$meta: 'vectorSearchScore'}")); DepsTracker deps; inclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 2UL); - // We do not add the dependencies for searchScore, searchHighlights, or searchScoreDetails - // because those values are not stored in the collection (or in mongod at all). + // We do not add the dependencies for searchScore, searchHighlights, searchScoreDetails, or + // distance because those values are not stored in the collection (or in mongod at all). ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kSearchScore]); ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kSearchHighlights]); ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kSearchScoreDetails]); + ASSERT_FALSE(deps.metadataDeps()[DocumentMetadataFields::kVectorSearchScore]); ASSERT_TRUE(deps.metadataDeps()[DocumentMetadataFields::kTextScore]); ASSERT_TRUE(deps.metadataDeps()[DocumentMetadataFields::kRandVal]); @@ -847,7 +849,8 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ShouldEvaluateMeta "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}}")); + "l: {$meta: 'searchScoreDetails'}, " + "m: {$meta: 'vectorSearchScore'}}")); MutableDocument inputDocBuilder(Document{{"a", 1}}); inputDocBuilder.metadata().setTextScore(0.0); @@ -861,6 +864,7 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ShouldEvaluateMeta inputDocBuilder.metadata().setSortKey(Value{Document{{"bar", 8}}}, true); inputDocBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); + inputDocBuilder.metadata().setVectorSearchScore(9.0); Document inputDoc = inputDocBuilder.freeze(); auto result = inclusion->applyTransformation(inputDoc); @@ -868,7 +872,7 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ShouldEvaluateMeta ASSERT_DOCUMENT_EQ(result, Document{fromjson("{a: 1, c: 0.0, d: 1.0, e: 2.0, f: 'foo', g: 3.0, " "h: [4, 5], i: 6, j: {foo: 7}, k: [{bar: 8}], " - "l: {scoreDetails: 'foo'}}")}); + "l: {scoreDetails: 'foo'}, m: 9.0}")}); } // @@ -1090,6 +1094,62 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ASSERT_DOCUMENT_EQ(expectedProjection, inclusion->serializeTransformation(boost::none)); } +TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, + ExtractComputedProjectionInProjectShouldNotIncludeId) { + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a" << BSON("$sum" << BSON_ARRAY("$myMeta" + << "$_id")))); + + auto r = static_cast<InclusionProjectionExecutor*>(inclusion.get())->getRoot(); + const std::set<StringData> reservedNames{}; + auto [addFields, deleteFlag] = + r->extractComputedProjectionsInProject("myMeta", "meta", reservedNames); + + ASSERT_EQ(addFields.nFields(), 0); + ASSERT_EQ(deleteFlag, false); + + auto expectedProjection = Document(fromjson("{_id: true, a: {$sum: ['$myMeta', '$_id']}}")); + ASSERT_DOCUMENT_EQ(expectedProjection, inclusion->serializeTransformation(boost::none)); +} + +TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, + ExtractComputedProjectionInProjectShouldNotHideDependentSubFields) { + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" + << "$myMeta" + << "b" + << "$a.x")); + + auto r = static_cast<InclusionProjectionExecutor*>(inclusion.get())->getRoot(); + const std::set<StringData> reservedNames{}; + auto [addFields, deleteFlag] = + r->extractComputedProjectionsInProject("myMeta", "meta", reservedNames); + + ASSERT_EQ(addFields.nFields(), 0); + ASSERT_EQ(deleteFlag, false); + + auto expectedProjection = Document(fromjson("{_id: true, a: '$myMeta', b: '$a.x'}")); + ASSERT_DOCUMENT_EQ(expectedProjection, inclusion->serializeTransformation(boost::none)); +} + +TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, + ExtractComputedProjectionInProjectShouldNotHideDependentSubFieldsWithDottedSibling) { + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" + << "$myMeta" + << "c.b" + << "$a.x")); + + auto r = static_cast<InclusionProjectionExecutor*>(inclusion.get())->getRoot(); + const std::set<StringData> reservedNames{}; + auto [addFields, deleteFlag] = + r->extractComputedProjectionsInProject("myMeta", "meta", reservedNames); + + ASSERT_EQ(addFields.nFields(), 0); + ASSERT_EQ(deleteFlag, false); + + auto expectedProjection = Document(fromjson("{_id: true, a: '$myMeta', c: {b: '$a.x'}}")); + ASSERT_DOCUMENT_EQ(expectedProjection, inclusion->serializeTransformation(boost::none)); +} + TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ApplyProjectionAfterSplit) { auto inclusion = makeInclusionProjectionWithDefaultPolicies( BSON("a" << true << "computedMeta1" diff --git a/src/mongo/db/exec/multi_plan.cpp b/src/mongo/db/exec/multi_plan.cpp index def29719b63..00254cef133 100644 --- a/src/mongo/db/exec/multi_plan.cpp +++ b/src/mongo/db/exec/multi_plan.cpp @@ -190,14 +190,13 @@ PlanStage::StageState MultiPlanStage::doWork(WorkingSetID* out) { .getPlanCache() ->remove(plan_cache_key_factory::make<PlanCacheKey>(*_query, collection())); - _bestPlanIdx = _backupPlanIdx; - _backupPlanIdx = kNoSuchPlan; + switchToBackupPlan(); return _candidates[_bestPlanIdx].root->work(out); } if (hasBackupPlan() && PlanStage::ADVANCED == state) { LOGV2_DEBUG(20589, 5, "Best plan had a blocking stage, became unblocked"); - _backupPlanIdx = kNoSuchPlan; + removeBackupPlan(); } return state; @@ -294,6 +293,7 @@ Status MultiPlanStage::pickBestPlan(PlanYieldPolicy* yieldPolicy) { plan_cache_util::updatePlanCache( expCtx()->opCtx, collection(), _cachingMode, *_query, std::move(ranking), _candidates); + removeRejectedPlans(); return Status::OK(); } @@ -364,6 +364,54 @@ bool MultiPlanStage::workAllPlans(size_t numResults, PlanYieldPolicy* yieldPolic return !doneWorking; } +void MultiPlanStage::removeRejectedPlans() { + // Move the best plan and the backup plan to the front of 'children'. + if (_bestPlanIdx != 0) { + std::swap(_children[_bestPlanIdx], _children[0]); + std::swap(_candidates[_bestPlanIdx], _candidates[0]); + if (_backupPlanIdx == 0) { + _backupPlanIdx = _bestPlanIdx; + } + _bestPlanIdx = 0; + } + size_t startIndex = 1; + if (_backupPlanIdx != kNoSuchPlan) { + if (_backupPlanIdx != 1) { + std::swap(_children[_backupPlanIdx], _children[1]); + std::swap(_candidates[_backupPlanIdx], _candidates[1]); + _backupPlanIdx = 1; + } + startIndex = 2; + } + + _rejected.reserve(_children.size() - startIndex); + for (size_t i = startIndex; i < _children.size(); ++i) { + rejectPlan(i); + } + _children.resize(startIndex); +} + +void MultiPlanStage::switchToBackupPlan() { + std::swap(_children[_backupPlanIdx], _children[_bestPlanIdx]); + std::swap(_candidates[_backupPlanIdx], _candidates[_bestPlanIdx]); + removeBackupPlan(); +} + +void MultiPlanStage::rejectPlan(size_t planIdx) { + auto rejectedPlan = std::move(_children[planIdx]); + if (opCtx() != nullptr) { + rejectedPlan->saveState(); + rejectedPlan->detachFromOperationContext(); + } + _rejected.emplace_back(std::move(rejectedPlan)); +} + +void MultiPlanStage::removeBackupPlan() { + rejectPlan(_backupPlanIdx); + _children.resize(1); + _backupPlanIdx = kNoSuchPlan; +} + bool MultiPlanStage::hasBackupPlan() const { return kNoSuchPlan != _backupPlanIdx; } @@ -398,6 +446,9 @@ unique_ptr<PlanStageStats> MultiPlanStage::getStats() { for (auto&& child : _children) { ret->children.emplace_back(child->getStats()); } + for (auto&& child : _rejected) { + ret->children.emplace_back(child->getStats()); + } return ret; } diff --git a/src/mongo/db/exec/multi_plan.h b/src/mongo/db/exec/multi_plan.h index 14d8ea413af..12ab61659db 100644 --- a/src/mongo/db/exec/multi_plan.h +++ b/src/mongo/db/exec/multi_plan.h @@ -75,7 +75,6 @@ public: std::unique_ptr<PlanStageStats> getStats() final; - const SpecificStats* getSpecificStats() const final; boost::optional<double> getCandidateScore(size_t candidateIdx) const; @@ -163,6 +162,19 @@ private: */ void tryYield(PlanYieldPolicy* yieldPolicy); + /** + * Deletes all children, except for best and backup plans. + * + * This is necessary to release any resources that rejected plans might have. + * For example, if multi-update can be done by scanning several indexes, + * it will be slowed down by rejected index scans because of index cursors + * that need to be reopeneed after every update. + */ + void removeRejectedPlans(); + void rejectPlan(size_t planIdx); + void switchToBackupPlan(); + void removeBackupPlan(); + static const int kNoSuchPlan = -1; // Describes the cases in which we should write an entry for the winning plan to the plan cache. @@ -178,6 +190,9 @@ private: // one-to-one with _candidates. std::vector<plan_ranker::CandidatePlan> _candidates; + // Rejected plans in saved and detached state. + std::vector<std::unique_ptr<PlanStage>> _rejected; + // index into _candidates, of the winner of the plan competition // uses -1 / kNoSuchPlan when best plan is not (yet) known int _bestPlanIdx; diff --git a/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp b/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp index bdf5dea8241..fc1e74a0c2e 100644 --- a/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp +++ b/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp @@ -74,7 +74,7 @@ std::unique_ptr<ProjectionExecutor> makeProjectionWithDefaultIdExclusionAndNeste return createProjectionExecutor(projSpec, policies); } -std::set<FieldRef> toFieldRefs(const std::set<std::string>& stringPaths) { +std::set<FieldRef> toFieldRefs(const OrderedPathSet& stringPaths) { std::set<FieldRef> fieldRefs; std::transform(stringPaths.begin(), stringPaths.end(), diff --git a/src/mongo/db/exec/projection_node.cpp b/src/mongo/db/exec/projection_node.cpp index 1ef569ecd75..00fcf70946f 100644 --- a/src/mongo/db/exec/projection_node.cpp +++ b/src/mongo/db/exec/projection_node.cpp @@ -229,7 +229,7 @@ Value ProjectionNode::applyExpressionsToValue(const Document& root, Value inputV } } -void ProjectionNode::reportProjectedPaths(std::set<std::string>* projectedPaths) const { +void ProjectionNode::reportProjectedPaths(OrderedPathSet* projectedPaths) const { for (auto&& projectedField : _projectedFields) { projectedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, projectedField)); } @@ -239,7 +239,7 @@ void ProjectionNode::reportProjectedPaths(std::set<std::string>* projectedPaths) } } -void ProjectionNode::reportComputedPaths(std::set<std::string>* computedPaths, +void ProjectionNode::reportComputedPaths(OrderedPathSet* computedPaths, StringMap<std::string>* renamedPaths) const { for (auto&& computedPair : _expressions) { // The expression's path is the concatenation of the path to this node, plus the field name diff --git a/src/mongo/db/exec/projection_node.h b/src/mongo/db/exec/projection_node.h index 2a587580330..cf79c26719c 100644 --- a/src/mongo/db/exec/projection_node.h +++ b/src/mongo/db/exec/projection_node.h @@ -102,7 +102,7 @@ public: /** * Recursively report all paths that are referenced by this projection. */ - void reportProjectedPaths(std::set<std::string>* preservedPaths) const; + void reportProjectedPaths(OrderedPathSet* preservedPaths) const; /** * Return an optional number, x, which indicates that it is safe to stop reading the document @@ -119,7 +119,7 @@ public: * 'renamedPaths'. Each entry in 'renamedPaths' maps from the path's new name to its old name * prior to application of this projection. */ - void reportComputedPaths(std::set<std::string>* computedPaths, + void reportComputedPaths(OrderedPathSet* computedPaths, StringMap<std::string>* renamedPaths) const; const std::string& getPath() const { diff --git a/src/mongo/db/exec/sbe/SConscript b/src/mongo/db/exec/sbe/SConscript index b99625b0833..f8246baf556 100644 --- a/src/mongo/db/exec/sbe/SConscript +++ b/src/mongo/db/exec/sbe/SConscript @@ -23,7 +23,7 @@ env.Library( '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/exec/js_function', '$BUILD_DIR/mongo/db/fts/base_fts', - '$BUILD_DIR/mongo/db/index/key_generator', + '$BUILD_DIR/mongo/db/index/index_access_method', '$BUILD_DIR/mongo/db/query/collation/collator_interface', '$BUILD_DIR/mongo/db/query/datetime/date_time_support', '$BUILD_DIR/mongo/db/query/query_index_bounds', @@ -59,8 +59,9 @@ sbeEnv.Library( LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/bson/dotted_path_support', '$BUILD_DIR/mongo/db/sorter/sorter_idl', - ] - ) + '$BUILD_DIR/mongo/db/sorter/sorter_stats', + ], +) sbeEnv.Library( target='query_sbe_stages', @@ -89,7 +90,6 @@ sbeEnv.Library( LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/concurrency/lock_manager', - '$BUILD_DIR/mongo/db/concurrency/write_conflict_exception', '$BUILD_DIR/mongo/db/exec/js_function', '$BUILD_DIR/mongo/db/exec/scoped_timer', '$BUILD_DIR/mongo/db/query/plan_yield_policy', @@ -106,6 +106,7 @@ sbeEnv.Library( LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/bson/dotted_path_support', '$BUILD_DIR/mongo/db/sorter/sorter_idl', + '$BUILD_DIR/mongo/db/sorter/sorter_stats', 'query_sbe', 'query_sbe_storage', ] diff --git a/src/mongo/db/exec/sbe/abt/abt_lower.cpp b/src/mongo/db/exec/sbe/abt/abt_lower.cpp index 7da6a8e2cef..7939d166a78 100644 --- a/src/mongo/db/exec/sbe/abt/abt_lower.cpp +++ b/src/mongo/db/exec/sbe/abt/abt_lower.cpp @@ -586,14 +586,15 @@ std::unique_ptr<sbe::PlanStage> SBENodeLowering::walk(const GroupByNode& n, auto& names = binderAgg->names(); auto& exprs = refsAgg->nodes(); - sbe::value::SlotMap<std::unique_ptr<sbe::EExpression>> aggs; + sbe::SlotExprPairVector aggs; + aggs.reserve(exprs.size()); for (size_t idx = 0; idx < exprs.size(); ++idx) { auto expr = SBEExpressionLowering{_env, _slotMap}.optimize(exprs[idx]); auto slot = _slotIdGenerator.generate(); _slotMap.emplace(names[idx], slot); - aggs.emplace(slot, std::move(expr)); + aggs.push_back({slot, std::move(expr)}); } // TODO: use collator slot. @@ -609,6 +610,11 @@ std::unique_ptr<sbe::PlanStage> SBENodeLowering::walk(const GroupByNode& n, true /*optimizedClose*/, collatorSlot, false /*allowDiskUse*/, + // Since we are always disallowing disk use for this stage, + // we need not provide merging expressions. Once spilling + // is permitted here, we will need to generate merging + // expressions during lowering. + sbe::makeSlotExprPairVec() /*mergingExprs*/, planNodeId); } @@ -1005,6 +1011,7 @@ std::unique_ptr<sbe::PlanStage> SBENodeLowering::walk(const IndexScanNode& n, co resultSlot, ridSlot, boost::none, + boost::none, indexKeysToInclude, vars, seekKeySlotLower, diff --git a/src/mongo/db/exec/sbe/expression_test_base.h b/src/mongo/db/exec/sbe/expression_test_base.h index 72d9820d760..bc5121407f9 100644 --- a/src/mongo/db/exec/sbe/expression_test_base.h +++ b/src/mongo/db/exec/sbe/expression_test_base.h @@ -69,6 +69,24 @@ protected: } /** + * Compiles 'expr' to bytecode when 'expr' is computing an aggregate. The current aggregate + * value can be read out of the provided 'aggAccessor'. + * + * Note that when actually executing the resulting bytecode, the caller is responsible for + * setting the value of 'aggAccessor' to the new resulting aggregate value. + */ + std::unique_ptr<vm::CodeFragment> compileAggExpression(const EExpression& expr, + value::SlotAccessor* aggAccessor) { + ON_BLOCK_EXIT([this] { + _ctx.aggExpression = false; + _ctx.accumulator = nullptr; + }); + _ctx.aggExpression = true; + _ctx.accumulator = aggAccessor; + return expr.compile(_ctx); + } + + /** * The caller takes ownership of the Value returned by this function and must call * 'releaseValue()' on it. The preferred way to ensure the Value is properly released is to * immediately store it in a ValueGuard. diff --git a/src/mongo/db/exec/sbe/expressions/expression.cpp b/src/mongo/db/exec/sbe/expressions/expression.cpp index 3a2b20a4657..65421e8b373 100644 --- a/src/mongo/db/exec/sbe/expressions/expression.cpp +++ b/src/mongo/db/exec/sbe/expressions/expression.cpp @@ -429,6 +429,8 @@ static stdx::unordered_map<std::string, BuiltinFn> kBuiltinFunctions = { BuiltinFn{[](size_t n) { return n > 0; }, vm::Builtin::doubleDoubleSum, false}}, {"aggDoubleDoubleSum", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::aggDoubleDoubleSum, true}}, + {"aggMergeDoubleDoubleSums", + BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::aggMergeDoubleDoubleSums, true}}, {"doubleDoubleSumFinalize", BuiltinFn{[](size_t n) { return n > 0; }, vm::Builtin::doubleDoubleSumFinalize, false}}, {"doubleDoubleMergeSumFinalize", @@ -436,6 +438,8 @@ static stdx::unordered_map<std::string, BuiltinFn> kBuiltinFunctions = { {"doubleDoublePartialSumFinalize", BuiltinFn{[](size_t n) { return n > 0; }, vm::Builtin::doubleDoublePartialSumFinalize, false}}, {"aggStdDev", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::aggStdDev, true}}, + {"aggMergeStdDevs", + BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::aggMergeStdDevs, true}}, {"stdDevPopFinalize", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::stdDevPopFinalize, false}}, {"stdDevSampFinalize", @@ -467,7 +471,10 @@ static stdx::unordered_map<std::string, BuiltinFn> kBuiltinFunctions = { {"tan", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::tan, false}}, {"tanh", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::tanh, false}}, {"round", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::round, false}}, - {"concat", BuiltinFn{[](size_t n) { return n > 0; }, vm::Builtin::concat, false}}, + {"concat", BuiltinFn{kAnyNumberOfArgs, vm::Builtin::concat, false}}, + {"concatArrays", BuiltinFn{kAnyNumberOfArgs, vm::Builtin::concatArrays, false}}, + {"aggConcatArraysCapped", + BuiltinFn{[](size_t n) { return n == 2; }, vm::Builtin::aggConcatArraysCapped, true}}, {"isMember", BuiltinFn{[](size_t n) { return n == 2; }, vm::Builtin::isMember, false}}, {"collIsMember", BuiltinFn{[](size_t n) { return n == 3; }, vm::Builtin::collIsMember, false}}, {"indexOfBytes", @@ -486,6 +493,11 @@ static stdx::unordered_map<std::string, BuiltinFn> kBuiltinFunctions = { BuiltinFn{[](size_t n) { return n >= 1; }, vm::Builtin::collSetIntersection, false}}, {"collSetDifference", BuiltinFn{[](size_t n) { return n == 3; }, vm::Builtin::collSetDifference, false}}, + {"aggSetUnion", BuiltinFn{[](size_t n) { return n == 1; }, vm::Builtin::aggSetUnion, true}}, + {"aggSetUnionCapped", + BuiltinFn{[](size_t n) { return n == 2; }, vm::Builtin::aggSetUnionCapped, true}}, + {"aggCollSetUnionCapped", + BuiltinFn{[](size_t n) { return n == 3; }, vm::Builtin::aggCollSetUnionCapped, true}}, {"runJsPredicate", BuiltinFn{[](size_t n) { return n == 2; }, vm::Builtin::runJsPredicate, false}}, {"regexCompile", BuiltinFn{[](size_t n) { return n == 2; }, vm::Builtin::regexCompile, false}}, diff --git a/src/mongo/db/exec/sbe/expressions/expression.h b/src/mongo/db/exec/sbe/expressions/expression.h index 2b9032f257b..d5a2c0fdf0e 100644 --- a/src/mongo/db/exec/sbe/expressions/expression.h +++ b/src/mongo/db/exec/sbe/expressions/expression.h @@ -348,6 +348,8 @@ private: std::string toString() const; }; +using SlotExprPairVector = std::vector<std::pair<value::SlotId, std::unique_ptr<EExpression>>>; + template <typename T, typename... Args> inline std::unique_ptr<EExpression> makeE(Args&&... args) { return std::make_unique<T>(std::forward<Args>(args)...); @@ -364,20 +366,30 @@ inline auto makeEs(Ts&&... pack) { namespace detail { // base case -inline void makeEM_unwind(value::SlotMap<std::unique_ptr<EExpression>>& result, - value::SlotId slot, - std::unique_ptr<EExpression> expr) { - result.emplace(slot, std::move(expr)); +template <typename R> +inline void makeSlotExprPairHelper(R& result, + value::SlotId slot, + std::unique_ptr<EExpression> expr) { + if constexpr (std::is_same_v<R, value::SlotMap<std::unique_ptr<EExpression>>>) { + result.emplace(slot, std::move(expr)); + } else { + result.push_back({slot, std::move(expr)}); + } } // recursive case -template <typename... Ts> -inline void makeEM_unwind(value::SlotMap<std::unique_ptr<EExpression>>& result, - value::SlotId slot, - std::unique_ptr<EExpression> expr, - Ts&&... rest) { - result.emplace(slot, std::move(expr)); - makeEM_unwind(result, std::forward<Ts>(rest)...); +template <typename R, typename... Ts> +inline void makeSlotExprPairHelper(R& result, + value::SlotId slot, + std::unique_ptr<EExpression> expr, + Ts&&... rest) { + if constexpr (std::is_same_v<R, value::SlotMap<std::unique_ptr<EExpression>>>) { + result.emplace(slot, std::move(expr)); + } else { + static_assert(std::is_same_v<R, SlotExprPairVector>); + result.push_back({slot, std::move(expr)}); + } + makeSlotExprPairHelper(result, std::forward<Ts>(rest)...); } } // namespace detail @@ -386,7 +398,7 @@ auto makeEM(Ts&&... pack) { value::SlotMap<std::unique_ptr<EExpression>> result; if constexpr (sizeof...(pack) > 0) { result.reserve(sizeof...(Ts) / 2); - detail::makeEM_unwind(result, std::forward<Ts>(pack)...); + detail::makeSlotExprPairHelper(result, std::forward<Ts>(pack)...); } return result; } @@ -399,6 +411,16 @@ auto makeSV(Args&&... args) { return v; } +template <typename... Ts> +auto makeSlotExprPairVec(Ts&&... pack) { + SlotExprPairVector v; + if constexpr (sizeof...(pack) > 0) { + v.reserve(sizeof...(Ts) / 2); + detail::makeSlotExprPairHelper(v, std::forward<Ts>(pack)...); + } + return v; +} + /** * This is a constant expression. It assumes the ownership of the input constant. */ diff --git a/src/mongo/db/exec/sbe/expressions/sbe_set_expressions_test.cpp b/src/mongo/db/exec/sbe/expressions/sbe_set_expressions_test.cpp index 30eb61a7b2d..279f7a287b4 100644 --- a/src/mongo/db/exec/sbe/expressions/sbe_set_expressions_test.cpp +++ b/src/mongo/db/exec/sbe/expressions/sbe_set_expressions_test.cpp @@ -28,6 +28,7 @@ */ #include "mongo/db/exec/sbe/expression_test_base.h" +#include "mongo/db/query/sbe_stage_builder_helpers.h" namespace mongo::sbe { @@ -93,6 +94,35 @@ TEST_F(SBEBuiltinSetOpTest, ReturnsNothingSetUnion) { runAndAssertNothing(compiledExpr.get()); } +TEST_F(SBEBuiltinSetOpTest, AggSetUnion) { + value::OwnedValueAccessor aggAccessor, inputAccessor; + auto inputSlot = bindAccessor(&inputAccessor); + auto setUnionExpr = + stage_builder::makeFunction("aggSetUnion", stage_builder::makeVariable(inputSlot)); + auto compiledExpr = compileAggExpression(*setUnionExpr, &aggAccessor); + + auto [arrTag1, arrVal1] = makeArray(BSON_ARRAY(1 << 2)); + inputAccessor.reset(arrTag1, arrVal1); + auto [resTag1, resVal1] = makeArraySet(BSON_ARRAY(1 << 2)); + runAndAssertExpression(compiledExpr.get(), {resTag1, resVal1}); + aggAccessor.reset(resTag1, resVal1); + + auto [arrTag2, arrVal2] = makeArraySet(BSON_ARRAY(1 << 3 << 2 << 6)); + inputAccessor.reset(arrTag2, arrVal2); + auto [resTag2, resVal2] = makeArraySet(BSON_ARRAY(1 << 2 << 3 << 6)); + runAndAssertExpression(compiledExpr.get(), {resTag2, resVal2}); + aggAccessor.reset(resTag2, resVal2); + + auto [arrTag3, arrVal3] = makeArray(BSONArray{}); + inputAccessor.reset(arrTag3, arrVal3); + auto [resTag3, resVal3] = makeArraySet(BSON_ARRAY(1 << 2 << 3 << 6)); + runAndAssertExpression(compiledExpr.get(), {resTag3, resVal3}); + aggAccessor.reset(resTag3, resVal3); + + inputAccessor.reset(value::TypeTags::Nothing, 0); + runAndAssertNothing(compiledExpr.get()); +} + TEST_F(SBEBuiltinSetOpTest, ComputesSetIntersection) { value::OwnedValueAccessor slotAccessor1, slotAccessor2; auto arrSlot1 = bindAccessor(&slotAccessor1); diff --git a/src/mongo/db/exec/sbe/sbe_hash_agg_test.cpp b/src/mongo/db/exec/sbe/sbe_hash_agg_test.cpp index 87fc8dbd1f2..89244fd4589 100644 --- a/src/mongo/db/exec/sbe/sbe_hash_agg_test.cpp +++ b/src/mongo/db/exec/sbe/sbe_hash_agg_test.cpp @@ -69,18 +69,23 @@ void HashAggStageTest::performHashAggWithSpillChecking( auto makeStageFn = [this, collatorSlot, shouldUseCollator, shouldSpill]( value::SlotId scanSlot, std::unique_ptr<PlanStage> scanStage) { auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction("sum", + makeE<EConstant>(value::TypeTags::NumberInt64, + value::bitcastFrom<int64_t>(1)))), makeSV(), true, boost::optional<value::SlotId>{shouldUseCollator, collatorSlot}, shouldSpill, + makeSlotExprPairVec( + spillSlot, + stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); return std::make_pair(countsSlot, std::move(hashAggStage)); @@ -166,20 +171,21 @@ TEST_F(HashAggStageTest, HashAggMinMaxTest) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(), - makeEM(minSlot, - stage_builder::makeFunction("min", makeE<EVariable>(scanSlot)), - maxSlot, - stage_builder::makeFunction("max", makeE<EVariable>(scanSlot)), - collMinSlot, - stage_builder::makeFunction( - "collMin", collExpr->clone(), makeE<EVariable>(scanSlot)), - collMaxSlot, - stage_builder::makeFunction( - "collMax", collExpr->clone(), makeE<EVariable>(scanSlot))), + makeSlotExprPairVec(minSlot, + stage_builder::makeFunction("min", makeE<EVariable>(scanSlot)), + maxSlot, + stage_builder::makeFunction("max", makeE<EVariable>(scanSlot)), + collMinSlot, + stage_builder::makeFunction( + "collMin", collExpr->clone(), makeE<EVariable>(scanSlot)), + collMaxSlot, + stage_builder::makeFunction( + "collMax", collExpr->clone(), makeE<EVariable>(scanSlot))), makeSV(), true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec() /* mergingExprs */, kEmptyPlanNodeId); auto outSlot = generateSlotId(); @@ -230,13 +236,15 @@ TEST_F(HashAggStageTest, HashAggAddToSetTest) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(), - makeEM(hashAggSlot, - stage_builder::makeFunction( - "collAddToSet", std::move(collExpr), makeE<EVariable>(scanSlot))), + makeSlotExprPairVec(hashAggSlot, + stage_builder::makeFunction("collAddToSet", + std::move(collExpr), + makeE<EVariable>(scanSlot))), makeSV(), true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec() /* mergingExprs */, kEmptyPlanNodeId); return std::make_pair(hashAggSlot, std::move(hashAggStage)); @@ -327,14 +335,16 @@ TEST_F(HashAggStageTest, HashAggSeekKeysTest) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction("sum", + makeE<EConstant>(value::TypeTags::NumberInt64, + value::bitcastFrom<int64_t>(1)))), makeSV(seekSlot), true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec() /* mergingExprs */, kEmptyPlanNodeId); return std::make_pair(countsSlot, std::move(hashAggStage)); @@ -387,17 +397,21 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpill) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), makeSV(), // Seek slot true, boost::none, true /* allowDiskUse */, + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -420,6 +434,7 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpill) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_FALSE(stats->usedDisk); + ASSERT_EQ(0, stats->numSpills); ASSERT_EQ(0, stats->spilledRecords); stage->close(); @@ -428,7 +443,6 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpill) { TEST_F(HashAggStageTest, HashAggBasicCountSpill) { // We estimate the size of result row like {int64, int64} at 50B. Set the memory threshold to // 64B so that exactly one row fits in memory. - const int expectedRowsToFitInMemory = 1; auto defaultInternalQuerySBEAggApproxMemoryUseInBytesBeforeSpill = internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.load(); internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.store(64); @@ -446,17 +460,21 @@ TEST_F(HashAggStageTest, HashAggBasicCountSpill) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), makeSV(), // Seek slot true, boost::none, true /* allowDiskUse */, + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -479,7 +497,14 @@ TEST_F(HashAggStageTest, HashAggBasicCountSpill) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - ASSERT_EQ(results.size() - expectedRowsToFitInMemory, stats->spilledRecords); + // Memory usage is estimated only every two rows at the most frequent. Also, we only start + // spilling after estimating that the memory budget is exceeded. These two factors result in + // fewer expected spills than there are input records, even though only one record fits in + // memory at a time. + ASSERT_EQ(stats->numSpills, 3); + // The input has one run of two consecutive values, so we expect to spill as many records as + // there are input values minus one. + ASSERT_EQ(stats->spilledRecords, 8); stage->close(); } @@ -513,17 +538,21 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpillIfNoMemCheck) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), makeSV(), // Seek slot true, boost::none, true /* allowDiskUse */, + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -546,6 +575,7 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpillIfNoMemCheck) { // Check that it did not spill. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_FALSE(stats->usedDisk); + ASSERT_EQ(0, stats->numSpills); ASSERT_EQ(0, stats->spilledRecords); stage->close(); @@ -554,7 +584,6 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpillIfNoMemCheck) { TEST_F(HashAggStageTest, HashAggBasicCountSpillDouble) { // We estimate the size of result row like {double, int64} at 50B. Set the memory threshold to // 64B so that exactly one row fits in memory. - const int expectedRowsToFitInMemory = 1; auto defaultInternalQuerySBEAggApproxMemoryUseInBytesBeforeSpill = internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.load(); internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.store(64); @@ -572,17 +601,21 @@ TEST_F(HashAggStageTest, HashAggBasicCountSpillDouble) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), makeSV(), // Seek slot true, boost::none, true /* allowDiskUse */, + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -605,7 +638,14 @@ TEST_F(HashAggStageTest, HashAggBasicCountSpillDouble) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - ASSERT_EQ(results.size() - expectedRowsToFitInMemory, stats->spilledRecords); + // Memory usage is estimated only every two rows at the most frequent. Also, we only start + // spilling after estimating that the memory budget is exceeded. These two factors result in + // fewer expected spills than there are input records, even though only one record fits in + // memory at a time. + ASSERT_EQ(stats->numSpills, 3); + // The input has one run of two consecutive values, so we expect to spill as many records as + // there are input values minus one. + ASSERT_EQ(stats->spilledRecords, 8); stage->close(); } @@ -627,17 +667,21 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpillWithNoGroupByDouble) { // Build a HashAggStage, with an empty group by slot and compute a simple count. auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), makeSV(), // Seek slot true, boost::none, true /* allowDiskUse */, + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -658,6 +702,7 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpillWithNoGroupByDouble) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_FALSE(stats->usedDisk); + ASSERT_EQ(0, stats->numSpills); ASSERT_EQ(0, stats->spilledRecords); stage->close(); @@ -666,7 +711,6 @@ TEST_F(HashAggStageTest, HashAggBasicCountNoSpillWithNoGroupByDouble) { TEST_F(HashAggStageTest, HashAggMultipleAccSpill) { // We estimate the size of result row like {double, int64} at 59B. Set the memory threshold to // 128B so that two rows fit in memory. - const int expectedRowsToFitInMemory = 2; auto defaultInternalQuerySBEAggApproxMemoryUseInBytesBeforeSpill = internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.load(); internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.store(128); @@ -685,19 +729,27 @@ TEST_F(HashAggStageTest, HashAggMultipleAccSpill) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); auto sumsSlot = generateSlotId(); + auto spillSlot1 = generateSlotId(); + auto spillSlot2 = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1))), - sumsSlot, - stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1))), + sumsSlot, + stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), makeSV(), // Seek slot true, boost::none, true /* allowDiskUse */, + makeSlotExprPairVec( + spillSlot1, + stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot1)), + spillSlot2, + stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot2))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -727,7 +779,10 @@ TEST_F(HashAggStageTest, HashAggMultipleAccSpill) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - ASSERT_EQ(results.size() - expectedRowsToFitInMemory, stats->spilledRecords); + ASSERT_EQ(stats->numSpills, 3); + // The input has one run of two consecutive values, so we expect to spill as many records as + // there are input values minus one. + ASSERT_EQ(stats->spilledRecords, 8); stage->close(); } @@ -752,19 +807,27 @@ TEST_F(HashAggStageTest, HashAggMultipleAccSpillAllToDisk) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); auto sumsSlot = generateSlotId(); + auto spillSlot1 = generateSlotId(); + auto spillSlot2 = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1))), - sumsSlot, - stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1))), + sumsSlot, + stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), makeSV(), // Seek slot true, boost::none, true, // allowDiskUse=true + makeSlotExprPairVec( + spillSlot1, + stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot1)), + spillSlot2, + stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot2))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -794,7 +857,9 @@ TEST_F(HashAggStageTest, HashAggMultipleAccSpillAllToDisk) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - ASSERT_EQ(results.size(), stats->spilledRecords); + // We expect each incoming value to result in a spill of a single record. + ASSERT_EQ(stats->numSpills, 9); + ASSERT_EQ(stats->spilledRecords, 9); stage->close(); } @@ -831,14 +896,18 @@ TEST_F(HashAggStageTest, HashAggSum10Groups) { // Build a HashAggStage, group by the scanSlot and compute a sum for each group. auto sumsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(sumsSlot, stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), + makeSlotExprPairVec(sumsSlot, + stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), makeSV(), // Seek slot true, boost::none, true, // allowDiskUse=true + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. @@ -872,17 +941,21 @@ TEST_F(HashAggStageTest, HashAggBasicCountWithRecordIds) { // Build a HashAggStage, group by the scanSlot and compute a simple count. auto countsSlot = generateSlotId(); + auto spillSlot = generateSlotId(); auto stage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), makeSV(), // Seek slot true, boost::none, true, // allowDiskUse=true + makeSlotExprPairVec( + spillSlot, stage_builder::makeFunction("sum", stage_builder::makeVariable(spillSlot))), kEmptyPlanNodeId); // Prepare the tree and get the 'SlotAccessor' for the output slot. diff --git a/src/mongo/db/exec/sbe/sbe_plan_size_test.cpp b/src/mongo/db/exec/sbe/sbe_plan_size_test.cpp index 191f0ffe562..f62b8a6229c 100644 --- a/src/mongo/db/exec/sbe/sbe_plan_size_test.cpp +++ b/src/mongo/db/exec/sbe/sbe_plan_size_test.cpp @@ -139,11 +139,12 @@ TEST_F(PlanSizeTest, Filter) { TEST_F(PlanSizeTest, HashAgg) { auto stage = makeS<HashAggStage>(mockS(), mockSV(), - makeEM(generateSlotId(), mockE()), + makeSlotExprPairVec(generateSlotId(), mockE()), makeSV(), true, generateSlotId(), false, + makeSlotExprPairVec(), kEmptyPlanNodeId); assertPlanSize(*stage); } @@ -168,6 +169,7 @@ TEST_F(PlanSizeTest, IndexScan) { generateSlotId(), generateSlotId(), generateSlotId(), + generateSlotId(), IndexKeysInclusionSet(1), mockSV(), generateSlotId(), diff --git a/src/mongo/db/exec/sbe/sbe_trial_run_tracker_test.cpp b/src/mongo/db/exec/sbe/sbe_trial_run_tracker_test.cpp index 54fadbe1f15..57d3ff4e05f 100644 --- a/src/mongo/db/exec/sbe/sbe_trial_run_tracker_test.cpp +++ b/src/mongo/db/exec/sbe/sbe_trial_run_tracker_test.cpp @@ -141,14 +141,16 @@ TEST_F(TrialRunTrackerTest, TrialEndsDuringOpenPhaseOfBlockingStage) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), - makeSV(), // Seek slot + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSV(), /* Seek slot */ true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec(), /* mergingExprs */ kEmptyPlanNodeId); auto tracker = std::make_unique<TrialRunTracker>(numResultsLimit, size_t{0}); @@ -210,14 +212,16 @@ TEST_F(TrialRunTrackerTest, OnlyDeepestNestedBlockingStageHasTrialRunTracker) { auto hashAggStage = makeS<HashAggStage>( std::move(unionStage), makeSV(unionSlot), - makeEM(countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), - makeSV(), // Seek slot + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction( + "sum", + makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeSV(), /* Seek slot */ true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec(), /* mergingExprs */ kEmptyPlanNodeId); hashAggStage->prepare(*ctx); @@ -277,14 +281,16 @@ TEST_F(TrialRunTrackerTest, SiblingBlockingStagesBothGetTrialRunTracker) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), - makeSV(), // Seek slot + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction("sum", + makeE<EConstant>(value::TypeTags::NumberInt64, + value::bitcastFrom<int64_t>(1)))), + makeSV(), /* Seek slot */ true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec(), /* mergingExprs */ kEmptyPlanNodeId); return std::make_pair(countsSlot, std::move(hashAggStage)); @@ -407,14 +413,16 @@ TEST_F(TrialRunTrackerTest, DisablingTrackingForAChildStagePreventsEarlyExit) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeEM(countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), - makeSV(), // Seek slot + makeSlotExprPairVec( + countsSlot, + stage_builder::makeFunction("sum", + makeE<EConstant>(value::TypeTags::NumberInt64, + value::bitcastFrom<int64_t>(1)))), + makeSV(), /* Seek slot */ true, boost::none, false /* allowDiskUse */, + makeSlotExprPairVec(), /* mergingExprs */ kEmptyPlanNodeId); return std::make_pair(countsSlot, std::move(hashAggStage)); diff --git a/src/mongo/db/exec/sbe/size_estimator.h b/src/mongo/db/exec/sbe/size_estimator.h index fb6684eea52..bbb92328331 100644 --- a/src/mongo/db/exec/sbe/size_estimator.h +++ b/src/mongo/db/exec/sbe/size_estimator.h @@ -92,6 +92,11 @@ inline size_t estimate(const S& stats) { return stats.estimateObjectSizeInBytes() - sizeof(S); } +template <typename A, typename B> +inline size_t estimate(const std::pair<A, B>& pair) { + return estimate(pair.first) + estimate(pair.second); +} + // Calculate the size of the inlined vector's elements. template <typename T, size_t N, typename A> size_t estimate(const absl::InlinedVector<T, N, A>& vector) { diff --git a/src/mongo/db/exec/sbe/stages/branch.cpp b/src/mongo/db/exec/sbe/stages/branch.cpp index bec12b12ee2..1e8809f0858 100644 --- a/src/mongo/db/exec/sbe/stages/branch.cpp +++ b/src/mongo/db/exec/sbe/stages/branch.cpp @@ -70,31 +70,29 @@ void BranchStage::prepare(CompileCtx& ctx) { _children[0]->prepare(ctx); _children[1]->prepare(ctx); + // All of the slots listed in '_outputVals' must be unique. for (size_t idx = 0; idx < _outputVals.size(); ++idx) { - std::vector<value::SlotAccessor*> accessors; - accessors.reserve(2); + auto slot = _outputVals[idx]; + auto [_, inserted] = dupCheck.insert(slot); + uassert(4822831, str::stream() << "duplicate field: " << slot, inserted); + } - { - auto slot = _inputThenVals[idx]; - auto [it, inserted] = dupCheck.insert(slot); - uassert(4822829, str::stream() << "duplicate field: " << slot, inserted); + for (size_t idx = 0; idx < _outputVals.size(); ++idx) { + auto thenSlot = _inputThenVals[idx]; + auto elseSlot = _inputElseVals[idx]; - accessors.emplace_back(_children[0]->getAccessor(ctx, slot)); - } - { - auto slot = _inputElseVals[idx]; - auto [it, inserted] = dupCheck.insert(slot); - uassert(4822830, str::stream() << "duplicate field: " << slot, inserted); + // Slots listed in '_inputThenVals' and '_inputElseVals' may not appear in '_outputVals'. + bool thenSlotFound = dupCheck.count(thenSlot); + bool elseSlotFound = dupCheck.count(elseSlot); + uassert(4822829, str::stream() << "duplicate field: " << thenSlot, !thenSlotFound); + uassert(4822830, str::stream() << "duplicate field: " << elseSlot, !elseSlotFound); - accessors.emplace_back(_children[1]->getAccessor(ctx, slot)); - } - { - auto slot = _outputVals[idx]; - auto [it, inserted] = dupCheck.insert(slot); - uassert(4822831, str::stream() << "duplicate field: " << slot, inserted); + std::vector<value::SlotAccessor*> accessors; + accessors.reserve(2); + accessors.emplace_back(_children[0]->getAccessor(ctx, thenSlot)); + accessors.emplace_back(_children[1]->getAccessor(ctx, elseSlot)); - _outValueAccessors.emplace_back(value::SwitchAccessor{std::move(accessors)}); - } + _outValueAccessors.emplace_back(value::SwitchAccessor{std::move(accessors)}); } // compile filter diff --git a/src/mongo/db/exec/sbe/stages/collection_helpers.h b/src/mongo/db/exec/sbe/stages/collection_helpers.h index 4116f2980ff..7471763a44a 100644 --- a/src/mongo/db/exec/sbe/stages/collection_helpers.h +++ b/src/mongo/db/exec/sbe/stages/collection_helpers.h @@ -40,6 +40,7 @@ namespace mongo::sbe { * A callback which gets called whenever a SCAN stage asks an underlying index scan for a result. */ using IndexKeyConsistencyCheckCallback = std::function<bool(OperationContext* opCtx, + StringMap<const IndexCatalogEntry*>&, value::SlotAccessor* snapshotIdAccessor, value::SlotAccessor* indexIdAccessor, value::SlotAccessor* indexKeyAccessor, diff --git a/src/mongo/db/exec/sbe/stages/hash_agg.cpp b/src/mongo/db/exec/sbe/stages/hash_agg.cpp index 14f40afeaa9..bdbcda14805 100644 --- a/src/mongo/db/exec/sbe/stages/hash_agg.cpp +++ b/src/mongo/db/exec/sbe/stages/hash_agg.cpp @@ -27,47 +27,67 @@ * it in the license file. */ -#include "mongo/platform/basic.h" +#include "mongo/db/exec/sbe/stages/hash_agg.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/write_conflict_exception.h" -#include "mongo/db/exec/sbe/stages/hash_agg.h" +#include "mongo/db/exec/sbe/size_estimator.h" #include "mongo/db/exec/sbe/util/spilling.h" +#include "mongo/db/stats/resource_consumption_metrics.h" #include "mongo/db/storage/kv/kv_engine.h" #include "mongo/db/storage/storage_engine.h" #include "mongo/util/str.h" -#include "mongo/db/exec/sbe/size_estimator.h" - namespace mongo { namespace sbe { HashAggStage::HashAggStage(std::unique_ptr<PlanStage> input, value::SlotVector gbs, - value::SlotMap<std::unique_ptr<EExpression>> aggs, + SlotExprPairVector aggs, value::SlotVector seekKeysSlots, bool optimizedClose, boost::optional<value::SlotId> collatorSlot, bool allowDiskUse, - PlanNodeId planNodeId) + SlotExprPairVector mergingExprs, + PlanNodeId planNodeId, + bool forceIncreasedSpilling) : PlanStage("group"_sd, planNodeId), _gbs(std::move(gbs)), _aggs(std::move(aggs)), _collatorSlot(collatorSlot), _allowDiskUse(allowDiskUse), _seekKeysSlots(std::move(seekKeysSlots)), - _optimizedClose(optimizedClose) { + _optimizedClose(optimizedClose), + _mergingExprs(std::move(mergingExprs)), + _forceIncreasedSpilling(forceIncreasedSpilling) { _children.emplace_back(std::move(input)); invariant(_seekKeysSlots.empty() || _seekKeysSlots.size() == _gbs.size()); tassert(5843100, "HashAgg stage was given optimizedClose=false and seek keys", _seekKeysSlots.empty() || _optimizedClose); + + if (_allowDiskUse) { + tassert(7039549, + "disk use enabled for HashAggStage but incorrect number of merging expresssions", + _aggs.size() == _mergingExprs.size()); + } + + if (_forceIncreasedSpilling) { + tassert(7039554, "'forceIncreasedSpilling' set but disk use not allowed", _allowDiskUse); + } } std::unique_ptr<PlanStage> HashAggStage::clone() const { - value::SlotMap<std::unique_ptr<EExpression>> aggs; + SlotExprPairVector aggs; + aggs.reserve(_aggs.size()); for (auto& [k, v] : _aggs) { - aggs.emplace(k, v->clone()); + aggs.push_back({k, v->clone()}); + } + + SlotExprPairVector mergingExprsClone; + mergingExprsClone.reserve(_mergingExprs.size()); + for (auto&& [k, v] : _mergingExprs) { + mergingExprsClone.push_back({k, v->clone()}); } + return std::make_unique<HashAggStage>(_children[0]->clone(), _gbs, std::move(aggs), @@ -75,7 +95,9 @@ std::unique_ptr<PlanStage> HashAggStage::clone() const { _optimizedClose, _collatorSlot, _allowDiskUse, - _commonStats.nodeId); + std::move(mergingExprsClone), + _commonStats.nodeId, + _forceIncreasedSpilling); } void HashAggStage::doSaveState(bool relinquishCursor) { @@ -120,34 +142,36 @@ void HashAggStage::prepare(CompileCtx& ctx) { } value::SlotSet dupCheck; + auto throwIfDupSlot = [&dupCheck](value::SlotId slot) { + auto [_, inserted] = dupCheck.emplace(slot); + tassert(7039551, "duplicate slot id", inserted); + }; + size_t counter = 0; // Process group by columns. for (auto& slot : _gbs) { - auto [it, inserted] = dupCheck.emplace(slot); - uassert(4822827, str::stream() << "duplicate field: " << slot, inserted); + throwIfDupSlot(slot); _inKeyAccessors.emplace_back(_children[0]->getAccessor(ctx, slot)); - // Construct accessors for the key to be processed from either the '_ht' or the - // '_recordStore'. Before the memory limit is reached the '_outHashKeyAccessors' will carry - // the group-by keys, otherwise the '_outRecordStoreKeyAccessors' will carry the group-by - // keys. + // Construct accessors for obtaining the key values from either the hash table '_ht' or the + // '_recordStore'. _outHashKeyAccessors.emplace_back(std::make_unique<HashKeyAccessor>(_htIt, counter)); _outRecordStoreKeyAccessors.emplace_back( - std::make_unique<value::MaterializedSingleRowAccessor>(_aggKeyRecordStore, counter)); - - counter++; + std::make_unique<value::MaterializedSingleRowAccessor>(_outKeyRowRecordStore, counter)); - // A SwitchAccessor is used to point the '_outKeyAccessors' to the key coming from the '_ht' - // or the '_recordStore' when draining the HashAgg stage in getNext. The group-by key will - // either be in the '_ht' or the '_recordStore' if the key lives in memory, or if the key - // has been spilled to disk, respectively. The SwitchAccessor allows toggling between the - // two so the parent stage can read it through the '_outAccessors'. + // A 'SwitchAccessor' is used to point the '_outKeyAccessors' to the key coming from the + // '_ht' or the '_recordStore' when draining the HashAgg stage in getNext(). If no spilling + // occurred, the keys will be obtained from the hash table. If spilling kicked in, then all + // of the data is written out to the record store, so the 'SwitchAccessor' is reconfigured + // to obtain all of the keys from the spill table. _outKeyAccessors.emplace_back( std::make_unique<value::SwitchAccessor>(std::vector<value::SlotAccessor*>{ _outHashKeyAccessors.back().get(), _outRecordStoreKeyAccessors.back().get()})); _outAccessors[slot] = _outKeyAccessors.back().get(); + + ++counter; } // Process seek keys (if any). The keys must come from outside of the subtree (by definition) so @@ -158,26 +182,18 @@ void HashAggStage::prepare(CompileCtx& ctx) { counter = 0; for (auto& [slot, expr] : _aggs) { - auto [it, inserted] = dupCheck.emplace(slot); - // Some compilers do not allow to capture local bindings by lambda functions (the one - // is used implicitly in uassert below), so we need a local variable to construct an - // error message. - const auto slotId = slot; - uassert(4822828, str::stream() << "duplicate field: " << slotId, inserted); - - // Construct accessors for the agg state to be processed from either the '_ht' or the - // '_recordStore' by the SwitchAccessor owned by '_outAggAccessors' below. + throwIfDupSlot(slot); + + // Just like with the output accessors for the keys, we construct output accessors for the + // aggregate values that read from either the hash table '_ht' or the '_recordStore'. _outRecordStoreAggAccessors.emplace_back( - std::make_unique<value::MaterializedSingleRowAccessor>(_aggValueRecordStore, counter)); + std::make_unique<value::MaterializedSingleRowAccessor>(_outAggRowRecordStore, counter)); _outHashAggAccessors.emplace_back(std::make_unique<HashAggAccessor>(_htIt, counter)); - counter++; - - // A SwitchAccessor is used to toggle the '_outAggAccessors' between the '_ht' and the - // '_recordStore' when updating the agg state via the bytecode. By compiling the agg - // EExpressions with a SwitchAccessor we can load the agg value into the memory of - // '_aggValueRecordStore' if the value comes from the '_recordStore' or we can use the - // agg value referenced through '_htIt' and run the bytecode to mutate the value through the - // SwitchAccessor. + + // A 'SwitchAccessor' is used to toggle the '_outAggAccessors' between the '_ht' and the + // '_recordStore'. Just like the key values, the aggregate values are always obtained from + // the hash table if no spilling occurred and are always obtained from the record store if + // spilling occurred. _outAggAccessors.emplace_back( std::make_unique<value::SwitchAccessor>(std::vector<value::SlotAccessor*>{ _outHashAggAccessors.back().get(), _outRecordStoreAggAccessors.back().get()})); @@ -187,10 +203,32 @@ void HashAggStage::prepare(CompileCtx& ctx) { ctx.root = this; ctx.aggExpression = true; ctx.accumulator = _outAggAccessors.back().get(); - _aggCodes.emplace_back(expr->compile(ctx)); ctx.aggExpression = false; + + ++counter; + } + + // If disk use is allowed, then we need to compile the merging expressions as well. + if (_allowDiskUse) { + counter = 0; + for (auto&& [spillSlot, mergingExpr] : _mergingExprs) { + throwIfDupSlot(spillSlot); + + _spilledAggsAccessors.push_back( + std::make_unique<value::MaterializedSingleRowAccessor>(_spilledAggRow, counter)); + _spilledAggsAccessorMap[spillSlot] = _spilledAggsAccessors.back().get(); + + ctx.root = this; + ctx.aggExpression = true; + ctx.accumulator = _outAggAccessors[counter].get(); + _mergingExprCodes.emplace_back(mergingExpr->compile(ctx)); + ctx.aggExpression = false; + + ++counter; + } } + _compiled = true; } @@ -200,6 +238,15 @@ value::SlotAccessor* HashAggStage::getAccessor(CompileCtx& ctx, value::SlotId sl return it->second; } } else { + // The slots into which we read spilled partial aggregates, accessible via + // '_spilledAggsAccessors', should only be visible to this stage. They are used internally + // when merging spilled partial aggregates and should never be read by ancestor stages. + // Therefore, they are only made visible when this stage is in the process of compiling + // itself. + if (auto it = _spilledAggsAccessorMap.find(slot); it != _spilledAggsAccessorMap.end()) { + return it->second; + } + return _children[0]->getAccessor(ctx, slot); } @@ -225,89 +272,91 @@ void HashAggStage::spillRowToDisk(const value::MaterializedRow& key, const value::MaterializedRow& val) { KeyString::Builder kb{KeyString::Version::kLatestVersion}; key.serializeIntoKeyString(kb); + // Add a unique integer to the end of the key, since record ids must be unique. We want equal + // keys to be adjacent in the 'RecordStore' so that we can merge the partial aggregates with a + // single pass. + kb.appendNumberLong(_ridCounter++); auto typeBits = kb.getTypeBits(); - auto rid = RecordId(kb.getBuffer(), kb.getSize()); - boost::optional<value::MaterializedRow> valFromRs = - readFromRecordStore(_opCtx, _recordStore->rs(), rid); - tassert(6031100, "Spilling a row doesn't support updating it in the store.", !valFromRs); - spillValueToDisk(rid, val, typeBits, false /*update*/); + upsertToRecordStore(_opCtx, _recordStore->rs(), rid, val, typeBits, false /*update*/); + _specificStats.spilledRecords++; } -void HashAggStage::spillValueToDisk(const RecordId& key, - const value::MaterializedRow& val, - const KeyString::TypeBits& typeBits, - bool update) { - auto nBytes = upsertToRecordStore(_opCtx, _recordStore->rs(), key, val, typeBits, update); - if (!update) { - _specificStats.spilledRecords++; +void HashAggStage::spill(MemoryCheckData& mcd) { + uassert(ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed, + "Exceeded memory limit for $group, but didn't allow external spilling;" + " pass allowDiskUse:true to opt in", + _allowDiskUse); + + // Since we flush the entire hash table to disk, we also clear any state related to estimating + // memory consumption. + mcd.reset(); + + if (!_recordStore) { + makeTemporaryRecordStore(); } - _specificStats.lastSpilledRecordSize = nBytes; + + for (auto&& it : *_ht) { + spillRowToDisk(it.first, it.second); + } + + auto& metricsCollector = ResourceConsumption::MetricsCollector::get(_opCtx); + // We're not actually doing any sorting here or using the 'Sorter' class, but for the purposes + // of $operationMetrics we incorporate the number of spilled records into the "keysSorted" + // metric. Similarly, "sorterSpills" despite the name counts the number of individual spill + // events. + metricsCollector.incrementKeysSorted(_ht->size()); + metricsCollector.incrementSorterSpills(1); + + _ht->clear(); + + ++_specificStats.numSpills; } // Checks memory usage. Ideally, we'd want to know the exact size of already accumulated data, but // we cannot, so we estimate it based on the last updated/inserted row, if we have one, or the first // row in the '_ht' table. If the estimated memory usage exceeds the allowed, this method initiates -// spilling (if haven't been done yet) and evicts some records from the '_ht' table into the temp -// store to keep the memory usage under the limit. +// spilling. void HashAggStage::checkMemoryUsageAndSpillIfNecessary(MemoryCheckData& mcd) { - // The '_ht' table might become empty in the degenerate case when all rows had to be evicted to - // meet the memory constraint during a previous check -- we don't need to keep checking memory - // usage in this case because the table will never get new rows. - if (_ht->empty()) { - return; - } + invariant(!_ht->empty()); // If the group-by key is empty we will only ever aggregate into a single row so no sense in - // spilling since we will just be moving a single row back and forth from disk to main memory. + // spilling. if (_inKeyAccessors.size() == 0) { return; } mcd.memoryCheckpointCounter++; - if (mcd.memoryCheckpointCounter >= mcd.nextMemoryCheckpoint) { - if (_htIt == _ht->end()) { - _htIt = _ht->begin(); - } - const long estimatedRowSize = - _htIt->first.memUsageForSorter() + _htIt->second.memUsageForSorter(); - long long estimatedTotalSize = _ht->size() * estimatedRowSize; + if (mcd.memoryCheckpointCounter < mcd.nextMemoryCheckpoint) { + // We haven't reached the next checkpoint at which we estimate memory usage and decide if we + // should spill. + return; + } + + const long estimatedRowSize = + _htIt->first.memUsageForSorter() + _htIt->second.memUsageForSorter(); + long long estimatedTotalSize = _ht->size() * estimatedRowSize; + + if (estimatedTotalSize >= _approxMemoryUseInBytesBeforeSpill) { + spill(mcd); + } else { + // Calculate the next memory checkpoint. We estimate it based on the prior growth of the + // '_ht' and the remaining available memory. If 'estimatedGainPerChildAdvance' suggests that + // the hash table is growing, then the checkpoint is estimated as some configurable + // percentage of the number of additional input rows that we would have to process to + // consume the remaining memory. On the other hand, a value of 'estimtedGainPerChildAdvance' + // close to zero indicates a stable hash stable size, in which case we can delay the next + // check progressively. const double estimatedGainPerChildAdvance = (static_cast<double>(estimatedTotalSize - mcd.lastEstimatedMemoryUsage) / mcd.memoryCheckpointCounter); - if (estimatedTotalSize >= _approxMemoryUseInBytesBeforeSpill) { - uassert(ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed, - "Exceeded memory limit for $group, but didn't allow external spilling." - " Pass allowDiskUse:true to opt in.", - _allowDiskUse); - if (!_recordStore) { - makeTemporaryRecordStore(); - } - - // Evict enough rows into the temporary store to drop below the memory constraint. - const long rowsToEvictCount = - 1 + (estimatedTotalSize - _approxMemoryUseInBytesBeforeSpill) / estimatedRowSize; - for (long i = 0; !_ht->empty() && i < rowsToEvictCount; i++) { - spillRowToDisk(_htIt->first, _htIt->second); - _ht->erase(_htIt); - _htIt = _ht->begin(); - } - estimatedTotalSize = _ht->size() * estimatedRowSize; - } - - // Calculate the next memory checkpoint. We estimate it based on the prior growth of the - // '_ht' and the remaining available memory. We have to keep doing this even after starting - // to spill because some accumulators can grow in size inside '_ht' (with no bounds). - // Value of 'estimatedGainPerChildAdvance' can be negative if the previous checkpoint - // evicted any records. And a value close to zero indicates a stable size of '_ht' so can - // delay the next check progressively. const long nextCheckpointCandidate = (estimatedGainPerChildAdvance > 0.1) ? mcd.checkpointMargin * (_approxMemoryUseInBytesBeforeSpill - estimatedTotalSize) / estimatedGainPerChildAdvance - : (estimatedGainPerChildAdvance < -0.1) ? mcd.atMostCheckFrequency - : mcd.nextMemoryCheckpoint * 2; + : mcd.nextMemoryCheckpoint * 2; + mcd.nextMemoryCheckpoint = std::min<long>(mcd.memoryCheckFrequency, std::max<long>(mcd.atMostCheckFrequency, nextCheckpointCandidate)); @@ -333,17 +382,28 @@ void HashAggStage::open(bool reOpen) { 5402503, "collatorSlot must be of collator type", tag == value::TypeTags::collator); auto collatorView = value::getCollatorView(collatorVal); const value::MaterializedRowHasher hasher(collatorView); - const value::MaterializedRowEq equator(collatorView); - _ht.emplace(0, hasher, equator); + _keyEq = value::MaterializedRowEq(collatorView); + _ht.emplace(0, hasher, _keyEq); } else { _ht.emplace(); } _seekKeys.resize(_seekKeysAccessors.size()); - // A default value for spilling a key to the record store. - value::MaterializedRow defaultVal{_outAggAccessors.size()}; - bool updateAggStateHt = false; + // Reset state since this stage may have been previously opened. + for (auto&& accessor : _outKeyAccessors) { + accessor->setIndex(0); + } + for (auto&& accessor : _outAggAccessors) { + accessor->setIndex(0); + } + _rsCursor.reset(); + _recordStore.reset(); + _outKeyRowRecordStore = {0}; + _outAggRowRecordStore = {0}; + _spilledAggRow = {0}; + _stashedNextRow = {0, 0}; + MemoryCheckData memoryCheckData; while (_children[0]->getNext() == PlanState::ADVANCED) { @@ -355,56 +415,34 @@ void HashAggStage::open(bool reOpen) { key.reset(idx++, false, tag, val); } - - if (_htIt = _ht->find(key); !_recordStore && _htIt == _ht->end()) { - // The memory limit hasn't been reached yet, insert a new key in '_ht' by copying - // the key. Note as a future optimization, we should avoid the lookup in the find() - // call and the emplace. + bool newKey = false; + _htIt = _ht->find(key); + if (_htIt == _ht->end()) { + // The key is not present in the hash table yet, so we insert it and initialize the + // corresponding accumulator. Note that as a future optimization, we could avoid + // doing a lookup both in the 'find()' call and in 'emplace()'. + newKey = true; key.makeOwned(); auto [it, _] = _ht->emplace(std::move(key), value::MaterializedRow{0}); - // Initialize accumulators. it->second.resize(_outAggAccessors.size()); _htIt = it; } - updateAggStateHt = _htIt != _ht->end(); - - if (updateAggStateHt) { - // Accumulate state in '_ht' by pointing the '_outAggAccessors' the - // '_outHashAggAccessors'. - for (size_t idx = 0; idx < _outAggAccessors.size(); ++idx) { - _outAggAccessors[idx]->setIndex(0); - auto [owned, tag, val] = _bytecode.run(_aggCodes[idx].get()); - _outHashAggAccessors[idx]->reset(owned, tag, val); - } - } else { - // The memory limit has been reached and the key wasn't in the '_ht' so we need - // to spill it to the '_recordStore'. - KeyString::Builder kb{KeyString::Version::kLatestVersion}; - // 'key' is moved only when 'updateAggStateHt' ends up "true", so it's safe to - // ignore the warning. - key.serializeIntoKeyString(kb); // NOLINT(bugprone-use-after-move) - auto typeBits = kb.getTypeBits(); - - auto rid = RecordId(kb.getBuffer(), kb.getSize()); - - boost::optional<value::MaterializedRow> valFromRs = - readFromRecordStore(_opCtx, _recordStore->rs(), rid); - if (!valFromRs) { - _aggValueRecordStore = defaultVal; - } else { - _aggValueRecordStore = *valFromRs; - } - - for (size_t idx = 0; idx < _outAggAccessors.size(); ++idx) { - _outAggAccessors[idx]->setIndex(1); - auto [owned, tag, val] = _bytecode.run(_aggCodes[idx].get()); - _aggValueRecordStore.reset(idx, owned, tag, val); - } - spillValueToDisk(rid, _aggValueRecordStore, typeBits, valFromRs ? true : false); + + // Accumulate state in '_ht'. + for (size_t idx = 0; idx < _outAggAccessors.size(); ++idx) { + auto [owned, tag, val] = _bytecode.run(_aggCodes[idx].get()); + _outHashAggAccessors[idx]->reset(owned, tag, val); } - // Estimates how much memory is being used and might start spilling. - checkMemoryUsageAndSpillIfNecessary(memoryCheckData); + if (_forceIncreasedSpilling && !newKey) { + // If configured to spill more than usual, we spill after seeing the same key twice. + spill(memoryCheckData); + } else { + // Estimates how much memory is being used. If we estimate that the hash table + // exceeds the allotted memory budget, its contents are spilled to the + // '_recordStore' and '_ht' is cleared. + checkMemoryUsageAndSpillIfNecessary(memoryCheckData); + } if (_tracker && _tracker->trackProgress<TrialRunTracker::kNumResults>(1)) { // During trial runs, we want to limit the amount of work done by opening a blocking @@ -421,8 +459,33 @@ void HashAggStage::open(bool reOpen) { _children[0]->close(); _childOpened = false; } - } + // If we spilled at any point while consuming the input, then do one final spill to write + // any leftover contents of '_ht' to the record store. That way, when recovering the input + // from the record store and merging partial aggregates we don't have to worry about the + // possibility of some of the data being in the hash table and some being in the record + // store. + if (_recordStore) { + if (!_ht->empty()) { + spill(memoryCheckData); + } + + _specificStats.spilledDataStorageSize = _recordStore->rs()->storageSize(_opCtx); + + // Establish a cursor, positioned at the beginning of the record store. + _rsCursor = _recordStore->rs()->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 + // under the hood. + for (auto&& accessor : _outKeyAccessors) { + accessor->setIndex(1); + } + for (auto&& accessor : _outAggAccessors) { + accessor->setIndex(1); + } + } + } if (!_seekKeysAccessors.empty()) { // Copy keys in order to do the lookup. @@ -434,20 +497,76 @@ void HashAggStage::open(bool reOpen) { } _htIt = _ht->end(); +} + +HashAggStage::SpilledRow HashAggStage::deserializeSpilledRecord(const Record& record, + BufBuilder& keyBuffer) { + // Read the values and type bits out of the value part of the record. + BufReader valReader(record.data.data(), record.data.size()); + auto val = value::MaterializedRow::deserializeForSorter(valReader, {}); + auto typeBits = KeyString::TypeBits::fromBuffer(KeyString::Version::kLatestVersion, &valReader); + + keyBuffer.reset(); + auto key = value::MaterializedRow::deserializeFromKeyString( + decodeKeyString(record.id, typeBits), &keyBuffer, _gbs.size() /*numPrefixValuesToRead*/); + return {std::move(key), std::move(val)}; +} - // Set the SwitchAccessors to point to the '_ht' so we can drain it first before draining the - // '_recordStore' in getNext(). - for (size_t idx = 0; idx < _outAggAccessors.size(); ++idx) { - _outAggAccessors[idx]->setIndex(0); +PlanState HashAggStage::getNextSpilled() { + if (_stashedNextRow.first.isEmpty()) { + auto nextRecord = _rsCursor->next(); + if (!nextRecord) { + return trackPlanState(PlanState::IS_EOF); + } + + // We are just starting the process of merging the spilled file segments. + auto recoveredRow = deserializeSpilledRecord(*nextRecord, _outKeyRowRSBuffer); + + _outKeyRowRecordStore = std::move(recoveredRow.first); + _outAggRowRecordStore = std::move(recoveredRow.second); + } else { + // We peeked at the next key last time around. + _outKeyRowRSBuffer = std::move(_stashedKeyBuffer); + _outKeyRowRecordStore = std::move(_stashedNextRow.first); + _outAggRowRecordStore = std::move(_stashedNextRow.second); + // Clear the stashed row. + _stashedNextRow = {0, 0}; } - _drainingRecordStore = false; + + // Find additional partial aggregates for the same key and merge them in order to compute the + // final output. + for (auto nextRecord = _rsCursor->next(); nextRecord; nextRecord = _rsCursor->next()) { + auto recoveredRow = deserializeSpilledRecord(*nextRecord, _stashedKeyBuffer); + if (!_keyEq(recoveredRow.first, _outKeyRowRecordStore)) { + // The newly recovered spilled row belongs to a new key, so we're done merging partial + // aggregates for the old key. Save the new row for later and return advanced. + _stashedNextRow = std::move(recoveredRow); + return trackPlanState(PlanState::ADVANCED); + } + + // Merge in the new partial aggregate values. + _spilledAggRow = std::move(recoveredRow.second); + for (size_t idx = 0; idx < _mergingExprCodes.size(); ++idx) { + auto [owned, tag, val] = _bytecode.run(_mergingExprCodes[idx].get()); + _outRecordStoreAggAccessors[idx]->reset(owned, tag, val); + } + } + + return trackPlanState(PlanState::ADVANCED); } PlanState HashAggStage::getNext() { auto optTimer(getOptTimer(_opCtx)); - if (_htIt == _ht->end() && !_drainingRecordStore) { - // First invocation of getNext() after open() when not draining the '_recordStore'. + // If we've spilled, then we need to produce the output by merging the spilled segments from the + // spill file. + if (_recordStore) { + return getNextSpilled(); + } + + // We didn't spill. Obtain the next output row from the hash table. + if (_htIt == _ht->end()) { + // First invocation of getNext() after open(). if (!_seekKeysAccessors.empty()) { _htIt = _ht->find(_seekKeys); } else { @@ -456,53 +575,13 @@ PlanState HashAggStage::getNext() { } else if (!_seekKeysAccessors.empty()) { // Subsequent invocation with seek keys. Return only 1 single row (if any). _htIt = _ht->end(); - } else if (!_drainingRecordStore) { - // Returning the results of the entire hash table first before draining the '_recordStore'. + } else { ++_htIt; } - if (_htIt == _ht->end() && !_recordStore) { - // The hash table has been drained and nothing was spilled to disk. + if (_htIt == _ht->end()) { + // The hash table has been drained (and we never spilled to disk) so we're done. return trackPlanState(PlanState::IS_EOF); - } else if (_htIt != _ht->end()) { - // Drain the '_ht' on the next 'getNext()' call. - return trackPlanState(PlanState::ADVANCED); - } else if (_seekKeysAccessors.empty()) { - // A record store was created to spill to disk. Drain it then clean it up. - if (!_rsCursor) { - _rsCursor = _recordStore->rs()->getCursor(_opCtx); - } - auto nextRecord = _rsCursor->next(); - if (nextRecord) { - // Point the out accessors to the recordStore accessors to allow parent stages to read - // the agg state from the '_recordStore'. - if (!_drainingRecordStore) { - for (size_t i = 0; i < _outKeyAccessors.size(); ++i) { - _outKeyAccessors[i]->setIndex(1); - } - for (size_t i = 0; i < _outAggAccessors.size(); ++i) { - _outAggAccessors[i]->setIndex(1); - } - } - _drainingRecordStore = true; - - // Read the agg state value from the '_recordStore' and Reconstruct the key from the - // typeBits stored along side of the value. - BufReader valReader(nextRecord->data.data(), nextRecord->data.size()); - auto val = value::MaterializedRow::deserializeForSorter(valReader, {}); - auto typeBits = - KeyString::TypeBits::fromBuffer(KeyString::Version::kLatestVersion, &valReader); - _aggValueRecordStore = val; - - _aggKeyRSBuffer.reset(); - _aggKeyRecordStore = value::MaterializedRow::deserializeFromKeyString( - decodeKeyString(nextRecord->id, typeBits), &_aggKeyRSBuffer); - return trackPlanState(PlanState::ADVANCED); - } else { - _rsCursor.reset(); - _recordStore.reset(); - return trackPlanState(PlanState::IS_EOF); - } } else { return trackPlanState(PlanState::ADVANCED); } @@ -522,11 +601,19 @@ std::unique_ptr<PlanStageStats> HashAggStage::getStats(bool includeDebugInfo) co childrenBob.append(str::stream() << slot, printer.print(expr->debugPrint())); } } + + if (!_mergingExprs.empty()) { + BSONObjBuilder nestedBuilder{bob.subobjStart("mergingExprs")}; + for (auto&& [slot, expr] : _mergingExprs) { + nestedBuilder.append(str::stream() << slot, printer.print(expr->debugPrint())); + } + } + // Spilling stats. bob.appendBool("usedDisk", _specificStats.usedDisk); + bob.appendNumber("numSpills", _specificStats.numSpills); bob.appendNumber("spilledRecords", _specificStats.spilledRecords); - bob.appendNumber("spilledBytesApprox", - _specificStats.lastSpilledRecordSize * _specificStats.spilledRecords); + bob.appendNumber("spilledDataStorageSize", _specificStats.spilledDataStorageSize); ret->debugInfo = bob.obj(); } @@ -544,11 +631,12 @@ void HashAggStage::close() { trackClose(); _ht = boost::none; - if (_recordStore) { - // A record store was created to spill to disk. Clean it up. - _recordStore.reset(); - _drainingRecordStore = false; - } + _rsCursor.reset(); + _recordStore.reset(); + _outKeyRowRecordStore = {0}; + _outAggRowRecordStore = {0}; + _spilledAggRow = {0}; + _stashedNextRow = {0, 0}; if (_childOpened) { _children[0]->close(); @@ -571,7 +659,7 @@ std::vector<DebugPrinter::Block> HashAggStage::debugPrint() const { ret.emplace_back(DebugPrinter::Block("[`")); bool first = true; - value::orderedSlotMapTraverse(_aggs, [&](auto slot, auto&& expr) { + for (auto&& [slot, expr] : _aggs) { if (!first) { ret.emplace_back(DebugPrinter::Block("`,")); } @@ -580,7 +668,7 @@ std::vector<DebugPrinter::Block> HashAggStage::debugPrint() const { ret.emplace_back("="); DebugPrinter::addBlocks(ret, expr->debugPrint()); first = false; - }); + } ret.emplace_back("`]"); if (!_seekKeysSlots.empty()) { @@ -595,6 +683,28 @@ std::vector<DebugPrinter::Block> HashAggStage::debugPrint() const { ret.emplace_back("`]"); } + if (!_mergingExprs.empty()) { + ret.emplace_back("spillSlots[`"); + for (size_t idx = 0; idx < _mergingExprs.size(); ++idx) { + if (idx) { + ret.emplace_back("`,"); + } + + DebugPrinter::addIdentifier(ret, _mergingExprs[idx].first); + } + ret.emplace_back("`]"); + + ret.emplace_back("mergingExprs[`"); + for (size_t idx = 0; idx < _mergingExprs.size(); ++idx) { + if (idx) { + ret.emplace_back("`,"); + } + + DebugPrinter::addBlocks(ret, _mergingExprs[idx].second->debugPrint()); + } + ret.emplace_back("`]"); + } + if (!_optimizedClose) { ret.emplace_back("reopen"); } @@ -615,6 +725,7 @@ size_t HashAggStage::estimateCompileTimeSize() const { size += size_estimator::estimate(_gbs); size += size_estimator::estimate(_aggs); size += size_estimator::estimate(_seekKeysSlots); + size += size_estimator::estimate(_mergingExprs); return size; } diff --git a/src/mongo/db/exec/sbe/stages/hash_agg.h b/src/mongo/db/exec/sbe/stages/hash_agg.h index 19fbca9d1c7..001b29be887 100644 --- a/src/mongo/db/exec/sbe/stages/hash_agg.h +++ b/src/mongo/db/exec/sbe/stages/hash_agg.h @@ -29,8 +29,6 @@ #pragma once -#include <unordered_map> - #include "mongo/db/exec/sbe/expressions/expression.h" #include "mongo/db/exec/sbe/stages/stages.h" #include "mongo/db/exec/sbe/vm/vm.h" @@ -61,21 +59,38 @@ namespace sbe { * determining whether two group-by keys are equal. For instance, the plan may require us to do a * case-insensitive group on a string field. * + * The 'allowDiskUse' flag controls whether this stage can spill. If false and the memory budget is + * exhausted, this stage throws a query-fatal error with code + * 'QueryExceededMemoryLimitNoDiskUseAllowed'. If true, then spilling is possible and the caller + * must provide a vector of 'mergingExprs'. This is a vector of (slot, expression) pairs which is + * symmetrical with 'aggs'. The slots are only visible internally and are used to store partial + * aggregate values that have been recovered from the spill table. Each of the expressions is an agg + * function which merges the partial aggregate value from this slot into the final aggregate value. + * In the debug string output, the internal slots used to house the partial aggregates are printed + * as a list of "spillSlots" and the expressions are printed as a parallel list of "mergingExprs". + * + * If 'forcedIncreasedSpilling' is true, then this stage will spill frequently even if the memory + * limit is not reached. This is intended to be used in test contexts to exercise the otherwise + * infrequently used spilling logic. + * * Debug string representation: * - * group [<group by slots>] [slot_1 = expr_1, ..., slot_n = expr_n] [<seek slots>]? reopen? - * collatorSlot? childStage + * group [<group by slots>] [slot_1 = expr_1, ..., slot_n = expr_n] [<seek slots>]? + * spillSlots[slot_1, ..., slot_n] mergingExprs[expr_1, ..., expr_n] reopen? collatorSlot? + * childStage */ class HashAggStage final : public PlanStage { public: HashAggStage(std::unique_ptr<PlanStage> input, value::SlotVector gbs, - value::SlotMap<std::unique_ptr<EExpression>> aggs, + SlotExprPairVector aggs, value::SlotVector seekKeysSlots, bool optimizedClose, boost::optional<value::SlotId> collatorSlot, bool allowDiskUse, - PlanNodeId planNodeId); + SlotExprPairVector mergingExprs, + PlanNodeId planNodeId, + bool forceIncreasedSpilling = false); std::unique_ptr<PlanStage> clone() const final; @@ -108,99 +123,138 @@ private: using HashKeyAccessor = value::MaterializedRowKeyAccessor<TableType::iterator>; using HashAggAccessor = value::MaterializedRowValueAccessor<TableType::iterator>; - void makeTemporaryRecordStore(); - - /** - * Spills a key and value pair to the '_recordStore' where the semantics are insert or update - * depending on the 'update' flag. When the 'update' flag is true this method already expects - * the 'key' to be inserted into the '_recordStore', otherwise the 'key' and 'val' pair are - * fresh. - * - * This method expects the key to be seralized into a KeyString::Value so that the key is - * memcmp-able and lookups can be done to update the 'val' in the '_recordStore'. Note that the - * 'typeBits' are needed to reconstruct the spilled 'key' when calling 'getNext' to deserialize - * the 'key' to a MaterializedRow. Since the '_recordStore' only stores the memcmp-able part of - * the KeyString we need to carry the 'typeBits' separately, and we do this by appending the - * 'typeBits' to the end of the serialized 'val' buffer and store them at the leaves of the - * backing B-tree of the '_recordStore'. used as the RecordId. - */ - void spillValueToDisk(const RecordId& key, - const value::MaterializedRow& val, - const KeyString::TypeBits& typeBits, - bool update); - void spillRowToDisk(const value::MaterializedRow& key, - const value::MaterializedRow& defaultVal); + using SpilledRow = std::pair<value::MaterializedRow, value::MaterializedRow>; /** * We check amount of used memory every T processed incoming records, where T is calculated * based on the estimated used memory and its recent growth. When the memory limit is exceeded, - * 'checkMemoryUsageAndSpillIfNecessary()' will create '_recordStore' and might spill some of - * the already accumulated data into it. + * 'checkMemoryUsageAndSpillIfNecessary()' will create '_recordStore' (if it hasn't already been + * created) and spill the contents of the hash table into this record store. */ struct MemoryCheckData { + MemoryCheckData() { + reset(); + } + + void reset() { + memoryCheckFrequency = std::min(atMostCheckFrequency, atLeastMemoryCheckFrequency); + nextMemoryCheckpoint = 0; + memoryCheckpointCounter = 0; + lastEstimatedMemoryUsage = 0; + } + const double checkpointMargin = internalQuerySBEAggMemoryUseCheckMargin.load(); - const long atMostCheckFrequency = internalQuerySBEAggMemoryCheckPerAdvanceAtMost.load(); - const long atLeastMemoryCheckFrequency = + const int64_t atMostCheckFrequency = internalQuerySBEAggMemoryCheckPerAdvanceAtMost.load(); + const int64_t atLeastMemoryCheckFrequency = internalQuerySBEAggMemoryCheckPerAdvanceAtLeast.load(); // The check frequency upper bound, which start at 'atMost' and exponentially backs off // to 'atLeast' as more data is accumulated. If 'atLeast' is less than 'atMost', the memory // checks will be done every 'atLeast' incoming records. - long memoryCheckFrequency = 1; + int64_t memoryCheckFrequency = 1; // The number of incoming records to process before the next memory checkpoint. - long nextMemoryCheckpoint = 0; + int64_t nextMemoryCheckpoint = 0; // The counter of the incoming records between memory checkpoints. - long memoryCheckpointCounter = 0; + int64_t memoryCheckpointCounter = 0; - long long lastEstimatedMemoryUsage = 0; - - MemoryCheckData() { - memoryCheckFrequency = std::min(atMostCheckFrequency, atLeastMemoryCheckFrequency); - } + int64_t lastEstimatedMemoryUsage = 0; }; + + /** + * Inserts a key and value pair to the '_recordStore'. They key is serialized to a + * 'KeyString::Value' which becomes the 'RecordId'. This makes the keys memcmp-able and ensures + * that the record store ends up sorted by the group-by keys. + * + * Note that the 'typeBits' are needed to reconstruct the spilled 'key' to a 'MaterializedRow', + * but are not necessary for comparison purposes. Therefore, we carry the type bits separately + * from the record id, instead appending them to the end of the serialized 'val' buffer. + */ + void spillRowToDisk(const value::MaterializedRow& key, const value::MaterializedRow& val); + void checkMemoryUsageAndSpillIfNecessary(MemoryCheckData& mcd); + void spill(MemoryCheckData& mcd); + + /** + * Given a 'record' from the record store, decodes it into a pair of materialized rows (one for + * the group-by keys and another for the agg values). + * + * The given 'keyBuffer' is cleared, and then used to hold data (e.g. long strings and other + * values that can't be inlined) obtained by decoding the 'RecordId' keystring to a + * 'MaterializedRow'. The values in the resulting 'MaterializedRow' may be pointers into + * 'keyBuffer', so it is important that 'keyBuffer' outlive the row. + */ + SpilledRow deserializeSpilledRecord(const Record& record, BufBuilder& keyBuffer); + + PlanState getNextSpilled(); + + void makeTemporaryRecordStore(); const value::SlotVector _gbs; - const value::SlotMap<std::unique_ptr<EExpression>> _aggs; + const SlotExprPairVector _aggs; const boost::optional<value::SlotId> _collatorSlot; const bool _allowDiskUse; const value::SlotVector _seekKeysSlots; // When this operator does not expect to be reopened (almost always) then it can close the child // early. const bool _optimizedClose{true}; + + // Expressions used to merge partial aggregates that have been spilled to disk and their + // corresponding input slots. For example, imagine that this list contains a pair (s12, + // sum(s12)). This means that the partial aggregate values will be read into slot s12 after + // being recovered from the spill table and can be merged using the 'sum()' agg function. + // + // When disk use is allowed, this vector must have the same length as '_aggs'. + const SlotExprPairVector _mergingExprs; + + // When true, we spill frequently without reaching the memory limit. This allows us to exercise + // the spilling logic more often in test contexts. + const bool _forceIncreasedSpilling; + value::SlotAccessorMap _outAccessors; + + // Accessors used to obtain the values of the group by slots when reading the input from the + // child. std::vector<value::SlotAccessor*> _inKeyAccessors; - // Accessors for the key stored in '_ht', a SwitchAccessor is used so we can produce the key - // from either the '_ht' or the '_recordStore'. + // This buffer stores values for '_outKeyRowRecordStore'; values in the '_outKeyRowRecordStore' + // can be pointers that point to data in this buffer. + BufBuilder _outKeyRowRSBuffer; + // Accessors for the key slots provided as output by this stage. The keys can either come from + // the hash table or recovered from a temporary record store. We use a 'SwitchAccessor' to + // switch between these two cases. std::vector<std::unique_ptr<HashKeyAccessor>> _outHashKeyAccessors; + // Row of key values to output used when recovering spilled data from the record store. + value::MaterializedRow _outKeyRowRecordStore{0}; + std::vector<std::unique_ptr<value::MaterializedSingleRowAccessor>> _outRecordStoreKeyAccessors; std::vector<std::unique_ptr<value::SwitchAccessor>> _outKeyAccessors; - // Accessor for the agg state value stored in the '_recordStore' when data is spilled to disk. - value::MaterializedRow _aggKeyRecordStore{0}; - value::MaterializedRow _aggValueRecordStore{0}; - std::vector<std::unique_ptr<value::MaterializedSingleRowAccessor>> _outRecordStoreKeyAccessors; + // Accessors for the output aggregate results. The aggregates can either come from the hash + // table or can be computed after merging partial aggregates spilled to a record store. We use a + // 'SwitchAccessor' to switch between these two cases. + std::vector<std::unique_ptr<HashAggAccessor>> _outHashAggAccessors; + // Row of agg values to output used when recovering spilled data from the record store. + value::MaterializedRow _outAggRowRecordStore{0}; std::vector<std::unique_ptr<value::MaterializedSingleRowAccessor>> _outRecordStoreAggAccessors; - - // This buffer stores values for the spilled '_aggKeyRecordStore' that's loaded into memory from - // the '_recordStore'. Values in the '_aggKeyRecordStore' row are pointers that point to data in - // this buffer. - BufBuilder _aggKeyRSBuffer; + std::vector<std::unique_ptr<value::SwitchAccessor>> _outAggAccessors; std::vector<value::SlotAccessor*> _seekKeysAccessors; value::MaterializedRow _seekKeys; - // Accesors for the agg state in '_ht', a SwitchAccessor is used so we can produce the agg state - // from either the '_ht' or the '_recordStore' when draining the HashAgg stage. - std::vector<std::unique_ptr<value::SwitchAccessor>> _outAggAccessors; - std::vector<std::unique_ptr<HashAggAccessor>> _outHashAggAccessors; + // Bytecode which gets executed to aggregate incoming rows into the hash table. std::vector<std::unique_ptr<vm::CodeFragment>> _aggCodes; + // Bytecode for the merging expressions, executed if partial aggregates are spilled to a record + // store and need to be subsequently combined. + std::vector<std::unique_ptr<vm::CodeFragment>> _mergingExprCodes; // Only set if collator slot provided on construction. value::SlotAccessor* _collatorAccessor = nullptr; + // Function object which can be used to check whether two materialized rows of key values are + // equal. This comparison is collation-aware if the query has a non-simple collation. + value::MaterializedRowEq _keyEq; + boost::optional<TableType> _ht; TableType::iterator _htIt; @@ -212,10 +266,31 @@ private: // Memory tracking and spilling to disk. const long long _approxMemoryUseInBytesBeforeSpill = internalQuerySBEAggApproxMemoryUseInBytesBeforeSpill.load(); + + // A record store which is instantiated and written to in the case of spilling. std::unique_ptr<TemporaryRecordStore> _recordStore; - bool _drainingRecordStore{false}; std::unique_ptr<SeekableRecordCursor> _rsCursor; + // A monotically increasing counter used to ensure uniqueness of 'RecordId' values. When + // spilling, the key is encoding into the 'RecordId' of the '_recordStore'. Record ids must be + // unique by definition, but we might end up spilling multiple partial aggregates for the same + // key. We ensure uniqueness by appending a unique integer to the end of this key, which is + // simply ignored during deserialization. + int64_t _ridCounter = 0; + + // Partial aggregates that have been spilled are read into '_spilledAggRow' and read using + // '_spilledAggsAccessors' so that they can be merged to compute the final aggregate value. + value::MaterializedRow _spilledAggRow{0}; + std::vector<std::unique_ptr<value::MaterializedSingleRowAccessor>> _spilledAggsAccessors; + value::SlotAccessorMap _spilledAggsAccessorMap; + + // Buffer to hold data for the deserialized key values from '_stashedNextRow'. + BufBuilder _stashedKeyBuffer; + // Place to stash the next keys and values during the streaming phase. The record store cursor + // doesn't offer a "peek" API, so we need to hold onto the next row between getNext() calls when + // the key value advances. + SpilledRow _stashedNextRow; + HashAggStats _specificStats; // If provided, used during a trial run to accumulate certain execution stats. Once the trial diff --git a/src/mongo/db/exec/sbe/stages/hash_lookup.cpp b/src/mongo/db/exec/sbe/stages/hash_lookup.cpp index 0d976f1a875..fbc8ff73058 100644 --- a/src/mongo/db/exec/sbe/stages/hash_lookup.cpp +++ b/src/mongo/db/exec/sbe/stages/hash_lookup.cpp @@ -334,7 +334,7 @@ void HashLookupStage::spillBufferedValueToDisk(OperationContext* opCtx, size_t HashLookupStage::bufferValueOrSpill(value::MaterializedRow& value) { size_t bufferIndex = _valueId; const long long newMemUsage = _computedTotalMemUsage + size_estimator::estimate(value); - if (newMemUsage <= _memoryUseInBytesBeforeSpill) { + if (!hasSpilledBufToDisk() && newMemUsage <= _memoryUseInBytesBeforeSpill) { _buffer.emplace_back(std::move(value)); _computedTotalMemUsage = newMemUsage; } else { diff --git a/src/mongo/db/exec/sbe/stages/ix_scan.cpp b/src/mongo/db/exec/sbe/stages/ix_scan.cpp index bfad6d9a2ae..fe88d9c9095 100644 --- a/src/mongo/db/exec/sbe/stages/ix_scan.cpp +++ b/src/mongo/db/exec/sbe/stages/ix_scan.cpp @@ -45,6 +45,7 @@ IndexScanStage::IndexScanStage(UUID collUuid, boost::optional<value::SlotId> recordSlot, boost::optional<value::SlotId> recordIdSlot, boost::optional<value::SlotId> snapshotIdSlot, + boost::optional<value::SlotId> indexIdSlot, IndexKeysInclusionSet indexKeysToInclude, value::SlotVector vars, boost::optional<value::SlotId> seekKeySlotLow, @@ -58,6 +59,7 @@ IndexScanStage::IndexScanStage(UUID collUuid, _recordSlot(recordSlot), _recordIdSlot(recordIdSlot), _snapshotIdSlot(snapshotIdSlot), + _indexIdSlot(indexIdSlot), _indexKeysToInclude(indexKeysToInclude), _vars(std::move(vars)), _seekKeySlotLow(seekKeySlotLow), @@ -76,6 +78,7 @@ std::unique_ptr<PlanStage> IndexScanStage::clone() const { _recordSlot, _recordIdSlot, _snapshotIdSlot, + _indexIdSlot, _indexKeysToInclude, _vars, _seekKeySlotLow, @@ -128,10 +131,17 @@ void IndexScanStage::prepare(CompileCtx& ctx) { static_cast<bool>(entry)); _ordering = entry->ordering(); + auto [indexIdTag, indexIdVal] = value::makeNewString(StringData(_indexName)); + _indexIdAccessor.reset(indexIdTag, indexIdVal); + + if (_indexIdSlot) { + _indexIdViewAccessor.reset(indexIdTag, indexIdVal); + } else { + _indexIdViewAccessor.reset(); + } + if (_snapshotIdAccessor) { - _snapshotIdAccessor->reset( - value::TypeTags::NumberInt64, - value::bitcastFrom<uint64_t>(_opCtx->recoveryUnit()->getSnapshotId().toNumber())); + _latestSnapshotId = _opCtx->recoveryUnit()->getSnapshotId().toNumber(); } } @@ -148,6 +158,10 @@ value::SlotAccessor* IndexScanStage::getAccessor(CompileCtx& ctx, value::SlotId return _snapshotIdAccessor.get(); } + if (_indexIdSlot && *_indexIdSlot == slot) { + return &_indexIdViewAccessor; + } + if (auto it = _accessorMap.find(slot); it != _accessorMap.end()) { return it->second; } @@ -219,9 +233,7 @@ void IndexScanStage::doRestoreState(bool relinquishCursor) { // Yield is the only time during plan execution that the snapshotId can change. As such, we // update it accordingly as part of yield recovery. if (_snapshotIdAccessor) { - _snapshotIdAccessor->reset( - value::TypeTags::NumberInt64, - value::bitcastFrom<uint64_t>(_opCtx->recoveryUnit()->getSnapshotId().toNumber())); + _latestSnapshotId = _opCtx->recoveryUnit()->getSnapshotId().toNumber(); } } @@ -385,6 +397,12 @@ PlanState IndexScanStage::getNext() { false, value::TypeTags::RecordId, value::bitcastFrom<RecordId*>(&_nextRecord->loc)); } + if (_snapshotIdAccessor) { + // Copy the latest snapshot ID into the 'snapshotId' slot. + _snapshotIdAccessor->reset(value::TypeTags::NumberInt64, + value::bitcastFrom<uint64_t>(_latestSnapshotId)); + } + if (_accessors.size()) { _valuesBuffer.reset(); readKeyStringValueIntoAccessors( @@ -423,6 +441,9 @@ std::unique_ptr<PlanStageStats> IndexScanStage::getStats(bool includeDebugInfo) if (_snapshotIdSlot) { bob.appendNumber("snapshotIdSlot", static_cast<long long>(*_snapshotIdSlot)); } + if (_indexIdSlot) { + bob.appendNumber("indexIdSlot", static_cast<long long>(*_indexIdSlot)); + } if (_seekKeySlotLow) { bob.appendNumber("seekKeySlotLow", static_cast<long long>(*_seekKeySlotLow)); } @@ -471,6 +492,12 @@ std::vector<DebugPrinter::Block> IndexScanStage::debugPrint() const { DebugPrinter::addIdentifier(ret, DebugPrinter::kNoneKeyword); } + if (_indexIdSlot) { + DebugPrinter::addIdentifier(ret, _indexIdSlot.value()); + } else { + DebugPrinter::addIdentifier(ret, DebugPrinter::kNoneKeyword); + } + ret.emplace_back(DebugPrinter::Block("[`")); size_t varIndex = 0; for (size_t keyIndex = 0; keyIndex < _indexKeysToInclude.size(); ++keyIndex) { diff --git a/src/mongo/db/exec/sbe/stages/ix_scan.h b/src/mongo/db/exec/sbe/stages/ix_scan.h index ce00ef17128..33fcd352d0c 100644 --- a/src/mongo/db/exec/sbe/stages/ix_scan.h +++ b/src/mongo/db/exec/sbe/stages/ix_scan.h @@ -51,7 +51,8 @@ namespace mongo::sbe { * The "output" slots are * - 'recordSlot': the "KeyString" representing the index entry, * - 'recordIdSlot': a reference that can be used to fetch the entire document, - * - 'snapshotIdSlot': the storage snapshot that this index scan is reading from, and + * - 'snapshotIdSlot': the storage snapshot that this index scan is reading from, + * - 'indexIdSlot': the name of the index being read from, and * - 'vars': one slot for each value in the index key that should be "projected" out of the entry. * * The 'indexKeysToInclude' bitset determines which values are included in the projection based @@ -63,10 +64,11 @@ namespace mongo::sbe { * * Debug string representation: * - * ixscan recordSlot? recordIdSlot? snapshotIdSlot? [slot_1 = fieldNo_1, ..., slot2 = fieldNo_n] + * ixscan recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? + * [slot_1 = fieldNo_1, ..., slot2 = fieldNo_n] * collectionUuid indexName forward * - * ixseek lowKey highKey recordSlot? recordIdSlot? snapshotIdSlot? + * ixseek lowKey highKey recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? * [slot_1 = fieldNo_1, ..., slot2 = fieldNo_n] * collectionUuid indexName forward */ @@ -78,6 +80,7 @@ public: boost::optional<value::SlotId> recordSlot, boost::optional<value::SlotId> recordIdSlot, boost::optional<value::SlotId> snapshotIdSlot, + boost::optional<value::SlotId> indexIdSlot, IndexKeysInclusionSet indexKeysToInclude, value::SlotVector vars, boost::optional<value::SlotId> seekKeySlotLow, @@ -125,6 +128,7 @@ private: const boost::optional<value::SlotId> _recordSlot; const boost::optional<value::SlotId> _recordIdSlot; const boost::optional<value::SlotId> _snapshotIdSlot; + const boost::optional<value::SlotId> _indexIdSlot; const IndexKeysInclusionSet _indexKeysToInclude; const value::SlotVector _vars; const boost::optional<value::SlotId> _seekKeySlotLow; @@ -141,6 +145,14 @@ private: std::unique_ptr<value::OwnedValueAccessor> _recordIdAccessor; std::unique_ptr<value::OwnedValueAccessor> _snapshotIdAccessor; + value::OwnedValueAccessor _indexIdAccessor; + value::ViewOfValueAccessor _indexIdViewAccessor; + + // This field holds the latest snapshot ID that we've received from _opCtx->recoveryUnit(). + // This field gets initialized by prepare(), and it gets updated each time doRestoreState() is + // called. + uint64_t _latestSnapshotId{0}; + // One accessor and slot for each key component that this stage will bind from an index entry's // KeyString. The accessors are in the same order as the key components they bind to. std::vector<value::OwnedValueAccessor> _accessors; diff --git a/src/mongo/db/exec/sbe/stages/plan_stats.h b/src/mongo/db/exec/sbe/stages/plan_stats.h index 263a4f86660..f487c7a45d2 100644 --- a/src/mongo/db/exec/sbe/stages/plan_stats.h +++ b/src/mongo/db/exec/sbe/stages/plan_stats.h @@ -278,8 +278,13 @@ struct HashAggStats : public SpecificStats { } bool usedDisk{false}; + // The number of times that the entire hash table was spilled. + long long numSpills{0}; + // The number of individual records spilled to disk. long long spilledRecords{0}; - long long lastSpilledRecordSize{0}; + // An estimate, in bytes, of the size of the final spill table after all spill events have taken + // place. + long long spilledDataStorageSize{0}; }; struct HashLookupStats : public SpecificStats { diff --git a/src/mongo/db/exec/sbe/stages/scan.cpp b/src/mongo/db/exec/sbe/stages/scan.cpp index 678d3f84ef9..3d601d8779e 100644 --- a/src/mongo/db/exec/sbe/stages/scan.cpp +++ b/src/mongo/db/exec/sbe/stages/scan.cpp @@ -209,6 +209,7 @@ void ScanStage::doSaveState(bool relinquishCursor) { cursor->setSaveStorageCursorOnDetachFromOperationContext(!relinquishCursor); } + _indexCatalogEntryMap.clear(); _coll.reset(); } @@ -391,9 +392,14 @@ PlanState ScanStage::getNext() { } // Return EOF if the index key is found to be inconsistent. - if (_scanCallbacks.indexKeyConsistencyCheckCallBack && - !_scanCallbacks.indexKeyConsistencyCheckCallBack( - _opCtx, _snapshotIdAccessor, _indexIdAccessor, _indexKeyAccessor, _coll, *nextRecord)) { + if (_scanCallbacks.indexKeyConsistencyCheckCallback && + !_scanCallbacks.indexKeyConsistencyCheckCallback(_opCtx, + _indexCatalogEntryMap, + _snapshotIdAccessor, + _indexIdAccessor, + _indexKeyAccessor, + _coll, + *nextRecord)) { return trackPlanState(PlanState::IS_EOF); } @@ -457,6 +463,7 @@ void ScanStage::close() { auto optTimer(getOptTimer(_opCtx)); trackClose(); + _indexCatalogEntryMap.clear(); _cursor.reset(); _randomCursor.reset(); _coll.reset(); @@ -742,6 +749,7 @@ void ParallelScanStage::doSaveState(bool relinquishCursor) { _cursor->save(); } + _indexCatalogEntryMap.clear(); _coll.reset(); } @@ -899,8 +907,9 @@ PlanState ParallelScanStage::getNext() { } // Return EOF if the index key is found to be inconsistent. - if (_scanCallbacks.indexKeyConsistencyCheckCallBack && - !_scanCallbacks.indexKeyConsistencyCheckCallBack(_opCtx, + if (_scanCallbacks.indexKeyConsistencyCheckCallback && + !_scanCallbacks.indexKeyConsistencyCheckCallback(_opCtx, + _indexCatalogEntryMap, _snapshotIdAccessor, _indexIdAccessor, _indexKeyAccessor, @@ -956,6 +965,7 @@ void ParallelScanStage::close() { auto optTimer(getOptTimer(_opCtx)); trackClose(); + _indexCatalogEntryMap.clear(); _cursor.reset(); _coll.reset(); _open = false; diff --git a/src/mongo/db/exec/sbe/stages/scan.h b/src/mongo/db/exec/sbe/stages/scan.h index 37462ac5e14..6f4980f52e1 100644 --- a/src/mongo/db/exec/sbe/stages/scan.h +++ b/src/mongo/db/exec/sbe/stages/scan.h @@ -45,11 +45,11 @@ struct ScanCallbacks { IndexKeyConsistencyCheckCallback indexKeyConsistencyCheck = {}, ScanOpenCallback scanOpen = {}) : indexKeyCorruptionCheckCallback(std::move(indexKeyCorruptionCheck)), - indexKeyConsistencyCheckCallBack(std::move(indexKeyConsistencyCheck)), + indexKeyConsistencyCheckCallback(std::move(indexKeyConsistencyCheck)), scanOpenCallback(std::move(scanOpen)) {} IndexKeyCorruptionCheckCallback indexKeyCorruptionCheckCallback; - IndexKeyConsistencyCheckCallback indexKeyConsistencyCheckCallBack; + IndexKeyConsistencyCheckCallback indexKeyConsistencyCheckCallback; ScanOpenCallback scanOpenCallback; }; @@ -83,12 +83,12 @@ struct ScanCallbacks { * * Debug string representations: * - * scan recordSlot|none recordIdSlot|none snapshotIdSlot|none indexIdSlot|none indexKeySlot|none - * indexKeyPatternSlot|none [slot1 = fieldName1, ... slot_n = fieldName_n] collectionUuid + * scan recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? indexKeySlot? + * indexKeyPatternSlot? [slot1 = fieldName1, ... slot_n = fieldName_n] collectionUuid * forward needOplogSlotForTs * - * seek seekKeySlot recordSlot|none recordIdSlot|none snapshotIdSlot|none indexIdSlot|none - * indexKeySlot|none indexKeyPatternSlot|none [slot1 = fieldName1, ... slot_n = fieldName_n] + * seek seekKeySlot recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? indexKeySlot? + * indexKeyPatternSlot? [slot1 = fieldName1, ... slot_n = fieldName_n] * collectionUuid forward needOplogSlotForTs */ class ScanStage final : public PlanStage { @@ -197,6 +197,8 @@ private: // collection is still valid. Only relevant to capped collections. bool _needsToCheckCappedPositionLost = false; + StringMap<const IndexCatalogEntry*> _indexCatalogEntryMap; + #if defined(MONGO_CONFIG_DEBUG_BUILD) // Debug-only buffer used to track the last thing returned from the stage. Between // saves/restores this is used to check that the storage cursor has not changed position. @@ -311,6 +313,8 @@ private: std::unique_ptr<SeekableRecordCursor> _cursor; + StringMap<const IndexCatalogEntry*> _indexCatalogEntryMap; + #if defined(MONGO_CONFIG_DEBUG_BUILD) // Debug-only buffer used to track the last thing returned from the stage. Between // saves/restores this is used to check that the storage cursor has not changed position. diff --git a/src/mongo/db/exec/sbe/stages/sort.cpp b/src/mongo/db/exec/sbe/stages/sort.cpp index 5acf73afe8d..eebdc0c2443 100644 --- a/src/mongo/db/exec/sbe/stages/sort.cpp +++ b/src/mongo/db/exec/sbe/stages/sort.cpp @@ -213,11 +213,11 @@ void SortStage::open(bool reOpen) { _specificStats.totalDataSizeBytes += _sorter->totalDataSizeSorted(); _mergeIt.reset(_sorter->done()); - _specificStats.spills += _sorter->numSpills(); + _specificStats.spills += _sorter->stats().spilledRanges(); _specificStats.keysSorted += _sorter->numSorted(); auto& metricsCollector = ResourceConsumption::MetricsCollector::get(_opCtx); metricsCollector.incrementKeysSorted(_sorter->numSorted()); - metricsCollector.incrementSorterSpills(_sorter->numSpills()); + metricsCollector.incrementSorterSpills(_sorter->stats().spilledRanges()); _children[0]->close(); } diff --git a/src/mongo/db/exec/sbe/stages/union.cpp b/src/mongo/db/exec/sbe/stages/union.cpp index a661e6c579f..3ddadb8c912 100644 --- a/src/mongo/db/exec/sbe/stages/union.cpp +++ b/src/mongo/db/exec/sbe/stages/union.cpp @@ -62,28 +62,30 @@ std::unique_ptr<PlanStage> UnionStage::clone() const { } void UnionStage::prepare(CompileCtx& ctx) { - value::SlotSet dupCheck; - for (size_t childNum = 0; childNum < _children.size(); childNum++) { _children[childNum]->prepare(ctx); } + // All of the slots listed in '_outputVals' must be unique. + value::SlotSet dupCheck; + for (auto slot : _outputVals) { + auto [it, inserted] = dupCheck.insert(slot); + uassert(4822807, str::stream() << "duplicate field: " << slot, inserted); + } + for (size_t idx = 0; idx < _outputVals.size(); ++idx) { std::vector<value::SlotAccessor*> accessors; accessors.reserve(_children.size()); for (size_t childNum = 0; childNum < _children.size(); childNum++) { + // Slots listed in '_inputVals' may not appear in '_outputVals'. auto slot = _inputVals[childNum][idx]; - auto [it, inserted] = dupCheck.insert(slot); - uassert(4822806, str::stream() << "duplicate field: " << slot, inserted); + bool slotFound = dupCheck.count(slot); + uassert(4822806, str::stream() << "duplicate field: " << slot, !slotFound); accessors.emplace_back(_children[childNum]->getAccessor(ctx, slot)); } - auto slot = _outputVals[idx]; - auto [it, inserted] = dupCheck.insert(slot); - uassert(4822807, str::stream() << "duplicate field: " << slot, inserted); - _outValueAccessors.emplace_back(value::SwitchAccessor{std::move(accessors)}); } } diff --git a/src/mongo/db/exec/sbe/util/spilling.cpp b/src/mongo/db/exec/sbe/util/spilling.cpp index c54f3bfe956..6e8c027d7fe 100644 --- a/src/mongo/db/exec/sbe/util/spilling.cpp +++ b/src/mongo/db/exec/sbe/util/spilling.cpp @@ -85,7 +85,6 @@ int upsertToRecordStore(OperationContext* opCtx, 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()); diff --git a/src/mongo/db/exec/sbe/util/spilling.h b/src/mongo/db/exec/sbe/util/spilling.h index 95f73e2b02e..0a562d7e864 100644 --- a/src/mongo/db/exec/sbe/util/spilling.h +++ b/src/mongo/db/exec/sbe/util/spilling.h @@ -55,9 +55,12 @@ boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* op RecordStore* rs, const RecordId& rid); -/** Inserts or updates a key/value into 'rs'. The 'update' flag controls whether or not an 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, RecordStore* rs, @@ -65,7 +68,6 @@ int upsertToRecordStore(OperationContext* opCtx, const value::MaterializedRow& val, const KeyString::TypeBits& typeBits, bool update); - int upsertToRecordStore(OperationContext* opCtx, RecordStore* rs, const RecordId& key, diff --git a/src/mongo/db/exec/sbe/values/slot.cpp b/src/mongo/db/exec/sbe/values/slot.cpp index 45cbc977980..d8b59c6c3db 100644 --- a/src/mongo/db/exec/sbe/values/slot.cpp +++ b/src/mongo/db/exec/sbe/values/slot.cpp @@ -457,7 +457,7 @@ static void serializeValueIntoKeyString(KeyString::Builder& buf, TypeTags tag, V // TODO SERVER-61629: convert this to serialize the 'arr' directly instead of // constructing a BSONArray. BSONArrayBuilder builder; - bson::convertToBsonObj(builder, getArrayView(val)); + bson::convertToBsonObj(builder, value::ArrayEnumerator{tag, val}); buf.appendBool(true); buf.appendArray(BSONArray(builder.done())); break; @@ -564,8 +564,10 @@ void MaterializedRow::serializeIntoKeyString(KeyString::Builder& buf) const { } } -MaterializedRow MaterializedRow::deserializeFromKeyString(const KeyString::Value& keyString, - BufBuilder* valueBufferBuilder) { +MaterializedRow MaterializedRow::deserializeFromKeyString( + const KeyString::Value& keyString, + BufBuilder* valueBufferBuilder, + boost::optional<size_t> numPrefixValsToRead) { BufReader reader(keyString.getBuffer(), keyString.getSize()); KeyString::TypeBits typeBits(keyString.getTypeBits()); KeyString::TypeBits::Reader typeBitsReader(typeBits); @@ -577,7 +579,8 @@ MaterializedRow MaterializedRow::deserializeFromKeyString(const KeyString::Value &reader, &typeBitsReader, false /* inverted */, typeBits.version, &valBuilder); } while (keepReading); - MaterializedRow result{valBuilder.numValues()}; + size_t sizeOfRow = numPrefixValsToRead ? *numPrefixValsToRead : valBuilder.numValues(); + MaterializedRow result{sizeOfRow}; valBuilder.readValues(result); return result; diff --git a/src/mongo/db/exec/sbe/values/slot.h b/src/mongo/db/exec/sbe/values/slot.h index f853f816d4d..2452c8bd094 100644 --- a/src/mongo/db/exec/sbe/values/slot.h +++ b/src/mongo/db/exec/sbe/values/slot.h @@ -483,10 +483,16 @@ public: * intended for spilling key values used in the HashAgg stage. The format is not guaranteed to * be stable between versions, so it should not be used for long-term storage or communication * between instances. + * + * If 'numPrefixValsToRead' is provided, then only the given number of values from 'keyString' + * are decoded into the resulting 'MaterializedRow'. The remaining suffix values in the + * 'keyString' are ignored. */ - static MaterializedRow deserializeFromKeyString(const KeyString::Value& keyString, + static MaterializedRow deserializeFromKeyString( + const KeyString::Value& keyString, + BufBuilder* valueBufferBuilder, + boost::optional<size_t> numPrefixValsToRead = boost::none); - BufBuilder* valueBufferBuilder); void serializeIntoKeyString(KeyString::Builder& builder) const; private: @@ -577,9 +583,9 @@ private: }; struct MaterializedRowEq { - using ComparatorType = StringData::ComparatorInterface*; + using ComparatorType = StringData::ComparatorInterface; - explicit MaterializedRowEq(const ComparatorType comparator = nullptr) + explicit MaterializedRowEq(const ComparatorType* comparator = nullptr) : _comparator(comparator) {} bool operator()(const MaterializedRow& lhs, const MaterializedRow& rhs) const { @@ -597,7 +603,7 @@ struct MaterializedRowEq { } private: - const ComparatorType _comparator = nullptr; + const ComparatorType* _comparator = nullptr; }; struct MaterializedRowLess { diff --git a/src/mongo/db/exec/sbe/values/value.cpp b/src/mongo/db/exec/sbe/values/value.cpp index 6c21f4f6e5d..e0f73c87ddf 100644 --- a/src/mongo/db/exec/sbe/values/value.cpp +++ b/src/mongo/db/exec/sbe/values/value.cpp @@ -860,7 +860,7 @@ bool isInfinity(TypeTags tag, Value val) { (tag == TypeTags::NumberDecimal && bitcastTo<Decimal128>(val).isInfinite()); } -void ArraySet::push_back(TypeTags tag, Value val) { +bool ArraySet::push_back(TypeTags tag, Value val) { if (tag != TypeTags::Nothing) { ValueGuard guard{tag, val}; auto [it, inserted] = _values.insert({tag, val}); @@ -868,7 +868,11 @@ void ArraySet::push_back(TypeTags tag, Value val) { if (inserted) { guard.reset(); } + + return inserted; } + + return false; } std::pair<TypeTags, Value> ArrayEnumerator::getViewOfValue() const { diff --git a/src/mongo/db/exec/sbe/values/value.h b/src/mongo/db/exec/sbe/values/value.h index 26ba3e5e1de..ae207106d8b 100644 --- a/src/mongo/db/exec/sbe/values/value.h +++ b/src/mongo/db/exec/sbe/values/value.h @@ -844,7 +844,14 @@ public: } } - void push_back(TypeTags tag, Value val); + /** + * Adds the given SBE value to the set if an equal value is not already present. Assumes + * ownership of the given value. + * + * Returns true if the value was newly inserted, otherwise returns false to indicate that an + * equal value was already present in the set. + */ + bool push_back(TypeTags tag, Value val); auto& values() const noexcept { return _values; diff --git a/src/mongo/db/exec/sbe/values/value_builder.h b/src/mongo/db/exec/sbe/values/value_builder.h index 9ad2b511242..00333e9f824 100644 --- a/src/mongo/db/exec/sbe/values/value_builder.h +++ b/src/mongo/db/exec/sbe/values/value_builder.h @@ -191,8 +191,11 @@ public: virtual size_t numValues() const = 0; protected: + // We expect most rows to end up containing this many values or fewer. + static constexpr int kInlinedVectorSize = 16; + std::pair<TypeTags, Value> getValue(size_t index, int bufferLen) { - invariant(index < _numValues); + invariant(index < _tagList.size()); auto tag = _tagList[index]; auto val = _valList[index]; @@ -224,9 +227,8 @@ protected: } void appendValue(TypeTags tag, Value val) noexcept { - _tagList[_numValues] = tag; - _valList[_numValues] = val; - ++_numValues; + _tagList.push_back(tag); + _valList.push_back(val); } void appendValue(std::pair<TypeTags, Value> in) noexcept { @@ -241,14 +243,12 @@ protected: // storing a pointer, we store an _offset_ into the under-construction buffer. Translation from // offset to pointer occurs as part of the 'readValues()' function. void appendValueBufferOffset(TypeTags tag) { - _tagList[_numValues] = tag; - _valList[_numValues] = value::bitcastFrom<int32_t>(_valueBufferBuilder->len()); - ++_numValues; + _tagList.push_back(tag); + _valList.push_back(value::bitcastFrom<int32_t>(_valueBufferBuilder->len())); } - std::array<TypeTags, Ordering::kMaxCompoundIndexKeys> _tagList; - std::array<Value, Ordering::kMaxCompoundIndexKeys> _valList; - size_t _numValues = 0; + absl::InlinedVector<TypeTags, kInlinedVectorSize> _tagList; + absl::InlinedVector<Value, kInlinedVectorSize> _valList; BufBuilder* _valueBufferBuilder; }; @@ -270,11 +270,12 @@ public: // buffer, this value will remain in that buffer, even though we've removed it from the // list. It will still get deallocated along with everything else when that buffer gets // cleared or deleted, though, so there is no leak. - --_numValues; + _tagList.pop_back(); + _valList.pop_back(); } size_t numValues() const override { - return _numValues; + return _tagList.size(); } /** @@ -284,7 +285,7 @@ public: */ void readValues(std::vector<OwnedValueAccessor>* accessors) { auto bufferLen = _valueBufferBuilder->len(); - for (size_t i = 0; i < _numValues; ++i) { + for (size_t i = 0; i < _tagList.size(); ++i) { auto [tag, val] = getValue(i, bufferLen); invariant(i < accessors->size()); (*accessors)[i].reset(false, tag, val); @@ -304,7 +305,7 @@ public: size_t numValues() const override { size_t nVals = 0; size_t bufIdx = 0; - while (bufIdx < _numValues) { + while (bufIdx < _tagList.size()) { auto tag = _tagList[bufIdx]; auto val = _valList[bufIdx]; if (tag == TypeTags::Boolean && !bitcastTo<bool>(val)) { @@ -323,7 +324,10 @@ public: auto bufferLen = _valueBufferBuilder->len(); size_t bufIdx = 0; size_t rowIdx = 0; - while (bufIdx < _numValues) { + // The 'row' output parameter might be smaller than the number of values owned by this + // builder. Be careful to only read as many values into 'row' as this output 'row' has space + // for. + while (rowIdx < row.size()) { invariant(rowIdx < row.size()); auto [_, tagNothing, valNothing] = getValue(bufIdx++, bufferLen); tassert(6136200, "sbe tag must be 'Boolean'", tagNothing == TypeTags::Boolean); 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 5b44bef6549..3be3212c627 100644 --- a/src/mongo/db/exec/sbe/values/value_serialization_test.cpp +++ b/src/mongo/db/exec/sbe/values/value_serialization_test.cpp @@ -268,6 +268,18 @@ TEST_F(ValueSerializeForKeyString, SbeArray) { runTest({{testDataTag, testDataVal}}); } +TEST_F(ValueSerializeForKeyString, ArraySet) { + auto [tag, val] = sbe::value::makeNewArraySet(); + sbe::value::ValueGuard guard{tag, val}; + auto* arraySet = sbe::value::getArraySetView(val); + + arraySet->push_back(value::TypeTags::NumberInt32, value::bitcastFrom<int32_t>(1)); + arraySet->push_back(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(2)); + arraySet->push_back(value::TypeTags::NumberDouble, value::bitcastFrom<double>(3.0)); + + runTest({{tag, val}}); +} + TEST_F(ValueSerializeForKeyString, DateTime) { runTest({{value::TypeTags::Date, value::bitcastFrom<int64_t>(1234)}, {value::TypeTags::Timestamp, value::bitcastFrom<uint64_t>(5678)}}); @@ -442,4 +454,17 @@ TEST_F(ValueSerializeForKeyString, BsonCodeWScope) { runTest({{cwsTag1, cwsVal1}, {cwsTag2, cwsVal2}, {cwsTag3, cwsVal3}}); } + +// Test that roundtripping through KeyString works for a wide row. KeyStrings used in indexes are +// typically constrained in the number of components they can have, since we limit compound indexes +// to at most 32 components. But roundtripping rows wider than 32 still needs to work. +// +// This test was originally designed to reproduce SERVER-76321. +TEST_F(ValueSerializeForKeyString, RoundtripWideRow) { + std::vector<std::pair<sbe::value::TypeTags, sbe::value::Value>> row; + for (int32_t i = 0; i < 40; ++i) { + row.emplace_back(sbe::value::TypeTags::NumberInt32, sbe::value::bitcastFrom<int32_t>(i)); + } + runTest(row); +} } // namespace mongo::sbe diff --git a/src/mongo/db/exec/sbe/vm/arith.cpp b/src/mongo/db/exec/sbe/vm/arith.cpp index e4c67775ad8..1d41ff59ce1 100644 --- a/src/mongo/db/exec/sbe/vm/arith.cpp +++ b/src/mongo/db/exec/sbe/vm/arith.cpp @@ -503,6 +503,57 @@ void ByteCode::aggDoubleDoubleSumImpl(value::Array* arr, } } +void ByteCode::aggMergeDoubleDoubleSumsImpl(value::Array* accumulator, + value::TypeTags rhsTag, + value::Value rhsValue) { + auto [accumWidestType, _1] = accumulator->getAt(AggSumValueElems::kNonDecimalTotalTag); + + tassert(7039532, "value must be of type 'Array'", rhsTag == value::TypeTags::Array); + auto nextDoubleDoubleArr = value::getArrayView(rhsValue); + + tassert(7039533, + "array does not have enough elements", + nextDoubleDoubleArr->size() >= AggSumValueElems::kMaxSizeOfArray - 1); + + // First aggregate the non-decimal sum, then the non-decimal addend. Both should be doubles. + auto [sumTag, sum] = nextDoubleDoubleArr->getAt(AggSumValueElems::kNonDecimalTotalSum); + tassert(7039534, "expected 'NumberDouble'", sumTag == value::TypeTags::NumberDouble); + aggDoubleDoubleSumImpl(accumulator, sumTag, sum); + + auto [addendTag, addend] = nextDoubleDoubleArr->getAt(AggSumValueElems::kNonDecimalTotalAddend); + tassert(7039535, "expected 'NumberDouble'", addendTag == value::TypeTags::NumberDouble); + // There is a special case when the 'sum' is infinite and the 'addend' is NaN. This DoubleDouble + // value represents infinity, not NaN. Therefore, we avoid incorporating the NaN 'addend' value + // into the sum. + if (std::isfinite(value::bitcastTo<double>(sum)) || + !std::isnan(value::bitcastTo<double>(addend))) { + aggDoubleDoubleSumImpl(accumulator, addendTag, addend); + } + + // Determine the widest non-decimal type that we've seen so far, and set the accumulator state + // accordingly. We do this after computing the sums, since 'aggDoubleDoubleSumImpl()' will + // set the widest type to 'NumberDouble' when we call it above. + auto [newValWidestType, _2] = nextDoubleDoubleArr->getAt(AggSumValueElems::kNonDecimalTotalTag); + tassert( + 7039536, "unexpected 'NumberDecimal'", newValWidestType != value::TypeTags::NumberDecimal); + tassert( + 7039537, "unexpected 'NumberDecimal'", accumWidestType != value::TypeTags::NumberDecimal); + auto widestType = getWidestNumericalType(newValWidestType, accumWidestType); + accumulator->setAt( + AggSumValueElems::kNonDecimalTotalTag, widestType, value::bitcastFrom<int32_t>(0)); + + // If there's a decimal128 sum as part of the incoming DoubleDouble sum, incorporate it into the + // accumulator. + if (nextDoubleDoubleArr->size() == AggSumValueElems::kMaxSizeOfArray) { + auto [decimalTotalTag, decimalTotalVal] = + nextDoubleDoubleArr->getAt(AggSumValueElems::kDecimalTotal); + tassert(7039538, + "The decimalTotal must be 'NumberDecimal'", + decimalTotalTag == TypeTags::NumberDecimal); + aggDoubleDoubleSumImpl(accumulator, decimalTotalTag, decimalTotalVal); + } +} + void ByteCode::aggStdDevImpl(value::Array* arr, value::TypeTags rhsTag, value::Value rhsValue) { if (!isNumber(rhsTag)) { return; @@ -551,6 +602,67 @@ void ByteCode::aggStdDevImpl(value::Array* arr, value::TypeTags rhsTag, value::V return setStdDevArray(newCountVal, newMeanVal, newM2Val, arr); } +void ByteCode::aggMergeStdDevsImpl(value::Array* accumulator, + value::TypeTags rhsTag, + value::Value rhsValue) { + tassert(7039542, "expected value of type 'Array'", rhsTag == value::TypeTags::Array); + auto nextArr = value::getArrayView(rhsValue); + + tassert(7039543, + "expected array to have exactly 3 elements", + accumulator->size() == AggStdDevValueElems::kSizeOfArray); + tassert(7039544, + "expected array to have exactly 3 elements", + nextArr->size() == AggStdDevValueElems::kSizeOfArray); + + auto [newCountTag, newCountVal] = nextArr->getAt(AggStdDevValueElems::kCount); + tassert(7039545, "expected 64-bit int", newCountTag == value::TypeTags::NumberInt64); + int64_t newCount = value::bitcastTo<int64_t>(newCountVal); + + // If the incoming partial aggregate has a count of zero, then it represents the partial + // standard deviation of no data points. This means that it can be safely ignored, and we return + // the accumulator as is. + if (newCount == 0) { + return; + } + + auto [oldCountTag, oldCountVal] = accumulator->getAt(AggStdDevValueElems::kCount); + tassert(7039546, "expected 64-bit int", oldCountTag == value::TypeTags::NumberInt64); + int64_t oldCount = value::bitcastTo<int64_t>(oldCountVal); + + auto [oldMeanTag, oldMeanVal] = accumulator->getAt(AggStdDevValueElems::kRunningMean); + tassert(7039547, "expected double", oldMeanTag == value::TypeTags::NumberDouble); + double oldMean = value::bitcastTo<double>(oldMeanVal); + + auto [newMeanTag, newMeanVal] = nextArr->getAt(AggStdDevValueElems::kRunningMean); + tassert(7039548, "expected double", newMeanTag == value::TypeTags::NumberDouble); + double newMean = value::bitcastTo<double>(newMeanVal); + + auto [oldM2Tag, oldM2Val] = accumulator->getAt(AggStdDevValueElems::kRunningM2); + tassert(7039531, "expected double", oldM2Tag == value::TypeTags::NumberDouble); + double oldM2 = value::bitcastTo<double>(oldM2Val); + + auto [newM2Tag, newM2Val] = nextArr->getAt(AggStdDevValueElems::kRunningM2); + tassert(7039541, "expected double", newM2Tag == value::TypeTags::NumberDouble); + double newM2 = value::bitcastTo<double>(newM2Val); + + const double delta = newMean - oldMean; + // We've already handled the case where 'newCount' is zero above. This means that 'totalCount' + // must be positive, and prevents us from ever dividing by zero in the subsequent calculation. + int64_t totalCount = oldCount + newCount; + if (delta != 0) { + newMean = ((oldCount * oldMean) + (newCount * newMean)) / totalCount; + newM2 += delta * delta * + (static_cast<double>(oldCount) * static_cast<double>(newCount) / totalCount); + } + newM2 += oldM2; + + setStdDevArray(value::bitcastFrom<int64_t>(totalCount), + value::bitcastFrom<double>(newMean), + value::bitcastFrom<double>(newM2), + accumulator); +} + std::tuple<bool, value::TypeTags, value::Value> ByteCode::aggStdDevFinalizeImpl( value::Value fieldValue, bool isSamp) { auto arr = value::getArrayView(fieldValue); diff --git a/src/mongo/db/exec/sbe/vm/vm.cpp b/src/mongo/db/exec/sbe/vm/vm.cpp index 7eb8eb7149e..812de5232af 100644 --- a/src/mongo/db/exec/sbe/vm/vm.cpp +++ b/src/mongo/db/exec/sbe/vm/vm.cpp @@ -1072,35 +1072,40 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::aggSum(value::TypeTags return genericAdd(accTag, accValue, fieldTag, fieldValue); } +template <bool merging> std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinAggDoubleDoubleSum( ArityType arity) { - auto [_, fieldTag, fieldValue] = getFromStack(1); // Move the incoming accumulator state from the stack. Given that we are now the owner of the // state we are free to do any in-place update as we see fit. auto [accTag, accValue] = moveOwnedFromStack(0); - value::ValueGuard guard{accTag, accValue}; // Initialize the accumulator. if (accTag == value::TypeTags::Nothing) { std::tie(accTag, accValue) = value::makeNewArray(); - value::ValueGuard guard{accTag, accValue}; + value::ValueGuard newArrGuard{accTag, accValue}; auto arr = value::getArrayView(accValue); arr->reserve(AggSumValueElems::kMaxSizeOfArray); - // The order of the following three elements should match to 'AggSumValueElems'. + // The order of the following three elements should match to 'AggSumValueElems'. An absent + // 'kDecimalTotal' element means that we've not seen any decimal value. So, we're not adding + // 'kDecimalTotal' element yet. arr->push_back(value::TypeTags::NumberInt32, value::bitcastFrom<int32_t>(0)); arr->push_back(value::TypeTags::NumberDouble, value::bitcastFrom<double>(0.0)); arr->push_back(value::TypeTags::NumberDouble, value::bitcastFrom<double>(0.0)); - // The absent 'kDecimalTotal' element means that we've not seen any decimal value. So, we're - // not adding 'kDecimalTotal' element yet. - aggDoubleDoubleSumImpl(arr, fieldTag, fieldValue); - guard.reset(); - return {true, accTag, accValue}; + newArrGuard.reset(); } + + value::ValueGuard guard{accTag, accValue}; tassert(5755317, "The result slot must be Array-typed", accTag == value::TypeTags::Array); + auto accumulator = value::getArrayView(accValue); + + if constexpr (merging) { + aggMergeDoubleDoubleSumsImpl(accumulator, fieldTag, fieldValue); + } else { + aggDoubleDoubleSumImpl(accumulator, fieldTag, fieldValue); + } - aggDoubleDoubleSumImpl(value::getArrayView(accValue), fieldTag, fieldValue); guard.reset(); return {true, accTag, accValue}; } @@ -1235,31 +1240,37 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinDoubleDoublePar return {true, tag, val}; } +template <bool merging> std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinAggStdDev(ArityType arity) { auto [_, fieldTag, fieldValue] = getFromStack(1); // Move the incoming accumulator state from the stack. Given that we are now the owner of the // state we are free to do any in-place update as we see fit. auto [accTag, accValue] = moveOwnedFromStack(0); - value::ValueGuard guard{accTag, accValue}; // Initialize the accumulator. if (accTag == value::TypeTags::Nothing) { - auto [newAccTag, newAccValue] = value::makeNewArray(); - value::ValueGuard newGuard{newAccTag, newAccValue}; - auto arr = value::getArrayView(newAccValue); + std::tie(accTag, accValue) = value::makeNewArray(); + value::ValueGuard newArrGuard{accTag, accValue}; + auto arr = value::getArrayView(accValue); arr->reserve(AggStdDevValueElems::kSizeOfArray); // The order of the following three elements should match to 'AggStdDevValueElems'. arr->push_back(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(0)); arr->push_back(value::TypeTags::NumberDouble, value::bitcastFrom<double>(0.0)); arr->push_back(value::TypeTags::NumberDouble, value::bitcastFrom<double>(0.0)); - aggStdDevImpl(arr, fieldTag, fieldValue); - newGuard.reset(); - return {true, newAccTag, newAccValue}; + newArrGuard.reset(); } + + value::ValueGuard guard{accTag, accValue}; tassert(5755210, "The result slot must be Array-typed", accTag == value::TypeTags::Array); + auto accumulator = value::getArrayView(accValue); + + if constexpr (merging) { + aggMergeStdDevsImpl(accumulator, fieldTag, fieldValue); + } else { + aggStdDevImpl(accumulator, fieldTag, fieldValue); + } - aggStdDevImpl(value::getArrayView(accValue), fieldTag, fieldValue); guard.reset(); return {true, accTag, accValue}; } @@ -3019,6 +3030,120 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinConcat(ArityTyp return {true, strTag, strValue}; } +std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinConcatArrays(ArityType arity) { + auto [resTag, resVal] = value::makeNewArray(); + value::ValueGuard resGuard{resTag, resVal}; + auto resView = value::getArrayView(resVal); + + for (ArityType idx = 0; idx < arity; ++idx) { + auto [_, tag, val] = getFromStack(idx); + if (!value::isArray(tag)) { + return {false, value::TypeTags::Nothing, 0}; + } + + for (auto ae = value::ArrayEnumerator{tag, val}; !ae.atEnd(); ae.advance()) { + auto [elTag, elVal] = ae.getViewOfValue(); + auto [copyTag, copyVal] = value::copyValue(elTag, elVal); + resView->push_back(copyTag, copyVal); + } + } + + resGuard.reset(); + + return {true, resTag, resVal}; +} + +std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinAggConcatArraysCapped( + ArityType arity) { + auto [ownArr, tagArr, valArr] = getFromStack(0); + auto [tagNewElem, valNewElem] = moveOwnedFromStack(1); + value::ValueGuard guardNewElem{tagNewElem, valNewElem}; + auto [_, tagSizeCap, valSizeCap] = getFromStack(2); + + tassert(7039508, + "'cap' parameter must be a 32-bit int", + tagSizeCap == value::TypeTags::NumberInt32); + const int32_t sizeCap = value::bitcastTo<int32_t>(valSizeCap); + + // We expect the new value we are adding to the accumulator to be a two-element array where + // the first element is the array to concatenate and the second value is the corresponding size. + tassert(7039512, "expected value of type 'Array'", tagNewElem == value::TypeTags::Array); + auto newArr = value::getArrayView(valNewElem); + tassert(7039527, + "array had unexpected size", + newArr->size() == static_cast<size_t>(AggArrayWithSize::kLast)); + + // Create a new array to hold size and added elements, if is it does not exist yet. + if (tagArr == value::TypeTags::Nothing) { + ownArr = true; + std::tie(tagArr, valArr) = value::makeNewArray(); + auto arr = value::getArrayView(valArr); + + auto [tagAccArr, valAccArr] = value::makeNewArray(); + + // The order is important! The accumulated array should be at index + // AggArrayWithSize::kValues, and the size should be at index + // AggArrayWithSize::kSizeOfValues. + arr->push_back(tagAccArr, valAccArr); + arr->push_back(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(0)); + } else { + // Take ownership of the accumulator. + topStack(false, value::TypeTags::Nothing, 0); + } + + tassert(7039513, "expected array to be owned", ownArr); + value::ValueGuard accumulatorGuard{tagArr, valArr}; + tassert(7039514, "expected accumulator to have type 'Array'", tagArr == value::TypeTags::Array); + auto arr = value::getArrayView(valArr); + tassert(7039515, + "accumulator was array of unexpected size", + arr->size() == static_cast<size_t>(AggArrayWithSize::kLast)); + + // Check that the accumulated size after concatentation won't exceed the limit. + { + auto [tagAccSize, valAccSize] = + arr->getAt(static_cast<size_t>(AggArrayWithSize::kSizeOfValues)); + auto [tagNewSize, valNewSize] = + newArr->getAt(static_cast<size_t>(AggArrayWithSize::kSizeOfValues)); + tassert(7039516, "expected 64-bit int", tagAccSize == value::TypeTags::NumberInt64); + tassert(7039517, "expected 64-bit int", tagNewSize == value::TypeTags::NumberInt64); + const int64_t currentSize = value::bitcastTo<int64_t>(valAccSize); + const int64_t newSize = value::bitcastTo<int64_t>(valNewSize); + const int64_t totalSize = currentSize + newSize; + + if (totalSize >= static_cast<int64_t>(sizeCap)) { + uasserted(ErrorCodes::ExceededMemoryLimit, + str::stream() << "Used too much memory for a single array. Memory limit: " + << sizeCap << ". Concatentating array of " << arr->size() + << " elements and " << currentSize << " bytes with array of " + << newArr->size() << " elements and " << newSize << " bytes."); + } + + // We are still under the size limit. Set the new total size in the accumulator. + arr->setAt(static_cast<size_t>(AggArrayWithSize::kSizeOfValues), + value::TypeTags::NumberInt64, + value::bitcastFrom<int64_t>(totalSize)); + } + + auto [tagAccArr, valAccArr] = arr->getAt(static_cast<size_t>(AggArrayWithSize::kValues)); + tassert(7039518, "expected value of type 'Array'", tagAccArr == value::TypeTags::Array); + auto accArr = value::getArrayView(valAccArr); + + auto [tagNewArray, valNewArray] = newArr->getAt(static_cast<size_t>(AggArrayWithSize::kValues)); + tassert(7039519, "expected value of type 'Array'", tagNewArray == value::TypeTags::Array); + + for (auto i = value::ArrayEnumerator{tagNewArray, valNewArray}; !i.atEnd(); i.advance()) { + auto [elTag, elVal] = i.getViewOfValue(); + // TODO SERVER-71952: Since 'valNewArray' is owned here, in the future we could avoid this + // copy by moving the element out of the array. + auto [copyTag, copyVal] = value::copyValue(elTag, elVal); + accArr->push_back(copyTag, copyVal); + } + + accumulatorGuard.reset(); + return {ownArr, tagArr, valArr}; +} + std::pair<value::TypeTags, value::Value> ByteCode::genericIsMember(value::TypeTags lhsTag, value::Value lhsVal, value::TypeTags rhsTag, @@ -3396,6 +3521,166 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinSetUnion(ArityT return setUnion(argTags, argVals); } +std::tuple<bool, value::TypeTags, value::Value> ByteCode::aggSetUnionCappedImpl( + value::TypeTags tagNewElem, + value::Value valNewElem, + int32_t sizeCap, + CollatorInterface* collator) { + value::ValueGuard guardNewElem{tagNewElem, valNewElem}; + auto [ownAcc, tagAcc, valAcc] = getFromStack(0); + + // We expect the new value we are adding to the accumulator to be a two-element array where + // the first element is the new set of values and the second value is the corresponding size. + tassert(7039526, "expected value of type 'Array'", tagNewElem == value::TypeTags::Array); + auto newArr = value::getArrayView(valNewElem); + tassert(7039528, + "array had unexpected size", + newArr->size() == static_cast<size_t>(AggArrayWithSize::kLast)); + + // Create a new array is it does not exist yet. + if (tagAcc == value::TypeTags::Nothing) { + ownAcc = true; + std::tie(tagAcc, valAcc) = value::makeNewArray(); + auto accArray = value::getArrayView(valAcc); + + auto [tagAccSet, valAccSet] = value::makeNewArraySet(collator); + + // The order is important! The accumulated array should be at index + // AggArrayWithSize::kValues, and the size should be at index + // AggArrayWithSize::kSizeOfValues. + accArray->push_back(tagAccSet, valAccSet); + accArray->push_back(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(0)); + } else { + // Take ownership of the accumulator. + topStack(false, value::TypeTags::Nothing, 0); + } + + tassert(7039520, "expected accumulator value to be owned", ownAcc); + value::ValueGuard guardArr{tagAcc, valAcc}; + + tassert( + 7039521, "expected accumulator to be of type 'Array'", tagAcc == value::TypeTags::Array); + auto accArray = value::getArrayView(valAcc); + tassert(7039522, + "array had unexpected size", + accArray->size() == static_cast<size_t>(AggArrayWithSize::kLast)); + + auto [tagAccArrSet, valAccArrSet] = + accArray->getAt(static_cast<size_t>(AggArrayWithSize::kValues)); + tassert( + 7039523, "expected value of type 'ArraySet'", tagAccArrSet == value::TypeTags::ArraySet); + auto accArrSet = value::getArraySetView(valAccArrSet); + + // Extract the current size of the accumulator. As we add elements to the set, we will increment + // the current size accordingly and throw an exception if we ever exceed the size limit. We + // cannot simply sum the two sizes, since the two sets could have a substantial intersection. + auto [tagAccSize, valAccSize] = + accArray->getAt(static_cast<size_t>(AggArrayWithSize::kSizeOfValues)); + tassert(7039524, "expected 64-bit int", tagAccSize == value::TypeTags::NumberInt64); + int64_t currentSize = value::bitcastTo<int64_t>(valAccSize); + + auto [tagNewValSet, valNewValSet] = + newArr->getAt(static_cast<size_t>(AggArrayWithSize::kValues)); + tassert( + 7039525, "expected value of type 'ArraySet'", tagNewValSet == value::TypeTags::ArraySet); + + for (auto i = value::ArrayEnumerator{tagNewValSet, valNewValSet}; !i.atEnd(); i.advance()) { + auto [elTag, elVal] = i.getViewOfValue(); + int elemSize = value::getApproximateSize(elTag, elVal); + // TODO SERVER-71952: Since 'valNewValSet' is owned here, in the future we could avoid this + // copy by moving the element out of the array. + auto [copyTag, copyVal] = value::copyValue(elTag, elVal); + bool inserted = accArrSet->push_back(copyTag, copyVal); + + if (inserted) { + currentSize += elemSize; + if (currentSize >= static_cast<int64_t>(sizeCap)) { + uasserted(ErrorCodes::ExceededMemoryLimit, + str::stream() << "Used too much memory for a single array. Memory limit: " + << sizeCap << ". Current set has " << accArrSet->size() + << " elements and is " << currentSize << " bytes."); + } + } + } + + // Update the accumulator with the new total size. + accArray->setAt(static_cast<size_t>(AggArrayWithSize::kSizeOfValues), + value::TypeTags::NumberInt64, + value::bitcastFrom<int64_t>(currentSize)); + + guardArr.reset(); + return {ownAcc, tagAcc, valAcc}; +} + +std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinAggSetUnion(ArityType arity) { + auto [ownAcc, tagAcc, valAcc] = getFromStack(0); + + if (tagAcc == value::TypeTags::Nothing) { + // Initialize the accumulator. + ownAcc = true; + std::tie(tagAcc, valAcc) = value::makeNewArraySet(); + } else { + // Take ownership of the accumulator. + topStack(false, value::TypeTags::Nothing, 0); + } + + tassert(7039552, "accumulator must be owned", ownAcc); + value::ValueGuard guardAcc{tagAcc, valAcc}; + tassert(7039553, "accumulator must be of type ArraySet", tagAcc == value::TypeTags::ArraySet); + auto acc = value::getArraySetView(valAcc); + + auto [tagNewSet, valNewSet] = moveOwnedFromStack(1); + value::ValueGuard guardNewSet{tagNewSet, valNewSet}; + if (!value::isArray(tagNewSet)) { + return {false, value::TypeTags::Nothing, 0}; + } + + auto i = value::ArrayEnumerator{tagNewSet, valNewSet}; + while (!i.atEnd()) { + auto [elTag, elVal] = i.getViewOfValue(); + auto [copyTag, copyVal] = value::copyValue(elTag, elVal); + acc->push_back(copyTag, copyVal); + i.advance(); + } + + guardAcc.reset(); + return {ownAcc, tagAcc, valAcc}; +} + +std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinAggSetUnionCapped( + ArityType arity) { + auto [tagNewElem, valNewElem] = moveOwnedFromStack(1); + value::ValueGuard guardNewElem{tagNewElem, valNewElem}; + + auto [_, tagSizeCap, valSizeCap] = getFromStack(2); + tassert(7039509, + "'cap' parameter must be a 32-bit int", + tagSizeCap == value::TypeTags::NumberInt32); + const size_t sizeCap = value::bitcastTo<int32_t>(valSizeCap); + + guardNewElem.reset(); + return aggSetUnionCappedImpl(tagNewElem, valNewElem, sizeCap, nullptr /*collator*/); +} + +std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinAggCollSetUnionCapped( + ArityType arity) { + auto [_1, tagColl, valColl] = getFromStack(1); + auto [tagNewElem, valNewElem] = moveOwnedFromStack(2); + value::ValueGuard guardNewElem{tagNewElem, valNewElem}; + auto [_2, tagSizeCap, valSizeCap] = getFromStack(3); + + tassert(7039510, "expected value of type 'collator'", tagColl == value::TypeTags::collator); + tassert(7039511, + "'cap' parameter must be a 32-bit int", + tagSizeCap == value::TypeTags::NumberInt32); + + guardNewElem.reset(); + return aggSetUnionCappedImpl(tagNewElem, + valNewElem, + value::bitcastTo<int32_t>(valSizeCap), + value::getCollatorView(valColl)); +} + std::tuple<bool, value::TypeTags, value::Value> ByteCode::builtinCollSetIntersection( ArityType arity) { invariant(arity >= 1); @@ -4382,7 +4667,7 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::dispatchBuiltin(Builti case Builtin::doubleDoubleSum: return builtinDoubleDoubleSum(arity); case Builtin::aggDoubleDoubleSum: - return builtinAggDoubleDoubleSum(arity); + return builtinAggDoubleDoubleSum<false /*merging*/>(arity); case Builtin::doubleDoubleSumFinalize: return builtinDoubleDoubleSumFinalize<>(arity); case Builtin::doubleDoubleMergeSumFinalize: @@ -4391,8 +4676,12 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::dispatchBuiltin(Builti return builtinDoubleDoubleSumFinalize<true /*keepIntegerPrecision*/>(arity); case Builtin::doubleDoublePartialSumFinalize: return builtinDoubleDoublePartialSumFinalize(arity); + case Builtin::aggMergeDoubleDoubleSums: + return builtinAggDoubleDoubleSum<true /*merging*/>(arity); case Builtin::aggStdDev: - return builtinAggStdDev(arity); + return builtinAggStdDev<false /*merging*/>(arity); + case Builtin::aggMergeStdDevs: + return builtinAggStdDev<true /*merging*/>(arity); case Builtin::stdDevPopFinalize: return builtinStdDevPopFinalize(arity); case Builtin::stdDevSampFinalize: @@ -4445,6 +4734,16 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::dispatchBuiltin(Builti return builtinRound(arity); case Builtin::concat: return builtinConcat(arity); + case Builtin::concatArrays: + return builtinConcatArrays(arity); + case Builtin::aggConcatArraysCapped: + return builtinAggConcatArraysCapped(arity); + case Builtin::aggSetUnion: + return builtinAggSetUnion(arity); + case Builtin::aggSetUnionCapped: + return builtinAggSetUnionCapped(arity); + case Builtin::aggCollSetUnionCapped: + return builtinAggCollSetUnionCapped(arity); case Builtin::isMember: return builtinIsMember(arity); case Builtin::collIsMember: diff --git a/src/mongo/db/exec/sbe/vm/vm.h b/src/mongo/db/exec/sbe/vm/vm.h index 255a1a497c2..e5148e8d4b2 100644 --- a/src/mongo/db/exec/sbe/vm/vm.h +++ b/src/mongo/db/exec/sbe/vm/vm.h @@ -511,12 +511,33 @@ enum class Builtin : uint8_t { collAddToSet, // agg function to append to a set (with collation) collAddToSetCapped, // agg function to append to a set (with collation), fails when the set // reaches specified size - doubleDoubleSum, // special double summation + + // Special double summation. + doubleDoubleSum, + // A variant of the standard sum aggregate function which maintains a DoubleDouble as the + // accumulator's underlying state. aggDoubleDoubleSum, + // Converts a DoubleDouble sum into a single numeric scalar for use once the summation is + // complete. doubleDoubleSumFinalize, + // A form of doubleDoubleSum finalization only necessary for sharding support when the cluster + // is not yet fully upgraded to FCV 6.0. doubleDoubleMergeSumFinalize, + // Converts a partial sum into a format suitable for serialization over the wire to the merging + // node. The merging node expects the internal state of the DoubleDouble summation to be + // serialized in a particular format. doubleDoublePartialSumFinalize, + // An agg function which can be used to sum a sequence of DoubleDouble inputs, producing the + // resulting total as a DoubleDouble. + aggMergeDoubleDoubleSums, + + // Implements Welford's online algorithm for computing sample or population standard deviation + // in a single pass. aggStdDev, + // Combines standard deviations that have been partially computed on a subset of the data + // using Welford's online algorithm. + aggMergeStdDevs, + stdDevPopFinalize, stdDevSampFinalize, bitTestZero, // test bitwise mask & value is zero @@ -527,6 +548,18 @@ enum class Builtin : uint8_t { toLower, coerceToString, concat, + concatArrays, + + // Agg function to concatenate arrays, failing when the accumulator reaches a specified size. + aggConcatArraysCapped, + + // Agg functions to compute the set union of two arrays, failing when the accumulator reaches a + // specified size. + aggSetUnionCapped, + aggCollSetUnionCapped, + // Agg function for a simple set union (with no size cap or collation). + aggSetUnion, + acos, acosh, asin, @@ -980,11 +1013,19 @@ private: value::TypeTags fieldTag, value::Value fieldValue); - void aggDoubleDoubleSumImpl(value::Array* arr, value::TypeTags rhsTag, value::Value rhsValue); + void aggDoubleDoubleSumImpl(value::Array* accumulator, + value::TypeTags rhsTag, + value::Value rhsValue); + void aggMergeDoubleDoubleSumsImpl(value::Array* accumulator, + value::TypeTags rhsTag, + value::Value rhsValue); // This is an implementation of the following algorithm: // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm - void aggStdDevImpl(value::Array* arr, value::TypeTags rhsTag, value::Value rhsValue); + void aggStdDevImpl(value::Array* accumulator, value::TypeTags rhsTag, value::Value rhsValue); + void aggMergeStdDevsImpl(value::Array* accumulator, + value::TypeTags rhsTag, + value::Value rhsValue); std::tuple<bool, value::TypeTags, value::Value> aggStdDevFinalizeImpl(value::Value fieldValue, bool isSamp); @@ -1103,14 +1144,24 @@ private: CollatorInterface* collator); std::tuple<bool, value::TypeTags, value::Value> builtinAddToSetCapped(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinCollAddToSetCapped(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinDoubleDoubleSum(ArityType arity); + // The template parameter is false for a regular DoubleDouble summation and true if merging + // partially computed DoubleDouble sums. + template <bool merging> std::tuple<bool, value::TypeTags, value::Value> builtinAggDoubleDoubleSum(ArityType arity); + // This is only for compatibility with mongos/sharding and we will revisit this later. template <bool keepIntegerPrecision = false> std::tuple<bool, value::TypeTags, value::Value> builtinDoubleDoubleSumFinalize(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinDoubleDoublePartialSumFinalize( ArityType arity); + + // The template parameter is false for a regular std dev and true if merging partially computed + // standard devations. + template <bool merging> std::tuple<bool, value::TypeTags, value::Value> builtinAggStdDev(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinStdDevPopFinalize(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinStdDevSampFinalize(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinBitTestZero(ArityType arity); @@ -1137,6 +1188,16 @@ private: std::tuple<bool, value::TypeTags, value::Value> builtinTanh(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinRound(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinConcat(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinConcatArrays(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinAggConcatArraysCapped(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinAggSetUnion(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinAggSetUnionCapped(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> builtinAggCollSetUnionCapped(ArityType arity); + std::tuple<bool, value::TypeTags, value::Value> aggSetUnionCappedImpl( + value::TypeTags tagNewElem, + value::Value valNewElem, + int32_t sizeCap, + CollatorInterface* collator); std::tuple<bool, value::TypeTags, value::Value> builtinIsMember(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinCollIsMember(ArityType arity); std::tuple<bool, value::TypeTags, value::Value> builtinIndexOfBytes(ArityType arity); diff --git a/src/mongo/db/exec/sort_executor.h b/src/mongo/db/exec/sort_executor.h index 6509c767e9a..8120f52caa9 100644 --- a/src/mongo/db/exec/sort_executor.h +++ b/src/mongo/db/exec/sort_executor.h @@ -139,7 +139,7 @@ public: } _output.reset(_sorter->done()); _stats.keysSorted += _sorter->numSorted(); - _stats.spills += _sorter->numSpills(); + _stats.spills += _sorter->stats().spilledRanges(); _stats.totalDataSizeBytes += _sorter->totalDataSizeSorted(); _sorter.reset(); } diff --git a/src/mongo/db/exec/stagedebug_cmd.cpp b/src/mongo/db/exec/stagedebug_cmd.cpp index d83af04df07..a4548ce4a4c 100644 --- a/src/mongo/db/exec/stagedebug_cmd.cpp +++ b/src/mongo/db/exec/stagedebug_cmd.cpp @@ -261,7 +261,7 @@ public: BSONObj keyPatternObj = keyPatternElement.Obj(); std::vector<const IndexDescriptor*> indexes; collection->getIndexCatalog()->findIndexesByKeyPattern( - opCtx, keyPatternObj, false, &indexes); + opCtx, keyPatternObj, IndexCatalog::InclusionPolicy::kReady, &indexes); uassert(16890, str::stream() << "Can't find index: " << keyPatternObj, !indexes.empty()); diff --git a/src/mongo/db/exec/update_stage.cpp b/src/mongo/db/exec/update_stage.cpp index 5a273dc2d89..0ed308dc79c 100644 --- a/src/mongo/db/exec/update_stage.cpp +++ b/src/mongo/db/exec/update_stage.cpp @@ -459,24 +459,41 @@ PlanStage::StageState UpdateStage::doWork(WorkingSetID* out) { bool writeToOrphan = false; if (!_params.request->explain() && _isUserInitiatedWrite) { - const auto action = _preWriteFilter.computeAction(member->doc.value()); - if (action == write_stage_common::PreWriteFilter::Action::kSkip) { - LOGV2_DEBUG( - 5983200, - 3, - "Skipping update operation to orphan document to prevent a wrong change " - "stream event", - "namespace"_attr = collection()->ns(), - "record"_attr = member->doc.value()); - return PlanStage::NEED_TIME; - } else if (action == write_stage_common::PreWriteFilter::Action::kWriteAsFromMigrate) { - LOGV2_DEBUG(6184701, - 3, - "Marking update operation to orphan document with the fromMigrate flag " - "to prevent a wrong change stream event", - "namespace"_attr = collection()->ns(), - "record"_attr = member->doc.value()); - writeToOrphan = true; + try { + const auto action = _preWriteFilter.computeAction(member->doc.value()); + if (action == write_stage_common::PreWriteFilter::Action::kSkip) { + LOGV2_DEBUG( + 5983200, + 3, + "Skipping update operation to orphan document to prevent a wrong change " + "stream event", + "namespace"_attr = collection()->ns(), + "record"_attr = member->doc.value()); + return PlanStage::NEED_TIME; + } else if (action == + write_stage_common::PreWriteFilter::Action::kWriteAsFromMigrate) { + LOGV2_DEBUG( + 6184701, + 3, + "Marking update operation to orphan document with the fromMigrate flag " + "to prevent a wrong change stream event", + "namespace"_attr = collection()->ns(), + "record"_attr = member->doc.value()); + writeToOrphan = true; + } + } catch (const ExceptionFor<ErrorCodes::StaleConfig>& ex) { + if (ex->getVersionReceived() == ChunkVersion::IGNORED() && + ex->getCriticalSectionSignal()) { + // If ChunkVersion is IGNORED and we encountered a critical section, then yield, + // wait for critical section to finish and then we'll resume the write from the + // point we had left. We do this to prevent large multi-writes from repeatedly + // failing due to StaleConfig and exhausting the mongos retry attempts. + planExecutorShardingCriticalSectionFuture(opCtx()) = + ex->getCriticalSectionSignal(); + memberFreer.dismiss(); // Keep this member around so we can retry deleting it. + return prepareToRetryWSM(id, out); + } + throw; } } @@ -506,6 +523,18 @@ PlanStage::StageState UpdateStage::doWork(WorkingSetID* out) { } catch (const WriteConflictException&) { memberFreer.dismiss(); // Keep this member around so we can retry updating it. return prepareToRetryWSM(id, out); + } catch (const ExceptionFor<ErrorCodes::StaleConfig>& ex) { + if (ex->getVersionReceived() == ChunkVersion::IGNORED() && + ex->getCriticalSectionSignal()) { + // If ChunkVersion is IGNORED and we encountered a critical section, then yield, + // wait for critical section to finish and then we'll resume the write from the + // point we had left. We do this to prevent large multi-writes from repeatedly + // failing due to StaleConfig and exhausting the mongos retry attempts. + planExecutorShardingCriticalSectionFuture(opCtx()) = ex->getCriticalSectionSignal(); + memberFreer.dismiss(); // Keep this member around so we can retry updating it. + return prepareToRetryWSM(id, out); + } + throw; } // Set member's obj to be the doc we want to return. diff --git a/src/mongo/db/exec/upsert_stage.cpp b/src/mongo/db/exec/upsert_stage.cpp index 423e300fcf7..212880f6af3 100644 --- a/src/mongo/db/exec/upsert_stage.cpp +++ b/src/mongo/db/exec/upsert_stage.cpp @@ -31,7 +31,7 @@ #include "mongo/db/catalog/document_validation.h" #include "mongo/db/catalog/local_oplog_info.h" -#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/concurrency/exception_util.h" #include "mongo/db/curop_failpoint_helpers.h" #include "mongo/db/query/query_feature_flags_gen.h" #include "mongo/db/s/collection_sharding_state.h" @@ -273,7 +273,10 @@ void UpsertStage::_generateNewDocumentFromSuppliedDoc(const FieldRefSet& immutab UpdateDriver replacementDriver(nullptr); // Create a new replacement-style update from the supplied document. - replacementDriver.parse(write_ops::UpdateModification::parseFromClassicUpdate(suppliedDoc), {}); + replacementDriver.parse( + write_ops::UpdateModification( + suppliedDoc, write_ops::UpdateModification::ClassicTag{}, true /* isReplacement */), + {}); replacementDriver.setLogOp(false); // We do not validate for storage, as we will validate the full document before inserting. diff --git a/src/mongo/db/exec/write_stage_common.cpp b/src/mongo/db/exec/write_stage_common.cpp index 9b8aeb371e3..b0a8db18564 100644 --- a/src/mongo/db/exec/write_stage_common.cpp +++ b/src/mongo/db/exec/write_stage_common.cpp @@ -31,7 +31,6 @@ #include "mongo/db/exec/write_stage_common.h" #include "mongo/db/catalog/collection.h" -#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/exec/shard_filterer_impl.h" #include "mongo/db/exec/working_set.h" #include "mongo/db/exec/working_set_common.h" @@ -56,14 +55,15 @@ PreWriteFilter::PreWriteFilter(OperationContext* opCtx, NamespaceString nss) feature_flags::gFeatureFlagNoChangeStreamEventsDueToOrphans.isEnabled(fcv); }()), _skipFiltering([&] { - // Always allow writes on replica sets. + // Allow writes on standalone and replica set. if (serverGlobalParams.clusterRole == ClusterRole::None) { return true; } - // Always allow writes on standalone and secondary nodes. - const auto replCoord{repl::ReplicationCoordinator::get(opCtx)}; - return !replCoord->canAcceptWritesForDatabase(opCtx, NamespaceString::kAdminDb); + // Only the primary node of a shard that is a replica set should run this filter. + const auto replCoord = repl::ReplicationCoordinator::get(opCtx); + return !replCoord->getSettings().usingReplSets() || + !replCoord->canAcceptWritesForDatabase(opCtx, NamespaceString::kAdminDb); }()) {} PreWriteFilter::Action PreWriteFilter::computeAction(const Document& doc) { diff --git a/src/mongo/db/exec/write_stage_common.h b/src/mongo/db/exec/write_stage_common.h index 5628822efff..489afa62f2f 100644 --- a/src/mongo/db/exec/write_stage_common.h +++ b/src/mongo/db/exec/write_stage_common.h @@ -29,8 +29,6 @@ #pragma once -#include "mongo/platform/basic.h" - #include "mongo/db/exec/shard_filterer.h" #include "mongo/db/exec/working_set.h" #include "mongo/db/namespace_string.h" |
