diff options
| author | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-11 15:07:35 -0300 |
|---|---|---|
| committer | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-11 15:07:35 -0300 |
| commit | 4cb8841196d0625dfa3825aa326f071cd27c7b8b (patch) | |
| tree | 1682a647d4463397c119183369ae6f750d5fdcff /src/mongo/db/exec | |
| parent | aa03c6362cbaa767638e6eed9b031d86dd2643d1 (diff) | |
| parent | 8f0827553e09872941945a093b647a4211a9db7f (diff) | |
Update upstream source from tag 'upstream/6.0.0'master
Update to upstream version '6.0.0'
with Debian dir 5604a80ec1c96ca76f25f40d78e6ef855abec322
Diffstat (limited to 'src/mongo/db/exec')
88 files changed, 1286 insertions, 4433 deletions
diff --git a/src/mongo/db/exec/SConscript b/src/mongo/db/exec/SConscript index fb803f2cb53..c2caf69f0f2 100644 --- a/src/mongo/db/exec/SConscript +++ b/src/mongo/db/exec/SConscript @@ -84,7 +84,6 @@ sortExecutorEnv.Library( ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/sorter/sorter_idl', - '$BUILD_DIR/mongo/db/sorter/sorter_stats', ], ) @@ -109,7 +108,7 @@ env.Library( 'stagedebug_cmd.cpp' ], LIBDEPS=[ - "$BUILD_DIR/mongo/db/index/index_access_method", + "$BUILD_DIR/mongo/db/index/index_access_methods", "$BUILD_DIR/mongo/db/query_exec", ], LIBDEPS_PRIVATE=[ @@ -130,7 +129,6 @@ env.CppUnitTest( "find_projection_executor_test.cpp", "inclusion_projection_executor_test.cpp", "projection_executor_builder_test.cpp", - "projection_executor_redaction_test.cpp", "projection_executor_test.cpp", "projection_executor_utils_test.cpp", "projection_executor_wildcard_access_test.cpp", diff --git a/src/mongo/db/exec/add_fields_projection_executor.cpp b/src/mongo/db/exec/add_fields_projection_executor.cpp index 067059167f5..a0fd7f08580 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. - OrderedPathSet _seenPaths; + std::set<std::string, PathPrefixComparator> _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 b1349d715d6..c2c8e8481e3 100644 --- a/src/mongo/db/exec/add_fields_projection_executor.h +++ b/src/mongo/db/exec/add_fields_projection_executor.h @@ -94,9 +94,9 @@ public: */ void parse(const BSONObj& spec); - Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options = {}) const final { - return _root->serialize(explain, options); + Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const final { + return _root->serialize(explain); } /** @@ -112,7 +112,7 @@ public: } DocumentSource::GetModPathsReturn getModifiedPaths() const final { - OrderedPathSet computedPaths; + std::set<std::string> 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 0870c53751a..007e5cc1ffb 100644 --- a/src/mongo/db/exec/add_fields_projection_executor_test.cpp +++ b/src/mongo/db/exec/add_fields_projection_executor_test.cpp @@ -685,62 +685,5 @@ 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 fb8be63cd8a..78bfd05e352 100644 --- a/src/mongo/db/exec/batched_delete_stage.cpp +++ b/src/mongo/db/exec/batched_delete_stage.cpp @@ -242,16 +242,6 @@ 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 8bba2da9e4d..bf2f1444fea 100644 --- a/src/mongo/db/exec/bucket_unpacker.cpp +++ b/src/mongo/db/exec/bucket_unpacker.cpp @@ -34,7 +34,6 @@ #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" @@ -42,14 +41,9 @@ #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; @@ -64,9 +58,6 @@ 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. @@ -86,7 +77,7 @@ auto constructObjectIdValue(const BSONElement& rhs, int bucketMaxSpanSeconds) { oid.init(date, maxOrMin == OIDInit::max); return oid; }; - // Make an ObjectId corresponding to a date value adjusted by the max bucket value for the + // Make an ObjectId cooresponding 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) { @@ -95,16 +86,9 @@ 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 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); + // Since we're out of range, just make a predicate that is true for all date types. + return makeDateOID(Date_t::min(), 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 @@ -146,9 +130,9 @@ std::unique_ptr<MatchExpression> makeOr(std::vector<std::unique_ptr<MatchExpress return std::make_unique<OrMatchExpression>(std::move(nontrivial)); } -BucketSpec::BucketPredicate handleIneligible(IneligiblePredicatePolicy policy, - const MatchExpression* matchExpr, - StringData message) { +std::unique_ptr<MatchExpression> handleIneligible(IneligiblePredicatePolicy policy, + const MatchExpression* matchExpr, + StringData message) { switch (policy) { case IneligiblePredicatePolicy::kError: uasserted( @@ -156,7 +140,7 @@ BucketSpec::BucketPredicate handleIneligible(IneligiblePredicatePolicy policy, "Error translating non-metadata time-series predicate to operate on buckets: " + message + ": " + matchExpr->serialize().toString()); case IneligiblePredicatePolicy::kIgnore: - return {}; + return nullptr; } MONGO_UNREACHABLE_TASSERT(5916307); } @@ -220,49 +204,40 @@ std::unique_ptr<MatchExpression> createTypeEqualityPredicate( return makeOr(std::move(typeEqualityPredicates)); } -// Checks for the situations when it's not possible to create a bucket-level predicate (against the -// computed control values) for the given event-level predicate ('matchExpr'). -boost::optional<StringData> checkComparisonPredicateEligibility( +std::unique_ptr<MatchExpression> createComparisonPredicate( const ComparisonMatchExpressionBase* matchExpr, - const StringData matchExprPath, - const BSONElement& matchExprData, const BucketSpec& bucketSpec, - ExpressionContext::CollationMatchesDefault collationMatchesDefault) { + 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(); + // 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 "operand can't be an object or array"_sd; + return handleIneligible(policy, matchExpr, "operand can't be an object or array"_sd); - const auto isTimeField = (matchExprPath == bucketSpec.timeField()); - - // A bucket might contain events with the missing fields. These events aren't taken in account - // when computing the control values for those fields. This design has two repercussions: - // 1. MatchExpressions have special comparison semantics regarding null, in that {$eq: null} - // will match all documents where the field is either null or missing. This semantics cannot - // be represented in terms of comparisons against the min/max control values. - // 2. Non-type-bracketing predicates, such as {$expr: {$lt(e): ['$x', 42]}} should evaluate to - // "true" if "x" is missing, which also cannot be represented as a bucket-level predicate. - // 1) time field cannot be empty. - // 2) the only type less than null is MinKey, which is internal, so we don't need to guard - // GT and GTE. - // 3) for the buckets that might have mixed schema data, we'll compare the types of min and - // max when _creating_ the bucket-level predicate (that check won't help with missing). + // 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 "can't handle comparison to null"_sd; - if (!isTimeField && - (matchExpr->matchType() == MatchExpression::INTERNAL_EXPR_LTE || - matchExpr->matchType() == MatchExpression::INTERNAL_EXPR_LT)) { - return "can't handle a non-type-bracketing LT or LTE comparisons"_sd; - } + return handleIneligible(policy, matchExpr, "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 "can't handle string comparison with a non-default collation"_sd; + return handleIneligible( + policy, matchExpr, "can't handle string comparison with a non-default collation"_sd); } // This function only handles time and measurement predicates--not metadata. @@ -277,73 +252,38 @@ boost::optional<StringData> checkComparisonPredicateEligibility( // We must avoid mapping predicates on fields computed via $addFields or a computed $project. if (bucketSpec.fieldIsComputed(matchExprPath.toString())) { - 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; + return handleIneligible(policy, matchExpr, "can't handle a computed field"); } + const auto isTimeField = (matchExprPath == bucketSpec.timeField()); if (isTimeField && matchExprData.type() != BSONType::Date) { - // TODO SERVER-84207: right now we will end up unpacking everything and applying the event - // filter, which indeed would be either trivially true or trivially false but it won't be - // optimized away. - return "can't handle comparison of time field to a non-Date type"_sd; + // 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 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 = checkComparisonPredicateEligibility( - 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)); - - // 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; } + auto minPath = std::string{kControlMinFieldNamePrefix} + matchExprPath; + auto maxPath = std::string{kControlMaxFieldNamePrefix} + matchExprPath; + switch (matchExpr->matchType()) { case MatchExpression::EQ: case MatchExpression::INTERNAL_EXPR_EQ: // For $eq, make both a $lte against 'control.min' and a $gte predicate against // 'control.max'. // - // 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 + // 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. 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 @@ -353,91 +293,60 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // // The same procedure applies to aggregation expressions of the form // {$expr: {$eq: [...]}} that can be rewritten to use $_internalExprEq. - 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); + 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))); 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, 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'}} + // 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'}} // 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. - 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); + 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))); 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, and the collection doesn't contain times outside the - // 32 bit range, include a predicate against the _id field which is + // is against the 'time' field, 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'}} @@ -446,83 +355,49 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // // The same procedure applies to aggregation expressions of the form // {$expr: {$gte: [...]}} that can be rewritten to use $_internalExprGte. - 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); + 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))); 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, 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'}} + // converted to the minimum for the corresponding range of ObjectIds. 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. - 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); + 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))); 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, and the collection doesn't contain times outside the - // 32 bit range, include a predicate against the _id field which is + // is against the 'time' field, 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 @@ -530,34 +405,19 @@ std::unique_ptr<MatchExpression> createComparisonPredicate( // // The same procedure applies to aggregation expressions of the form // {$expr: {$lte: [...]}} that can be rewritten to use $_internalExprLte. - 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); + 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))); default: MONGO_UNREACHABLE_TASSERT(5348302); @@ -566,119 +426,9 @@ 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 = checkComparisonPredicateEligibility( - 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> createTightExprTimeFieldPredicate( - const ExprMatchExpression* matchExpr, - const BucketSpec& bucketSpec, - ExpressionContext::CollationMatchesDefault collationMatchesDefault, - boost::intrusive_ptr<ExpressionContext> pExpCtx) { - using namespace timeseries; - RewriteExpr::RewriteResult rewriteRes = - RewriteExpr::rewrite(matchExpr->getExpression(), pExpCtx->getCollator()); - auto unownedExpr = rewriteRes.matchExpression(); - - // There might be children in the $and expression that cannot be rewritten to a match - // expression. If this is the case we cannot assume that the tight predicate or - // wholeBucketFilter produced by the rewritten $and expression is correct. Measurements in the - // bucket might fit the rewritten $and expression, but fail to fit the other children of the - // $and expression and will be returned incorrectly. - - // It is an error to call 'createPredicate' on predicates on the meta field, and it only - // returns a value for predicates on the 'timeField'. - if (unownedExpr && rewriteRes.allSubExpressionsRewritten() && - unownedExpr->path() == bucketSpec.timeField() && - ComparisonMatchExpressionBase::isInternalExprComparison(unownedExpr->matchType())) { - const auto compareMatchExpr = - checked_cast<const ComparisonMatchExpressionBase*>(unownedExpr); - return createTightComparisonPredicate( - compareMatchExpr, bucketSpec, collationMatchesDefault); - } - - return handleIneligible(BucketSpec::IneligiblePredicatePolicy::kIgnore, - matchExpr, - "can only handle comparison $expr match expressions on the timeField") - .tightPredicate; -} - } // namespace -BucketSpec::BucketPredicate BucketSpec::createPredicatesOnBucketLevelField( +std::unique_ptr<MatchExpression> BucketSpec::createPredicatesOnBucketLevelField( const MatchExpression* matchExpr, const BucketSpec& bucketSpec, int bucketMaxSpanSeconds, @@ -693,8 +443,7 @@ BucketSpec::BucketPredicate 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, or $mod. Unrenamable expressions can't be split into a whole bucket level - // filter, when we should return nullptr. + // such as $exists, $mod, or $elemMatch. // // 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 @@ -712,65 +461,39 @@ BucketSpec::BucketPredicate BucketSpec::createPredicatesOnBucketLevelField( if (!includeMetaField) return handleIneligible(policy, matchExpr, "cannot handle an excluded meta field"); - 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}; - } + auto result = matchExpr->shallowClone(); + expression::applyRenamesToExpression( + result.get(), + {{bucketSpec.metaField().get(), timeseries::kBucketMetaFieldName.toString()}}); + return result; } if (matchExpr->matchType() == MatchExpression::AND) { auto nextAnd = static_cast<const AndMatchExpression*>(matchExpr); - auto looseAndExpression = std::make_unique<AndMatchExpression>(); - auto tightAndExpression = std::make_unique<AndMatchExpression>(); - for (size_t i = 0; i < nextAnd->numChildren(); i++) { - auto child = createPredicatesOnBucketLevelField(nextAnd->getChild(i), - bucketSpec, - bucketMaxSpanSeconds, - collationMatchesDefault, - pExpCtx, - haveComputedMetaField, - includeMetaField, - assumeNoMixedSchemaData, - policy); - if (child.loosePredicate) { - looseAndExpression->add(std::move(child.loosePredicate)); - } + auto andMatchExpr = std::make_unique<AndMatchExpression>(); - 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; + 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)); } } - - // 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() == 1) { + return andMatchExpr->releaseChild(0); } - - // 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); + if (andMatchExpr->numChildren() > 0) { + return andMatchExpr; } - return {std::move(looseExpression), std::move(tightExpression)}; + // No error message here: an empty AND is valid. + return nullptr; } 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: @@ -778,9 +501,9 @@ BucketSpec::BucketPredicate 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 looseOrExpression = std::make_unique<OrMatchExpression>(); - auto tightOrExpression = std::make_unique<OrMatchExpression>(); + auto result = std::make_unique<OrMatchExpression>(); + bool alwaysTrue = false; for (size_t i = 0; i < nextOr->numChildren(); i++) { auto child = createPredicatesOnBucketLevelField(nextOr->getChild(i), bucketSpec, @@ -791,86 +514,51 @@ BucketSpec::BucketPredicate BucketSpec::createPredicatesOnBucketLevelField( includeMetaField, assumeNoMixedSchemaData, policy); - if (looseOrExpression && child.loosePredicate) { - looseOrExpression->add(std::move(child.loosePredicate)); + if (child) { + result->add(std::move(child)); } else { - // For loose expression, null means always true, we can short circuit here. - looseOrExpression = nullptr; - } + // Since this argument is always-true, the entire OR is always-true. + alwaysTrue = true; - // 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)); + // Only short circuit if we're uninterested in reporting errors. + if (policy == IneligiblePredicatePolicy::kIgnore) + break; } } + if (alwaysTrue) + return nullptr; - // 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)}; + // No special case for an empty OR: returning nullptr would be incorrect because it + // means 'always-true', here. + return result; } else if (ComparisonMatchExpression::isComparisonMatchExpression(matchExpr) || ComparisonMatchExpressionBase::isInternalExprComparison(matchExpr->matchType())) { - 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, - createTightExprTimeFieldPredicate(checked_cast<const ExprMatchExpression*>(matchExpr), - bucketSpec, - collationMatchesDefault, - pExpCtx)}; + return createComparisonPredicate( + checked_cast<const ComparisonMatchExpressionBase*>(matchExpr), + bucketSpec, + bucketMaxSpanSeconds, + collationMatchesDefault, + pExpCtx, + haveComputedMetaField, + includeMetaField, + assumeNoMixedSchemaData, + policy); } 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()), - nullptr}; + return std::make_unique<InternalBucketGeoWithinMatchExpression>( + geoExpr.getGeometryPtr(), geoExpr.getField()); } } 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>(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}; + 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; } 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 @@ -879,7 +567,7 @@ BucketSpec::BucketPredicate BucketSpec::createPredicatesOnBucketLevelField( "Can't push down {$exists: true} when the collection may have mixed-schema " "buckets.", policy != IneligiblePredicatePolicy::kError); - return {}; + return nullptr; } } else if (matchExpr->matchType() == MatchExpression::MATCH_IN) { // {a: {$in: [X, Y]}} is equivalent to {$or: [ {a: X}, {a: Y} ]}. @@ -921,11 +609,11 @@ BucketSpec::BucketPredicate BucketSpec::createPredicatesOnBucketLevelField( } } if (alwaysTrue) - return {}; + return nullptr; // As above, no special case for an empty IN: returning nullptr would be incorrect because // it means 'always-true', here. - return {std::move(result), nullptr}; + return result; } return handleIneligible(policy, matchExpr, "can't handle this predicate"); } @@ -969,10 +657,12 @@ 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. So we can use default values for the rest of - // the arguments. + // 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. + {}, }, maxSpanSeconds, collationMatchesDefault, @@ -981,14 +671,13 @@ BSONObj BucketSpec::pushdownPredicate( includeMetaField, assumeNoMixedSchemaData, policy) - .loosePredicate : nullptr; BSONObjBuilder result; if (metaOnlyPredicate) - metaOnlyPredicate->serialize(&result, {}); + metaOnlyPredicate->serialize(&result); if (bucketMetricPredicate) - bucketMetricPredicate->serialize(&result, {}); + bucketMetricPredicate->serialize(&result); return result.obj(); } @@ -1004,15 +693,11 @@ 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, @@ -1060,15 +745,11 @@ 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, @@ -1080,7 +761,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 'BucketSpec'. + // phase according to the provided 'Behavior' and 'BucketSpec'. std::vector<std::pair<std::string, BSONObjIterator>> _fieldIters; }; @@ -1150,37 +831,12 @@ 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, @@ -1195,7 +851,7 @@ void BucketUnpackerV1::extractSingleMeasurement( for (auto&& dataElem : dataRegion) { auto colName = dataElem.fieldNameStringData(); - if (!determineIncludeField(colName, spec.behavior(), unpackFieldsToIncludeExclude)) { + if (!determineIncludeField(colName, behavior, unpackFieldsToIncludeExclude)) { continue; } auto value = dataElem[targetIdx]; @@ -1223,15 +879,11 @@ 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, @@ -1261,7 +913,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 'BucketSpec'. + // phase according to the provided 'Behavior' and 'BucketSpec'. std::vector<ColumnStore> _fieldColumns; // Element count @@ -1316,43 +968,12 @@ 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, @@ -1388,16 +1009,12 @@ std::size_t BucketUnpackerV2::numberOfFields() { BucketSpec::BucketSpec(const std::string& timeField, const boost::optional<std::string>& metaField, const std::set<std::string>& fields, - Behavior behavior, - const std::set<std::string>& computedProjections, - bool usesExtendedRange) + const std::set<std::string>& computedProjections) : _fieldSet(fields), - _behavior(behavior), _computedMetaProjFields(computedProjections), _timeField(timeField), _timeFieldHashed(FieldNameHasher().hashedFieldName(_timeField)), - _metaField(metaField), - _usesExtendedRange(usesExtendedRange) { + _metaField(metaField) { if (_metaField) { _metaFieldHashed = FieldNameHasher().hashedFieldName(*_metaField); } @@ -1405,12 +1022,10 @@ 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), - _usesExtendedRange(other._usesExtendedRange) { + _metaField(other._metaField) { if (_metaField) { _metaFieldHashed = HashedFieldName{*_metaField, other._metaFieldHashed->hash()}; } @@ -1418,12 +1033,10 @@ 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)), - _usesExtendedRange(other._usesExtendedRange) { + _metaField(std::move(other._metaField)) { if (_metaField) { _metaFieldHashed = HashedFieldName{*_metaField, other._metaFieldHashed->hash()}; } @@ -1432,7 +1045,6 @@ 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()}; @@ -1440,7 +1052,6 @@ BucketSpec& BucketSpec::operator=(const BucketSpec& other) { if (_metaField) { _metaFieldHashed = HashedFieldName{*_metaField, other._metaFieldHashed->hash()}; } - _usesExtendedRange = other._usesExtendedRange; } return *this; } @@ -1482,8 +1093,8 @@ BucketUnpacker::BucketUnpacker(BucketUnpacker&& other) = default; BucketUnpacker::~BucketUnpacker() = default; BucketUnpacker& BucketUnpacker::operator=(BucketUnpacker&& rhs) = default; -BucketUnpacker::BucketUnpacker(BucketSpec spec) { - setBucketSpec(std::move(spec)); +BucketUnpacker::BucketUnpacker(BucketSpec spec, Behavior unpackerBehavior) { + setBucketSpecAndBehavior(std::move(spec), unpackerBehavior); } void BucketUnpacker::addComputedMetaProjFields(const std::vector<StringData>& computedFieldNames) { @@ -1492,7 +1103,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 (_spec.behavior() == BucketSpec::Behavior::kInclude) { + if (_unpackerBehavior == BucketUnpacker::Behavior::kInclude) { _spec.addIncludeExcludeField(field); } else { // Since exclude is applied after addComputedMetaProjFields, we must erase the new field @@ -1533,27 +1144,6 @@ 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()) { - if (_computedMetaProjections[name]) { - 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 " @@ -1565,6 +1155,7 @@ Document BucketUnpacker::extractSingleMeasurement(int j) { j, _spec, fieldsToIncludeExcludeDuringUnpack(), + _unpackerBehavior, _bucket, _metaValue, _includeTimeField, @@ -1578,10 +1169,9 @@ Document BucketUnpacker::extractSingleMeasurement(int j) { return measurement.freeze(); } -void BucketUnpacker::reset(BSONObj&& bucket, bool bucketMatchedQuery) { +void BucketUnpacker::reset(BSONObj&& bucket) { _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(); @@ -1596,8 +1186,7 @@ void BucketUnpacker::reset(BSONObj&& bucket, bool bucketMatchedQuery) { "The $_internalUnpackBucket stage requires the data region to have a timeField object", timeFieldElem); - _metaBSONElem = _bucket[timeseries::kBucketMetaFieldName]; - _metaValue = Value{_metaBSONElem}; + _metaValue = Value{_bucket[timeseries::kBucketMetaFieldName]}; 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 @@ -1679,10 +1268,10 @@ void BucketUnpacker::reset(BSONObj&& bucket, bool bucketMatchedQuery) { continue; } - // 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'. + // Includes a field when '_unpackerBehavior' is 'kInclude' and it's found in 'fieldSet' or + // _unpackerBehavior is 'kExclude' and it's not found in 'fieldSet'. if (determineIncludeField( - colName, _spec.behavior(), fieldsToIncludeExcludeDuringUnpack())) { + colName, _unpackerBehavior, fieldsToIncludeExcludeDuringUnpack())) { _unpackingImpl->addField(elem); } } @@ -1733,7 +1322,7 @@ int BucketUnpacker::computeMeasurementCount(const BSONObj& bucket, StringData ti } void BucketUnpacker::determineIncludeTimeField() { - const bool isInclude = _spec.behavior() == BucketSpec::Behavior::kInclude; + const bool isInclude = _unpackerBehavior == BucketUnpacker::Behavior::kInclude; const bool fieldSetContainsTime = _spec.fieldSet().find(_spec.timeField()) != _spec.fieldSet().end(); @@ -1753,32 +1342,27 @@ void BucketUnpacker::eraseMetaFromFieldSetAndDetermineIncludeMeta() { } else if (auto itr = _spec.fieldSet().find(*_spec.metaField()); itr != _spec.fieldSet().end()) { _spec.removeIncludeExcludeField(*_spec.metaField()); - _includeMetaField = _spec.behavior() == BucketSpec::Behavior::kInclude; + _includeMetaField = _unpackerBehavior == BucketUnpacker::Behavior::kInclude; } else { - _includeMetaField = _spec.behavior() == BucketSpec::Behavior::kExclude; + _includeMetaField = _unpackerBehavior == BucketUnpacker::Behavior::kExclude; } } -void BucketUnpacker::eraseUnneededComputedMetaProjFields() { - // If this is an inclusion spec and the current computed field is not part of in the include - // fields, it means the computed field should not be available after the current unpack stage. - // Similarly, for exclusion spec, if the current computed field is part of the exclude fields, - // the computed fields should not be available after the current unpack stage. This can happen - // if there was a $project stage after a $addFields stage. - bool removeIfInFieldSet = _spec.behavior() == BucketSpec::Behavior::kExclude; - auto conditionToErase = [&](const std::string& computedField) { - bool inFieldSet = _spec.fieldSet().find(computedField) != _spec.fieldSet().end(); - return inFieldSet == removeIfInFieldSet; - }; - _spec.eraseIfPredTrueFromComputedMetaProjFields(conditionToErase); +void BucketUnpacker::eraseExcludedComputedMetaProjFields() { + if (_unpackerBehavior == BucketUnpacker::Behavior::kExclude) { + for (const auto& field : _spec.fieldSet()) { + _spec.eraseFromComputedMetaProjFields(field); + } + } } -void BucketUnpacker::setBucketSpec(BucketSpec&& bucketSpec) { +void BucketUnpacker::setBucketSpecAndBehavior(BucketSpec&& bucketSpec, Behavior behavior) { + _unpackerBehavior = behavior; _spec = std::move(bucketSpec); eraseMetaFromFieldSetAndDetermineIncludeMeta(); determineIncludeTimeField(); - eraseUnneededComputedMetaProjFields(); + eraseExcludedComputedMetaProjFields(); _includeMinTimeAsMetadata = _spec.includeMinTimeAsMetadata; _includeMaxTimeAsMetadata = _spec.includeMaxTimeAsMetadata; @@ -1799,7 +1383,7 @@ const std::set<std::string>& BucketUnpacker::fieldsToIncludeExcludeDuringUnpack( _unpackFieldsToIncludeExclude = std::set<std::string>(); const auto& metaProjFields = _spec.computedMetaProjFields(); - if (_spec.behavior() == BucketSpec::Behavior::kInclude) { + if (_unpackerBehavior == BucketUnpacker::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 3c63d6abe49..287bd9f2540 100644 --- a/src/mongo/db/exec/bucket_unpacker.h +++ b/src/mongo/db/exec/bucket_unpacker.h @@ -54,18 +54,11 @@ 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 = {}, - Behavior behavior = Behavior::kExclude, - const std::set<std::string>& computedProjections = {}, - bool usesExtendedRange = false); + const std::set<std::string>& computedProjections = {}); BucketSpec(const BucketSpec&); BucketSpec(BucketSpec&&); @@ -99,14 +92,6 @@ public: return _fieldSet; } - void setBehavior(Behavior behavior) { - _behavior = behavior; - } - - Behavior behavior() const { - return _behavior; - } - void addComputedMetaProjFields(const StringData& field) { _computedMetaProjFields.emplace(field); } @@ -115,24 +100,8 @@ public: return _computedMetaProjFields; } - // Remove fields that the predicate function evaluates to true for. - void eraseIfPredTrueFromComputedMetaProjFields(const std::function<bool(std::string)> pred) { - auto it = _computedMetaProjFields.begin(); - while (it != _computedMetaProjFields.end()) { - if (pred(*it)) { - it = _computedMetaProjFields.erase(it); - } else { - ++it; - } - } - } - - void setUsesExtendedRange(bool usesExtendedRange) { - _usesExtendedRange = usesExtendedRange; - } - - bool usesExtendedRange() const { - return _usesExtendedRange; + void eraseFromComputedMetaProjFields(const std::string& field) { + _computedMetaProjFields.erase(field); } // Returns whether 'field' depends on a pushed down $addFields or computed $project. @@ -149,45 +118,27 @@ 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 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. + * 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. * - * 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: + * 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: * {$and: [ * {_id: {$lt: ObjectId(...)}}, * {control.min.time: {$_internalExprLt: new Date(...)}} * ]} * - * 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. + * 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. */ - static BucketPredicate createPredicatesOnBucketLevelField( + static std::unique_ptr<MatchExpression> createPredicatesOnBucketLevelField( const MatchExpression* matchExpr, const BucketSpec& bucketSpec, int bucketMaxSpanSeconds, @@ -233,7 +184,6 @@ 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. @@ -244,7 +194,6 @@ private: boost::optional<std::string> _metaField = boost::none; boost::optional<HashedFieldName> _metaFieldHashed = boost::none; - bool _usesExtendedRange = false; }; /** @@ -252,6 +201,10 @@ 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. */ @@ -261,7 +214,7 @@ public: static const std::set<StringData> reservedBucketFieldNames; BucketUnpacker(); - BucketUnpacker(BucketSpec spec); + BucketUnpacker(BucketSpec spec, Behavior unpackerBehavior); BucketUnpacker(const BucketUnpacker& other) = delete; BucketUnpacker(BucketUnpacker&& other); ~BucketUnpacker(); @@ -275,11 +228,6 @@ 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. */ @@ -298,6 +246,7 @@ public: */ BucketUnpacker copy() const { BucketUnpacker unpackerCopy; + unpackerCopy._unpackerBehavior = _unpackerBehavior; unpackerCopy._spec = _spec; unpackerCopy._includeMetaField = _includeMetaField; unpackerCopy._includeTimeField = _includeTimeField; @@ -307,10 +256,10 @@ public: /** * This resets the unpacker to prepare to unpack a new bucket described by the given document. */ - void reset(BSONObj&& bucket, bool bucketMatchedQuery = false); + void reset(BSONObj&& bucket); - BucketSpec::Behavior behavior() const { - return _spec.behavior(); + Behavior behavior() const { + return _unpackerBehavior; } const BucketSpec& bucketSpec() const { @@ -321,10 +270,6 @@ public: return _bucket; } - bool bucketMatchedQuery() const { - return _bucketMatchedQuery; - } - bool includeMetaField() const { return _includeMetaField; } @@ -361,7 +306,7 @@ public: return std::string{timeseries::kControlMaxFieldNamePrefix} + field; } - void setBucketSpec(BucketSpec&& bucketSpec); + void setBucketSpecAndBehavior(BucketSpec&& bucketSpec, Behavior behavior); void setIncludeMinTimeAsMetadata(); void setIncludeMaxTimeAsMetadata(); @@ -382,19 +327,16 @@ private: // included in the materialized measurements. void eraseMetaFromFieldSetAndDetermineIncludeMeta(); - // Erase computed meta projection fields if they are present in the exclusion field set or if - // they are not present in the inclusion set. - void eraseUnneededComputedMetaProjFields(); + // Erase computed meta projection fields if they are present in the exclusion field set. + 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}; @@ -415,8 +357,6 @@ 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. @@ -443,9 +383,9 @@ private: * Determines if an arbitrary field should be included in the materialized measurements. */ inline bool determineIncludeField(StringData fieldName, - BucketSpec::Behavior unpackerBehavior, + BucketUnpacker::Behavior unpackerBehavior, const std::set<std::string>& unpackFieldsToIncludeExclude) { - const bool isInclude = unpackerBehavior == BucketSpec::Behavior::kInclude; + const bool isInclude = unpackerBehavior == BucketUnpacker::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 1d62aae3768..8ee0f4e05f5 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, - BucketSpec::Behavior behavior, + BucketUnpacker::Behavior behavior, BSONObj bucket, boost::optional<std::string> metaFieldName = boost::none) { - auto spec = - BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields), behavior}; - BucketUnpacker unpacker{std::move(spec)}; + auto spec = BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields)}; + + BucketUnpacker unpacker{std::move(spec), behavior}; unpacker.reset(std::move(bucket)); return unpacker; } @@ -72,13 +72,12 @@ public: * the given 'bucket'. Asserts that 'reset()' throws the given 'errorCode'. */ void assertUnpackerThrowsCode(std::set<std::string> fields, - BucketSpec::Behavior behavior, + BucketUnpacker::Behavior behavior, BSONObj bucket, boost::optional<std::string> metaFieldName, int errorCode) { - auto spec = - BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields), behavior}; - BucketUnpacker unpacker{std::move(spec)}; + auto spec = BucketSpec{kUserDefinedTimeName.toString(), metaFieldName, std::move(fields)}; + BucketUnpacker unpacker{std::move(spec), behavior}; ASSERT_THROWS_CODE(unpacker.reset(std::move(bucket)), AssertionException, errorCode); } @@ -170,11 +169,6 @@ public: } return root.obj(); } - - bool computedMetaProjFieldsContainsField(std::set<std::string>& computedMetaProjFields, - std::string field) { - return computedMetaProjFields.find(field) != computedMetaProjFields.end(); - } }; TEST_F(BucketUnpackerTest, UnpackBasicIncludeAllMeasurementFields) { @@ -187,7 +181,7 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeAllMeasurementFields) { "a:{'0':1, '1':2}, b:{'1':1}}}"); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); @@ -211,7 +205,7 @@ TEST_F(BucketUnpackerTest, ExcludeASingleField) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); @@ -239,7 +233,7 @@ TEST_F(BucketUnpackerTest, EmptyIncludeGetsEmptyMeasurements) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); @@ -265,7 +259,7 @@ TEST_F(BucketUnpackerTest, EmptyExcludeMaterializesAllFields) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -293,7 +287,7 @@ TEST_F(BucketUnpackerTest, SparseColumnsWhereOneColumnIsExhaustedBeforeTheOther) auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -320,7 +314,7 @@ TEST_F(BucketUnpackerTest, UnpackBasicIncludeWithDollarPrefix) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -347,7 +341,7 @@ TEST_F(BucketUnpackerTest, BucketsWithMetadataOnly) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -373,7 +367,7 @@ TEST_F(BucketUnpackerTest, UnorderedRowKeysDoesntAffectMaterialization) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -403,7 +397,7 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadata) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -430,7 +424,7 @@ TEST_F(BucketUnpackerTest, MissingMetaFieldDoesntMaterializeMetadataUnorderedKey auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -458,7 +452,7 @@ TEST_F(BucketUnpackerTest, ExcludedMetaFieldDoesntMaterializeMetadataWhenBucketH auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -485,7 +479,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnUndefinedMeta) { auto test = [&](BSONObj bucket) { assertUnpackerThrowsCode(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString(), 5369600); @@ -505,7 +499,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnUnexpectedMeta) { auto test = [&](BSONObj bucket) { assertUnpackerThrowsCode(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), boost::none /* no metaField provided */, 5369601); @@ -524,7 +518,7 @@ TEST_F(BucketUnpackerTest, NullMetaInBucketMaterializesAsNull) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -556,7 +550,7 @@ TEST_F(BucketUnpackerTest, GetNextHandlesMissingMetaInBucket) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker(fields, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, std::move(bucket), kUserDefinedMetaName.toString()); ASSERT_TRUE(unpacker.hasNext()); @@ -585,7 +579,7 @@ TEST_F(BucketUnpackerTest, EmptyDataRegionInBucketIsTolerated) { auto test = [&](BSONObj bucket) { auto unpacker = makeBucketUnpacker( - fields, BucketSpec::Behavior::kExclude, bucket, kUserDefinedMetaName.toString()); + fields, BucketUnpacker::Behavior::kExclude, bucket, kUserDefinedMetaName.toString()); ASSERT_FALSE(unpacker.hasNext()); }; @@ -597,7 +591,7 @@ TEST_F(BucketUnpackerTest, UnpackerResetThrowsOnEmptyBucket) { auto bucket = Document{}; assertUnpackerThrowsCode(std::move(fields), - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, bucket.toBson(), kUserDefinedMetaName.toString(), 5346510); @@ -615,125 +609,51 @@ TEST_F(BucketUnpackerTest, EraseMetaFromFieldSetAndDetermineIncludeMeta) { } })"); auto unpacker = makeBucketUnpacker(empFields, - BucketSpec::Behavior::kInclude, + BucketUnpacker::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), - BucketSpec::Behavior::kInclude}; + auto specWithMetaInclude = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; // This calls eraseMetaFromFieldSetAndDetermineIncludeMeta. - unpacker.setBucketSpec(std::move(specWithMetaInclude)); + unpacker.setBucketSpecAndBehavior(std::move(specWithMetaInclude), + BucketUnpacker::Behavior::kInclude); 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), - BucketSpec::Behavior::kInclude}; + std::move(fieldsNoMetaInclude)}; std::set<std::string> fieldsNoMetaExclude{"foo"}; auto specWithFooExclude = BucketSpec{kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), - std::move(fieldsNoMetaExclude), - BucketSpec::Behavior::kExclude}; + std::move(fieldsNoMetaExclude)}; - unpacker.setBucketSpec(std::move(specWithFooExclude)); + unpacker.setBucketSpecAndBehavior(std::move(specWithFooExclude), + BucketUnpacker::Behavior::kExclude); ASSERT_TRUE(unpacker.includeMetaField()); - unpacker.setBucketSpec(std::move(specWithFooInclude)); + unpacker.setBucketSpecAndBehavior(std::move(specWithFooInclude), + BucketUnpacker::Behavior::kInclude); 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), - BucketSpec::Behavior::kExclude}; - + auto specMetaExclude = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(excludeFields)}; auto specMetaInclude = specMetaExclude; - specMetaInclude.setBehavior(BucketSpec::Behavior::kInclude); - - unpacker.setBucketSpec(std::move(specMetaExclude)); + unpacker.setBucketSpecAndBehavior(std::move(specMetaExclude), + BucketUnpacker::Behavior::kExclude); ASSERT_TRUE(unpacker.includeMetaField()); - unpacker.setBucketSpec(std::move(specMetaInclude)); + unpacker.setBucketSpecAndBehavior(std::move(specMetaInclude), + BucketUnpacker::Behavior::kInclude); ASSERT_FALSE(unpacker.includeMetaField()); } -TEST_F(BucketUnpackerTest, EraseUnneededComputedMetaProjFieldsWithInclusiveProject) { - auto bucket = fromjson(R"( -{ - control: {version: 1}, - data: { - _id: {'0':4, '1':5, '2':6}, - time: {'0':4, '1': 5, '2': 6} - } -})"); - std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()}; - auto unpacker = makeBucketUnpacker(unpackerFields, - BucketSpec::Behavior::kInclude, - std::move(bucket), - kUserDefinedMetaName.toString()); - - // Add fields to '_computedMetaProjFields'. - unpacker.addComputedMetaProjFields({"hello"_sd, "bye"_sd}); - auto computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields(); - ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello")); - ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye")); - - auto spec = unpacker.bucketSpec(); - std::set<std::string> includeFields{kUserDefinedTimeName.toString(), "bye"}; - spec.setFieldSet(includeFields); - spec.setBehavior(BucketSpec::Behavior::kInclude); - - // This calls eraseUnneededComputedMetaProjFields(). - unpacker.setBucketSpec(std::move(spec)); - computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields(); - // As "hello" was not in the includes, it should be removed. - ASSERT_FALSE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello")); - // As "bye" was in the includes, it should still be in '_computedMetaProjFields'. - ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye")); -} - -TEST_F(BucketUnpackerTest, EraseUnneededComputedMetaProjFieldsWithExclusiveProject) { - auto bucket = fromjson(R"( -{ - control: {version: 1}, - data: { - _id: {'0':4, '1':5, '2':6}, - time: {'0':4, '1': 5, '2': 6} - } -})"); - std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()}; - auto unpacker = makeBucketUnpacker(unpackerFields, - BucketSpec::Behavior::kInclude, - std::move(bucket), - kUserDefinedMetaName.toString()); - - // Add fields to '_computedMetaProjFields'. - unpacker.addComputedMetaProjFields({"hello"_sd, "bye"_sd}); - auto computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields(); - ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello")); - ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye")); - - auto spec = unpacker.bucketSpec(); - std::set<std::string> excludeFields{kUserDefinedTimeName.toString(), "bye"}; - spec.setFieldSet(excludeFields); - spec.setBehavior(BucketSpec::Behavior::kExclude); - - // This calls eraseUnneededComputedMetaProjFields(). - unpacker.setBucketSpec(std::move(spec)); - computedMetaProjFields = unpacker.bucketSpec().computedMetaProjFields(); - // As "hello" was not excluded, it should still exist. - ASSERT_TRUE(computedMetaProjFieldsContainsField(computedMetaProjFields, "hello")); - // As "bye" was in the excludes, it should be removed from '_computedMetaProjFields'. - ASSERT_FALSE(computedMetaProjFieldsContainsField(computedMetaProjFields, "bye")); -} - TEST_F(BucketUnpackerTest, DetermineIncludeTimeField) { auto bucket = fromjson(R"( { @@ -745,25 +665,21 @@ TEST_F(BucketUnpackerTest, DetermineIncludeTimeField) { })"); std::set<std::string> unpackerFields{kUserDefinedTimeName.toString()}; auto unpacker = makeBucketUnpacker(unpackerFields, - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(bucket), kUserDefinedMetaName.toString()); std::set<std::string> includeFields{kUserDefinedTimeName.toString()}; - auto includeSpec = BucketSpec{kUserDefinedTimeName.toString(), - kUserDefinedMetaName.toString(), - std::move(includeFields), - BucketSpec::Behavior::kInclude}; + auto includeSpec = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(includeFields)}; // This calls determineIncludeTimeField. - unpacker.setBucketSpec(std::move(includeSpec)); + unpacker.setBucketSpecAndBehavior(std::move(includeSpec), BucketUnpacker::Behavior::kInclude); ASSERT_TRUE(unpacker.includeTimeField()); std::set<std::string> excludeFields{kUserDefinedTimeName.toString()}; - auto excludeSpec = BucketSpec{kUserDefinedTimeName.toString(), - kUserDefinedMetaName.toString(), - std::move(excludeFields), - BucketSpec::Behavior::kExclude}; - unpacker.setBucketSpec(std::move(excludeSpec)); + auto excludeSpec = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(excludeFields)}; + unpacker.setBucketSpecAndBehavior(std::move(excludeSpec), BucketUnpacker::Behavior::kExclude); ASSERT_FALSE(unpacker.includeTimeField()); } @@ -778,26 +694,24 @@ TEST_F(BucketUnpackerTest, DetermineIncludeFieldIncludeMode) { {"data", Document{}}} .toBson(); - auto spec = BucketSpec{kUserDefinedTimeName.toString(), - kUserDefinedMetaName.toString(), - std::move(fields), - BucketSpec::Behavior::kInclude}; + auto spec = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; BucketUnpacker includeUnpacker; - includeUnpacker.setBucketSpec(std::move(spec)); + includeUnpacker.setBucketSpecAndBehavior(std::move(spec), BucketUnpacker::Behavior::kInclude); // 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, - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, includeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_TRUE(determineIncludeField(includedMeasurementField, - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, includeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_FALSE(determineIncludeField(excludedMeasurementField, - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, includeUnpacker.fieldsToIncludeExcludeDuringUnpack())); } @@ -812,23 +726,21 @@ TEST_F(BucketUnpackerTest, DetermineIncludeFieldExcludeMode) { {"data", Document{}}} .toBson(); - auto spec = BucketSpec{kUserDefinedTimeName.toString(), - kUserDefinedMetaName.toString(), - std::move(fields), - BucketSpec::Behavior::kExclude}; + auto spec = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; BucketUnpacker excludeUnpacker; - excludeUnpacker.setBucketSpec(std::move(spec)); + excludeUnpacker.setBucketSpecAndBehavior(std::move(spec), BucketUnpacker::Behavior::kExclude); excludeUnpacker.reset(std::move(bucket)); ASSERT_FALSE(determineIncludeField(kUserDefinedTimeName, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, excludeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_FALSE(determineIncludeField(includedMeasurementField, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, excludeUnpacker.fieldsToIncludeExcludeDuringUnpack())); ASSERT_TRUE(determineIncludeField(excludedMeasurementField, - BucketSpec::Behavior::kExclude, + BucketUnpacker::Behavior::kExclude, excludeUnpacker.fieldsToIncludeExcludeDuringUnpack())); } @@ -846,11 +758,9 @@ 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), - BucketSpec::Behavior::kInclude}; - auto unpacker = BucketUnpacker{std::move(spec)}; + auto spec = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; + auto unpacker = BucketUnpacker{std::move(spec), BucketUnpacker::Behavior::kInclude}; auto d1 = dateFromISOString("2020-02-17T00:00:00.000Z").getValue(); auto d2 = dateFromISOString("2020-02-17T01:00:00.000Z").getValue(); @@ -893,11 +803,9 @@ 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), - BucketSpec::Behavior::kInclude}; - auto unpacker = BucketUnpacker{std::move(spec)}; + auto spec = BucketSpec{ + kUserDefinedTimeName.toString(), kUserDefinedMetaName.toString(), std::move(fields)}; + auto unpacker = BucketUnpacker{std::move(spec), BucketUnpacker::Behavior::kInclude}; auto d1 = dateFromISOString("2020-02-17T00:00:00.000Z").getValue(); auto d2 = dateFromISOString("2020-02-17T01:00:00.000Z").getValue(); @@ -984,7 +892,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountLess) { auto modifiedCompressedBucket = modifyCompressedBucketElementCount(*compressedBucket, -1); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -1019,7 +927,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMore) { auto modifiedCompressedBucket = modifyCompressedBucketElementCount(*compressedBucket, 1); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -1054,7 +962,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedCountMissing) { auto modifiedCompressedBucket = modifyCompressedBucketElementCount(*compressedBucket, 0); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -1091,7 +999,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchDataField) { modifyCompressedBucketRemoveLastInField(*compressedBucket, "a"_sd); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketSpec::Behavior::kInclude, + BucketUnpacker::Behavior::kInclude, std::move(modifiedCompressedBucket), kUserDefinedMetaName.toString()); @@ -1126,7 +1034,7 @@ TEST_F(BucketUnpackerTest, TamperedCompressedElementMismatchTimeField) { modifyCompressedBucketRemoveLastInField(*compressedBucket, "time"_sd); auto unpacker = makeBucketUnpacker(std::move(fields), - BucketSpec::Behavior::kInclude, + BucketUnpacker::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 5898ac224d9..506ce18d1ce 100644 --- a/src/mongo/db/exec/collection_scan.cpp +++ b/src/mongo/db/exec/collection_scan.cpp @@ -185,26 +185,9 @@ PlanStage::StageState CollectionScan::doWork(WorkingSetID* out) { << "attempting to resume no longer exists in the collection. " << "recordId: " << recordIdToSeek); } - - 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); - } - } } + + return PlanStage::NEED_TIME; } if (_lastSeenId.isNull() && _params.direction == CollectionScanParams::FORWARD && @@ -371,24 +354,16 @@ 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)) { - _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; + if (!beforeStartOfRange(_params, *member) && Filter::passes(member, _filter)) { + if (_params.stopApplyingFilterAfterFirstMatch) { + _filter = nullptr; } + *out = memberID; + return PlanStage::ADVANCED; + } else { + _workingSet->free(memberID); return PlanStage::NEED_TIME; } - if (_params.stopApplyingFilterAfterFirstMatch) { - _filter = nullptr; - } - *out = memberID; - return PlanStage::ADVANCED; } bool CollectionScan::isEOF() { @@ -431,7 +406,9 @@ void CollectionScan::doReattachToOperationContext() { unique_ptr<PlanStageStats> CollectionScan::getStats() { // Add a BSON representation of the filter to the stats tree, if there is one. if (nullptr != _filter) { - _commonStats.filter = _filter->serialize(); + BSONObjBuilder bob; + _filter->serialize(&bob); + _commonStats.filter = bob.obj(); } unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_COLLSCAN); diff --git a/src/mongo/db/exec/collection_scan_common.h b/src/mongo/db/exec/collection_scan_common.h index 8aecb6750fc..ba5559a4491 100644 --- a/src/mongo/db/exec/collection_scan_common.h +++ b/src/mongo/db/exec/collection_scan_common.h @@ -42,16 +42,12 @@ struct CollectionScanParams { }; enum class ScanBoundInclusion { - kExcludeBothStartAndEndRecords = 0b00, - kIncludeStartRecordOnly = 0b01, - kIncludeEndRecordOnly = 0b10, - kIncludeBothStartAndEndRecords = 0b11, + kExcludeBothStartAndEndRecords, + kIncludeStartRecordOnly, + kIncludeEndRecordOnly, + kIncludeBothStartAndEndRecords, }; - static ScanBoundInclusion makeInclusion(bool startInclusive, bool endInclusive) { - return ScanBoundInclusion(int(startInclusive) | (int(endInclusive) << 1)); - } - // If present, this parameter sets the start point of a forward scan or the end point of a // reverse scan. A forward scan will start scanning at the document with the lowest RecordId // greater than or equal to minRecord. A reverse scan will stop and return EOF on the first @@ -115,10 +111,6 @@ 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 581ef2ef294..83941b91c4e 100644 --- a/src/mongo/db/exec/delete_stage.cpp +++ b/src/mongo/db/exec/delete_stage.cpp @@ -178,38 +178,23 @@ PlanStage::StageState DeleteStage::doWork(WorkingSetID* out) { bool writeToOrphan = false; if (!_params->isExplain && !_params->fromMigrate) { - 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; + 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; } } @@ -250,18 +235,6 @@ 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 7172370e4c3..dd833b3c28f 100644 --- a/src/mongo/db/exec/document_value/document.cpp +++ b/src/mongo/db/exec/document_value/document.cpp @@ -88,11 +88,8 @@ const StringDataSet Document::allMetadataFieldNames{Document::metaFieldTextScore Document::metaFieldGeoNearPoint, Document::metaFieldSearchScore, Document::metaFieldSearchHighlights, - Document::metaFieldSearchSortValues, Document::metaFieldIndexKey, - Document::metaFieldSearchScoreDetails, - Document::metaFieldVectorSearchScore, - Document::metaFieldSearchSequenceToken}; + Document::metaFieldSearchScoreDetails}; DocumentStorageIterator::DocumentStorageIterator(DocumentStorage* storage, BSONObjIterator bsonIt) : _bsonIt(std::move(bsonIt)), @@ -133,7 +130,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->bsonHasMetadata() && !fieldName.empty() && fieldName[0] == '$' && + if (_storage->stripMetadata() && fieldName[0] == '$' && Document::allMetadataFieldNames.contains(fieldName)) { return true; } @@ -346,8 +343,8 @@ void DocumentStorage::reserveFields(size_t expectedFields) { } intrusive_ptr<DocumentStorage> DocumentStorage::clone() const { - auto out = make_intrusive<DocumentStorage>( - _bson, _bsonHasMetadata, _modified, _numBytesFromBSONInCache); + auto out = + make_intrusive<DocumentStorage>(_bson, _stripMetadata, _modified, _numBytesFromBSONInCache); if (_cache) { // Make a copy of the buffer with the fields. @@ -376,7 +373,6 @@ intrusive_ptr<DocumentStorage> DocumentStorage::clone() const { out->_haveLazyLoadedMetadata = _haveLazyLoadedMetadata; out->_metadataFields = _metadataFields; - out->_snapshottedSize = _snapshottedSize; return out; } @@ -393,12 +389,11 @@ DocumentStorage::~DocumentStorage() { } } -void DocumentStorage::reset(const BSONObj& bson, bool bsonHasMetadata) { +void DocumentStorage::reset(const BSONObj& bson, bool stripMetadata) { _bson = bson; _numBytesFromBSONInCache = 0; - _bsonHasMetadata = bsonHasMetadata; + _stripMetadata = stripMetadata; _modified = false; - _snapshottedSize = 0; // Clean cache. for (auto it = iteratorCacheOnly(); !it.atEnd(); it.advance()) { @@ -414,22 +409,10 @@ void DocumentStorage::reset(const BSONObj& bson, bool bsonHasMetadata) { _metadataFields = DocumentMetadataFields{}; } -Document DocumentStorage::shred() const { - MutableDocument md; - // Iterate raw bson if possible. This avoids caching all of the values in a doc that might get - // thrown away. - if (!isModified() && !bsonHasMetadata()) { - for (const auto& elem : _bson) { - md[elem.fieldNameStringData()] = Value(elem).shred(); - } - } else { - for (DocumentStorageIterator it = iterator(); !it.atEnd(); it.advance()) { - const auto& valueElem = it.get(); - md[it.fieldName()] = valueElem.val.shred(); - } +void DocumentStorage::fillCache() const { + for (DocumentStorageIterator it = iterator(); !it.atEnd(); it.advance()) { + it->val.fillCache(); } - md.setMetadata(DocumentMetadataFields(metadata())); - return md.freeze(); } void DocumentStorage::loadLazyMetadata() const { @@ -437,13 +420,11 @@ void DocumentStorage::loadLazyMetadata() const { return; } - bool oldModified = _metadataFields.isModified(); - BSONObjIterator it(_bson); while (it.more()) { BSONElement elem(it.next()); auto fieldName = elem.fieldNameStringData(); - if (!fieldName.empty() && fieldName[0] == '$') { + if (fieldName[0] == '$') { if (fieldName == Document::metaFieldTextScore) { _metadataFields.setTextScore(elem.Double()); } else if (fieldName == Document::metaFieldSearchScore) { @@ -479,17 +460,10 @@ 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()); - } else if (fieldName == Document::metaFieldSearchSequenceToken) { - _metadataFields.setSearchSequenceToken(Value(elem)); } } } - _metadataFields.setModified(oldModified); _haveLazyLoadedMetadata = true; } @@ -539,11 +513,22 @@ 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 (isTriviallyConvertible()) { + if (!storage().isModified() && !storage().stripMetadata()) { return storage().bsonObj(); + } else { + return boost::none; } - return boost::none; } constexpr StringData Document::metaFieldTextScore; @@ -554,44 +539,35 @@ constexpr StringData Document::metaFieldGeoNearPoint; constexpr StringData Document::metaFieldSearchScore; constexpr StringData Document::metaFieldSearchHighlights; constexpr StringData Document::metaFieldSearchScoreDetails; -constexpr StringData Document::metaFieldSearchSortValues; -constexpr StringData Document::metaFieldVectorSearchScore; -void Document::toBsonWithMetaData(BSONObjBuilder* builder) const { - toBson(builder); +BSONObj Document::toBsonWithMetaData() const { + BSONObjBuilder bb; + toBson(&bb); if (!metadata()) { - return; + return bb.obj(); } if (metadata().hasTextScore()) - builder->append(metaFieldTextScore, metadata().getTextScore()); + bb.append(metaFieldTextScore, metadata().getTextScore()); if (metadata().hasRandVal()) - builder->append(metaFieldRandVal, metadata().getRandVal()); + bb.append(metaFieldRandVal, metadata().getRandVal()); if (metadata().hasSortKey()) - builder->append(metaFieldSortKey, - DocumentMetadataFields::serializeSortKey(metadata().isSingleElementKey(), - metadata().getSortKey())); + bb.append(metaFieldSortKey, + DocumentMetadataFields::serializeSortKey(metadata().isSingleElementKey(), + metadata().getSortKey())); if (metadata().hasGeoNearDistance()) - builder->append(metaFieldGeoNearDistance, metadata().getGeoNearDistance()); + bb.append(metaFieldGeoNearDistance, metadata().getGeoNearDistance()); if (metadata().hasGeoNearPoint()) - metadata().getGeoNearPoint().addToBsonObj(builder, metaFieldGeoNearPoint); + metadata().getGeoNearPoint().addToBsonObj(&bb, metaFieldGeoNearPoint); if (metadata().hasSearchScore()) - builder->append(metaFieldSearchScore, metadata().getSearchScore()); + bb.append(metaFieldSearchScore, metadata().getSearchScore()); if (metadata().hasSearchHighlights()) - metadata().getSearchHighlights().addToBsonObj(builder, metaFieldSearchHighlights); + metadata().getSearchHighlights().addToBsonObj(&bb, metaFieldSearchHighlights); if (metadata().hasIndexKey()) - builder->append(metaFieldIndexKey, metadata().getIndexKey()); + bb.append(metaFieldIndexKey, metadata().getIndexKey()); if (metadata().hasSearchScoreDetails()) - builder->append(metaFieldSearchScoreDetails, metadata().getSearchScoreDetails()); - if (metadata().hasSearchSortValues()) { - builder->append(metaFieldSearchSortValues, metadata().getSearchSortValues()); - } - if (metadata().hasSearchSequenceToken()) { - metadata().getSearchSequenceToken().addToBsonObj(builder, metaFieldSearchSequenceToken); - } - if (metadata().hasVectorSearchScore()) { - builder->append(metaFieldVectorSearchScore, metadata().getVectorSearchScore()); - } + bb.append(metaFieldSearchScoreDetails, metadata().getSearchScoreDetails()); + return bb.obj(); } Document Document::fromBsonWithMetaData(const BSONObj& bson) { @@ -728,17 +704,31 @@ const Value Document::getNestedField(const FieldPath& path, vector<Position>* po return getNestedFieldHelper(*this, path, positions, 0); } -size_t Document::getApproximateSize() const { - return sizeof(Document) + storage().snapshottedApproximateSize(); +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::getCurrentApproximateSize() const { - return sizeof(Document) + storage().currentApproximateSize(); +size_t Document::getApproximateSize() const { + return getApproximateSizeWithoutBackingBSON() + storage().bsonObjSize(); } size_t Document::memUsageForSorter() const { - return storage().currentApproximateSize() - storage().bsonObjSize() + - storage().nonCachedBsonObjSize(); + return getApproximateSizeWithoutBackingBSON() + 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 6114aee792d..8fcfb28dd8b 100644 --- a/src/mongo/db/exec/document_value/document.h +++ b/src/mongo/db/exec/document_value/document.h @@ -100,10 +100,7 @@ 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 constexpr StringData metaFieldSearchSequenceToken = "$searchSequenceToken"_sd; static const StringDataSet allMetadataFieldNames; @@ -194,8 +191,7 @@ public: /** * Get the approximate size of the Document, plus its underlying storage and sub-values. Returns - * size in bytes. The return value of this function is snapshotted. All subsequent calls of this - * method will return the same value. + * size in bytes. * * Note: Some memory may be shared with other Documents or between fields within a single * Document so this can overestimate usage. @@ -206,11 +202,6 @@ 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 { @@ -248,10 +239,10 @@ public: } /** - * Returns a cache-only copy of the document with no backing bson. + * Populates the internal cache by recursively walking the underlying BSON. */ - Document shred() const { - return storage().shred(); + void fillCache() const { + storage().fillCache(); } /** Calculate a hash value. @@ -262,38 +253,12 @@ 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; - - template <typename BSONTraits = BSONObj::DefaultSizeTrait> - BSONObj toBson() const { - if (isTriviallyConvertible()) { - return storage().bsonObj(); - } - - BSONObjBuilder bb; - toBson(&bb); - return bb.obj<BSONTraits>(); - } + BSONObj toBson() const; /** * Serializes this document iff the conversion is "trivial," meaning that the underlying storage @@ -307,18 +272,7 @@ public: /** * Like the 'toBson()' method, but includes metadata as top-level fields. */ - 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>(); - } + BSONObj toBsonWithMetaData() const; /** * Like Document(BSONObj) but treats top-level fields with special names as metadata. @@ -416,6 +370,12 @@ 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; }; // @@ -551,11 +511,13 @@ public: } /** - * Replace the current base Document with the BSON object. Setting 'bsonHasMetadata' to true - * signals that the BSON object contains metadata fields. + * 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. */ - void reset(const BSONObj& bson, bool bsonHasMetadata) { - storage().reset(bson, bsonHasMetadata); + void reset(const BSONObj& bson, bool stripMetadata) { + storage().reset(bson, stripMetadata); } /** Add the given field to the Document. @@ -692,7 +654,6 @@ 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); @@ -711,12 +672,8 @@ 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()); } @@ -738,13 +695,13 @@ public: storage().makeOwned(); } - /** - * 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). + /** 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). */ - DocumentStorage& newStorageWithBson(const BSONObj& bson, bool bsonHasMetadata) { - reset(make_intrusive<DocumentStorage>(bson, bsonHasMetadata, false, 0)); + DocumentStorage& newStorageWithBson(const BSONObj& bson, bool stripMetadata) { + reset(make_intrusive<DocumentStorage>(bson, stripMetadata, false, 0)); return const_cast<DocumentStorage&>(*storagePtr()); } @@ -782,19 +739,12 @@ 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/resetsnapshottedApproximateSize + // this should only be called by storage methods and peek/freeze 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 d7fb3b337d6..1387ca908b4 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 contains metadata fields we can set the - * 'bsonHasMetadata' flag to true. + * 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. */ DocumentStorage(const BSONObj& bson, - bool bsonHasMetadata, + bool stripMetadata, bool modified, uint32_t numBytesFromBSONInCache) : _cache(nullptr), @@ -373,17 +373,17 @@ public: _hashTabMask(0), _bson(bson), _numBytesFromBSONInCache(numBytesFromBSONInCache), - _bsonHasMetadata(bsonHasMetadata), + _stripMetadata(stripMetadata), _modified(modified) {} ~DocumentStorage(); - void reset(const BSONObj& bson, bool bsonHasMetadata); + void reset(const BSONObj& bson, bool stripMetadata); /** - * Returns a cache-only copy of the document with no backing bson. + * Populates the cache by recursively walking the underlying BSON. */ - Document shred() const; + void fillCache() const; static const DocumentStorage& emptyDoc() { return kEmptyDoc; @@ -551,7 +551,7 @@ public: * WorkingSetMember. */ const DocumentMetadataFields& metadata() const { - if (_bsonHasMetadata) { + if (_stripMetadata) { loadLazyMetadata(); } return _metadataFields; @@ -589,8 +589,8 @@ public: return _firstElement ? _firstElement->plusBytes(_usedBytes) : nullptr; } - auto bsonHasMetadata() const { - return _bsonHasMetadata; + auto stripMetadata() const { + return _stripMetadata; } Position constructInCache(const BSONElement& elem); @@ -599,37 +599,10 @@ 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> @@ -712,23 +685,22 @@ private: // whole backing BSON, but only the portion of backing BSON that's not already in the cache. uint32_t _numBytesFromBSONInCache = 0; - // 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()'. + // 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()'. mutable bool _haveLazyLoadedMetadata = false; mutable DocumentMetadataFields _metadataFields; - // 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}; + // 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}; // 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 27f64ef7286..90e15ef5c2a 100644 --- a/src/mongo/db/exec/document_value/document_metadata_fields.cpp +++ b/src/mongo/db/exec/document_value/document_metadata_fields.cpp @@ -46,7 +46,6 @@ 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; } @@ -55,7 +54,6 @@ DocumentMetadataFields::DocumentMetadataFields(DocumentMetadataFields&& other) DocumentMetadataFields& DocumentMetadataFields::operator=(DocumentMetadataFields&& other) { _holder = std::move(other._holder); - _modified = true; return *this; } @@ -87,21 +85,12 @@ void DocumentMetadataFields::mergeWith(const DocumentMetadataFields& other) { if (!hasSearchScoreDetails() && other.hasSearchScoreDetails()) { setSearchScoreDetails(other.getSearchScoreDetails()); } - if (!hasSearchSequenceToken() && other.hasSearchSequenceToken()) { - setSearchSequenceToken(other.getSearchSequenceToken()); - } if (!hasTimeseriesBucketMinTime() && other.hasTimeseriesBucketMinTime()) { setTimeseriesBucketMinTime(other.getTimeseriesBucketMinTime()); } 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) { @@ -132,21 +121,12 @@ void DocumentMetadataFields::copyFrom(const DocumentMetadataFields& other) { if (other.hasSearchScoreDetails()) { setSearchScoreDetails(other.getSearchScoreDetails()); } - if (other.hasSearchSequenceToken()) { - setSearchSequenceToken(other.getSearchSequenceToken()); - } if (other.hasTimeseriesBucketMinTime()) { setTimeseriesBucketMinTime(other.getTimeseriesBucketMinTime()); } if (other.hasTimeseriesBucketMaxTime()) { setTimeseriesBucketMaxTime(other.getTimeseriesBucketMaxTime()); } - if (other.hasSearchSortValues()) { - setSearchSortValues(other.getSearchSortValues()); - } - if (other.hasVectorSearchScore()) { - setVectorSearchScore(other.getVectorSearchScore()); - } } size_t DocumentMetadataFields::getApproximateSize() const { @@ -168,8 +148,7 @@ size_t DocumentMetadataFields::getApproximateSize() const { size -= sizeof(_holder->searchHighlights); size += _holder->indexKey.objsize(); size += _holder->searchScoreDetails.objsize(); - size += _holder->searchSortValues.objsize(); - size -= sizeof(_holder->searchSequenceToken); + return size; } @@ -217,10 +196,6 @@ void DocumentMetadataFields::serializeForSorter(BufBuilder& buf) const { buf.appendNum(static_cast<char>(MetaType::kSearchScoreDetails + 1)); getSearchScoreDetails().appendSelfToBufBuilder(buf); } - if (hasSearchSequenceToken()) { - buf.appendNum(static_cast<char>(MetaType::kSearchSequenceToken + 1)); - getSearchSequenceToken().serializeForSorter(buf); - } if (hasTimeseriesBucketMinTime()) { buf.appendNum(static_cast<char>(MetaType::kTimeseriesBucketMinTime + 1)); buf.appendNum(getTimeseriesBucketMinTime().toMillisSinceEpoch()); @@ -229,14 +204,6 @@ 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)); } @@ -269,19 +236,9 @@ 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<LittleEndian<long long>>())); + out->setTimeseriesBucketMinTime(Date_t::fromMillisSinceEpoch(buf.read<long long>())); } else if (marker == static_cast<char>(MetaType::kTimeseriesBucketMaxTime) + 1) { - 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 if (marker == static_cast<char>(MetaType::kSearchSequenceToken) + 1) { - out->setSearchSequenceToken( - Value::deserializeForSorter(buf, Value::SorterDeserializeSettings())); + out->setTimeseriesBucketMaxTime(Date_t::fromMillisSinceEpoch(buf.read<long long>())); } else { uasserted(28744, "Unrecognized marker, unable to deserialize buffer"); } @@ -341,12 +298,6 @@ 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::kSearchSequenceToken: - return "$search sequence token"; - 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 6759675c7ea..12932c29686 100644 --- a/src/mongo/db/exec/document_value/document_metadata_fields.h +++ b/src/mongo/db/exec/document_value/document_metadata_fields.h @@ -67,9 +67,6 @@ public: kSearchScoreDetails, kTimeseriesBucketMinTime, kTimeseriesBucketMaxTime, - kSearchSortValues, - kVectorSearchScore, - kSearchSequenceToken, // New fields must be added before the kNumFields sentinel. kNumFields @@ -151,7 +148,11 @@ public: } void setTextScore(double score) { - _setCommon(MetaType::kTextScore); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kTextScore); _holder->textScore = score; } @@ -165,7 +166,11 @@ public: } void setRandVal(double val) { - _setCommon(MetaType::kRandVal); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kRandVal); _holder->randVal = val; } @@ -179,7 +184,11 @@ public: } void setSortKey(Value sortKey, bool isSingleElementKey) { - _setCommon(MetaType::kSortKey); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kSortKey); _holder->isSingleElementKey = isSingleElementKey; _holder->sortKey = std::move(sortKey); } @@ -198,7 +207,11 @@ public: } void setGeoNearDistance(double dist) { - _setCommon(MetaType::kGeoNearDist); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kGeoNearDist); _holder->geoNearDistance = dist; } @@ -212,7 +225,11 @@ public: } void setGeoNearPoint(Value point) { - _setCommon(MetaType::kGeoNearPoint); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kGeoNearPoint); _holder->geoNearPoint = std::move(point); } @@ -226,7 +243,11 @@ public: } void setSearchScore(double score) { - _setCommon(MetaType::kSearchScore); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kSearchScore); _holder->searchScore = score; } @@ -240,7 +261,11 @@ public: } void setSearchHighlights(Value highlights) { - _setCommon(MetaType::kSearchHighlights); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kSearchHighlights); _holder->searchHighlights = highlights; } @@ -254,7 +279,11 @@ public: } void setIndexKey(BSONObj indexKey) { - _setCommon(MetaType::kIndexKey); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kIndexKey); _holder->indexKey = indexKey.getOwned(); } @@ -268,7 +297,11 @@ public: } void setRecordId(RecordId rid) { - _setCommon(MetaType::kRecordId); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + + _holder->metaFields.set(MetaType::kRecordId); _holder->recordId = rid; } @@ -282,7 +315,10 @@ public: } void setSearchScoreDetails(BSONObj details) { - _setCommon(MetaType::kSearchScoreDetails); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + _holder->metaFields.set(MetaType::kSearchScoreDetails); _holder->searchScoreDetails = details.getOwned(); } @@ -291,14 +327,15 @@ public: } Date_t getTimeseriesBucketMinTime() const { - tassert(6850100, - "Document must have timeseries bucket min time metadata field set", - hasTimeseriesBucketMinTime()); + invariant(hasTimeseriesBucketMinTime()); return _holder->timeseriesBucketMinTime; } void setTimeseriesBucketMinTime(Date_t time) { - _setCommon(MetaType::kTimeseriesBucketMinTime); + if (!_holder) { + _holder = std::make_unique<MetadataHolder>(); + } + _holder->metaFields.set(MetaType::kTimeseriesBucketMinTime); _holder->timeseriesBucketMinTime = time; } @@ -307,83 +344,20 @@ public: } Date_t getTimeseriesBucketMaxTime() const { - tassert(6850101, - "Document must have timeseries bucket max time metadata field set", - hasTimeseriesBucketMaxTime()); + invariant(hasTimeseriesBucketMaxTime()); return _holder->timeseriesBucketMaxTime; } void setTimeseriesBucketMaxTime(Date_t time) { - _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; - } - - bool hasSearchSequenceToken() const { - return _holder && _holder->metaFields.test(MetaType::kSearchSequenceToken); - } - - Value getSearchSequenceToken() const { - invariant(hasSearchSequenceToken()); - return _holder->searchSequenceToken; - } - - void setSearchSequenceToken(Value details) { - _setCommon(MetaType::kSearchSequenceToken); - _holder->searchSequenceToken = details; - } - - 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; + _holder->metaFields.set(MetaType::kTimeseriesBucketMaxTime); + _holder->timeseriesBucketMaxTime = time; } + void serializeForSorter(BufBuilder& buf) const; +private: // A simple data struct housing all possible metadata fields. struct MetadataHolder { std::bitset<MetaType::kNumFields> metaFields; @@ -405,18 +379,10 @@ private: BSONObj searchScoreDetails; Date_t timeseriesBucketMinTime; Date_t timeseriesBucketMaxTime; - BSONObj searchSortValues; - double vectorSearchScore{0.0}; - Value searchSequenceToken; }; // 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 23ab2277c96..5515009b2bd 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,7 +34,6 @@ #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 { @@ -51,8 +50,6 @@ 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); @@ -72,8 +69,6 @@ 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) { @@ -88,8 +83,6 @@ 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) { @@ -132,14 +125,6 @@ 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) { @@ -154,8 +139,6 @@ 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); @@ -171,8 +154,6 @@ 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) } @@ -189,8 +170,6 @@ 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); @@ -209,8 +188,6 @@ 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) } @@ -264,8 +241,6 @@ 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) { @@ -291,173 +266,6 @@ 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 ca1123a7555..421da707001 100644 --- a/src/mongo/db/exec/document_value/document_value_test.cpp +++ b/src/mongo/db/exec/document_value/document_value_test.cpp @@ -355,89 +355,6 @@ 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["field"]; - 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); -} - -TEST(ShredDocument, OutputHasNoBackingBSON) { - BSONObj bson = - BSON("a" << 1 << "subObj" << BSON("a" << 1) << "subArray" << BSON_ARRAY(BSON("a" << 1))); - auto original = fromBson(bson); - auto originalSize = original.getApproximateSize(); - - auto shredded = original.shred(); - auto originalSizeAfterShredding = original.getApproximateSize(); - // Fields in the original doc shouldn't be cached since it was raw bson - ASSERT_EQ(originalSize, originalSizeAfterShredding); - - // BSON is more compact than ValueElement - auto shreddedSize = shredded.getApproximateSize(); - ASSERT_LT(originalSize, shreddedSize); - - // Accessing a field shouldn't change the size since all fields are already cached. - shredded["a"]; - ASSERT_EQ(shredded.getCurrentApproximateSize(), shreddedSize); -} - -TEST(ShredDocument, HandlesModifiedDocuments) { - BSONObj bson = BSON("a" << 1 << "subObj" << BSON("a" << 1)); - Document original = fromBson(bson); - MutableDocument md(original); - md["b"] = Value(2); - md["subObj"]["b"] = Value(2); - Document shredded = md.freeze().shred(); - - ASSERT(!shredded["b"].missing()); - ASSERT(!shredded["subObj"]["b"].missing()); -} - -TEST(ShredDocument, HandlesMetadata) { - BSONObj bson = BSON("a" << 1 << "subObj" << BSON("a" << 1)); - Document original = fromBson(bson); - MutableDocument md(original); - DocumentMetadataFields meta; - meta.setSearchScore(6); - md.setMetadata(std::move(meta)); - Document shredded = md.freeze().shred(); - ASSERT_EQ(6, shredded.metadata().getSearchScore()); -} - /** Add Document fields. */ class AddField { public: @@ -788,22 +705,6 @@ 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 { @@ -930,15 +831,6 @@ TEST(MetaFields, FromBsonWithMetadataAcceptsIndexKeyMetadata) { ASSERT_BSONOBJ_EQ(bsonWithoutMetadata, BSON("a" << 1)); } -TEST(MetaFields, FromBsonWithMetadataHandlesEmptyFieldName) { - auto bson = BSON("" << 1 << "$indexKey" << BSON("b" << 1)); - auto doc = Document::fromBsonWithMetaData(bson); - ASSERT_TRUE(doc.metadata().hasIndexKey()); - ASSERT_BSONOBJ_EQ(doc.metadata().getIndexKey(), BSON("b" << 1)); - auto bsonWithoutMetadata = doc.toBson(); - ASSERT_BSONOBJ_EQ(bsonWithoutMetadata, BSON("" << 1)); -} - TEST(MetaFields, CopyMetadataFromCopiesAllMetadata) { Document source = Document::fromBsonWithMetaData( BSON("a" << 1 << "$textScore" << 9.9 << "b" << 1 << "$randVal" << 42.0 << "c" << 1 @@ -948,8 +840,7 @@ TEST(MetaFields, CopyMetadataFromCopiesAllMetadata) { << "foo" << "h" << 1 << "$indexKey" << BSON("y" << 1) << "$searchScoreDetails" << BSON("scoreDetails" - << "foo") - << "$searchSortValues" << BSON("a" << 1) << "$vectorSearchScore" << 6.7)); + << "foo"))); MutableDocument destination{}; destination.copyMetaDataFrom(source); @@ -966,8 +857,6 @@ 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 { @@ -988,8 +877,6 @@ 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()); } @@ -1010,10 +897,6 @@ 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())); } @@ -1028,7 +911,6 @@ TEST_F(SerializationTest, MetaSerializationNoVals) { << "def"_sd)); docBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); - docBuilder.metadata().setVectorSearchScore(40.0); assertRoundTrips(docBuilder.freeze()); } @@ -1043,7 +925,6 @@ TEST_F(SerializationTest, MetaSerializationWithVals) { docBuilder.metadata().setIndexKey(BSON("key" << 42)); docBuilder.metadata().setSearchScoreDetails(BSON("scoreDetails" << "foo")); - docBuilder.metadata().setVectorSearchScore(40.0); assertRoundTrips(docBuilder.freeze()); } @@ -1066,8 +947,6 @@ 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()); @@ -1079,8 +958,6 @@ 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()); @@ -1089,110 +966,6 @@ 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) { @@ -1210,10 +983,8 @@ 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. 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); + // Do a sanity check on the amount of space taken by metadata in document 2. + ASSERT_LT(doc2.getMetadataApproximateSize(), 300U); Document emptyDoc; ASSERT_LT(emptyDoc.getMetadataApproximateSize(), 100U); diff --git a/src/mongo/db/exec/document_value/document_value_test_util.h b/src/mongo/db/exec/document_value/document_value_test_util.h index 7b88c9688fa..b6959a7d17f 100644 --- a/src/mongo/db/exec/document_value/document_value_test_util.h +++ b/src/mongo/db/exec/document_value/document_value_test_util.h @@ -59,13 +59,6 @@ #define _ASSERT_DOCVAL_COMPARISON(NAME, a, b) \ ::mongo::unittest::assertComparison_##NAME(__FILE__, __LINE__, #a, #b, a, b) -// TODO SERVER-87736 make these not say "AUTO". -// These are backport-special macros, adapted from the "AUTO" version on more recent branches. The -// automatic functionality doesn't exist on this branch. But the assertions should still pass. -#define ASSERT_VALUE_EQ_AUTO(expected, val) ASSERT_EQ(expected, val.toString()) -#define ASSERT_DOCUMENT_EQ_AUTO(expected, actual) \ - ASSERT_BSONOBJ_EQ(fromjson(expected), actual.toBson()) - namespace mongo { namespace unittest { diff --git a/src/mongo/db/exec/document_value/value.cpp b/src/mongo/db/exec/document_value/value.cpp index 616efb2128a..248514180f0 100644 --- a/src/mongo/db/exec/document_value/value.cpp +++ b/src/mongo/db/exec/document_value/value.cpp @@ -45,7 +45,6 @@ #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" @@ -936,7 +935,7 @@ void Value::hash_combine(size_t& seed, case Code: case Symbol: { StringData sd = getRawData(); - seed = murmur3<sizeof(size_t)>(sd, seed); + MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); break; } @@ -945,7 +944,7 @@ void Value::hash_combine(size_t& seed, if (stringComparator) { stringComparator->hash_combine(seed, sd); } else { - seed = murmur3<sizeof(size_t)>(sd, seed); + MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); } break; } @@ -969,14 +968,14 @@ void Value::hash_combine(size_t& seed, case BinData: { StringData sd = getRawData(); - seed = murmur3<sizeof(size_t)>(sd, seed); + MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); boost::hash_combine(seed, _storage.binDataType()); break; } case RegEx: { StringData sd = getRawData(); - seed = murmur3<sizeof(size_t)>(sd, seed); + MurmurHash3_x86_32(sd.rawData(), sd.size(), seed, &seed); break; } @@ -1254,17 +1253,14 @@ ostream& operator<<(ostream& out, const Value& val) { verify(false); } -Value Value::shred() const { +void Value::fillCache() const { if (isObject()) { - return Value(getDocument().shred()); + getDocument().fillCache(); } else if (isArray()) { - std::vector<Value> values; for (auto&& val : getArray()) { - values.push_back(val.shred()); + val.fillCache(); } - return Value(values); } - return Value(*this); } void Value::serializeForSorter(BufBuilder& buf) const { diff --git a/src/mongo/db/exec/document_value/value.h b/src/mongo/db/exec/document_value/value.h index 62a31f25727..bf54221326c 100644 --- a/src/mongo/db/exec/document_value/value.h +++ b/src/mongo/db/exec/document_value/value.h @@ -344,9 +344,9 @@ public: friend std::ostream& operator<<(std::ostream& out, const Value& v); /** - * Returns a cache-only copy of the value with no backing bson. + * Populates the internal cache by recursively walking the underlying BSON. */ - Value shred() const; + void fillCache() const; void swap(Value& rhs) { _storage.swap(rhs._storage); @@ -430,16 +430,13 @@ public: ImplicitValue(T&& arg) : Value(std::forward<T>(arg)) {} ImplicitValue(std::initializer_list<ImplicitValue> values) : Value(convertToValues(values)) {} - ImplicitValue(std::vector<ImplicitValue> values) : Value(convertToValues(values)) {} - template <typename T> - ImplicitValue(std::vector<T> values) : Value(convertToValues(values)) {} + ImplicitValue(std::vector<int> values) : Value(convertToValues(values)) {} - template <typename T> - static std::vector<Value> convertToValues(const std::vector<T>& vec) { + static std::vector<Value> convertToValues(const std::vector<int>& vec) { std::vector<Value> values; values.reserve(vec.size()); - for_each(vec.begin(), vec.end(), ([&](const T& val) { values.emplace_back(val); })); + for_each(vec.begin(), vec.end(), ([&](const int& val) { values.emplace_back(val); })); return values; } 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 ac854fdadbe..963a29bc11a 100644 --- a/src/mongo/db/exec/document_value/value_comparator_test.cpp +++ b/src/mongo/db/exec/document_value/value_comparator_test.cpp @@ -312,33 +312,5 @@ 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.cpp b/src/mongo/db/exec/exclusion_projection_executor.cpp index 2061bf4fe93..9823ed1b125 100644 --- a/src/mongo/db/exec/exclusion_projection_executor.cpp +++ b/src/mongo/db/exec/exclusion_projection_executor.cpp @@ -38,15 +38,14 @@ std::pair<BSONObj, bool> ExclusionNode::extractProjectOnFieldAndRename(const Str BSONObjBuilder extractedExclusion; // Check for a projection directly on 'oldName'. For example, {oldName: 0}. - if (auto it = _projectedFieldsSet.find(oldName); it != _projectedFieldsSet.end()) { + if (auto it = _projectedFields.find(oldName); it != _projectedFields.end()) { extractedExclusion.append(newName, false); - _projectedFieldsSet.erase(it); - _projectedFields.remove(std::string(oldName)); + _projectedFields.erase(it); } // Check for a projection on subfields of 'oldName'. For example, {oldName: {a: 0, b: 0}}. if (auto it = _children.find(oldName); it != _children.end()) { - extractedExclusion.append(newName, it->second->serialize(boost::none, {}).toBson()); + extractedExclusion.append(newName, it->second->serialize(boost::none).toBson()); _children.erase(it); } diff --git a/src/mongo/db/exec/exclusion_projection_executor.h b/src/mongo/db/exec/exclusion_projection_executor.h index 37623206723..a9c1fade72d 100644 --- a/src/mongo/db/exec/exclusion_projection_executor.h +++ b/src/mongo/db/exec/exclusion_projection_executor.h @@ -86,9 +86,6 @@ protected: Value transformSkippedValueForOutput(const Value& value) const final { return value; } - bool isIncluded() const final { - return false; - } }; /** @@ -99,12 +96,10 @@ protected: */ class ExclusionProjectionExecutor : public ProjectionExecutor { public: - ExclusionProjectionExecutor( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies, - bool allowFastPath = false, - boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none) - : ProjectionExecutor(expCtx, policies, proj), _root(new ExclusionNode(_policies)) {} + ExclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies, + bool allowFastPath = false) + : ProjectionExecutor(expCtx, policies), _root(new ExclusionNode(_policies)) {} TransformerType getType() const final { return TransformerType::kExclusionProjection; @@ -118,17 +113,16 @@ public: return _root.get(); } - Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options = {}) const final { + Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument output; // The ExclusionNode tree in '_root' will always have a top-level _id node if _id is to be // excluded. If the _id node is not present, then explicitly set {_id: true} to avoid // ambiguity in the expected behavior of the serialized projection. - _root->serialize(explain, &output, options); - auto idFieldName = options.serializeFieldPath("_id"); - if (output.peek()[idFieldName].missing()) { - output.addField(idFieldName, Value{true}); + _root->serialize(explain, &output); + if (output.peek()["_id"].missing()) { + output.addField("_id", Value{true}); } return output.freeze(); } @@ -155,7 +149,7 @@ public: return {DocumentSource::GetModPathsReturn::Type::kAllPaths, {}, {}}; } - OrderedPathSet modifiedPaths; + std::set<std::string> 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 c3866ef865f..983c5e1995a 100644 --- a/src/mongo/db/exec/exclusion_projection_executor_test.cpp +++ b/src/mongo/db/exec/exclusion_projection_executor_test.cpp @@ -363,8 +363,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldEvaluateMetaExpressions) { "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}, " - "m: {$meta: 'vectorSearchScore'}}")); + "l: {$meta: 'searchScoreDetails'}}")); MutableDocument inputDocBuilder(Document{{"a", 1}, {"b", 2}}); inputDocBuilder.metadata().setTextScore(0.0); @@ -378,7 +377,6 @@ 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); @@ -386,7 +384,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'}, m: 9.0}")}); + "l: {scoreDetails: 'foo'}}")}); } TEST(ExclusionProjectionExecutionTest, ShouldAddMetaExpressionsToDependencies) { @@ -400,20 +398,18 @@ TEST(ExclusionProjectionExecutionTest, ShouldAddMetaExpressionsToDependencies) { "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}}, " - "m: {$meta: 'vectorSearchScore'}")); + "l: {$meta: 'searchScoreDetails'}}")); DepsTracker deps; exclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 0UL); - // 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). + // 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). 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/fetch.cpp b/src/mongo/db/exec/fetch.cpp index 2059faf7d79..9d51dad5ca4 100644 --- a/src/mongo/db/exec/fetch.cpp +++ b/src/mongo/db/exec/fetch.cpp @@ -181,8 +181,10 @@ unique_ptr<PlanStageStats> FetchStage::getStats() { _commonStats.isEOF = isEOF(); // Add a BSON representation of the filter to the stats tree, if there is one. - if (_filter) { - _commonStats.filter = _filter->serialize(); + if (nullptr != _filter) { + BSONObjBuilder bob; + _filter->serialize(&bob); + _commonStats.filter = bob.obj(); } unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_FETCH); diff --git a/src/mongo/db/exec/inclusion_projection_executor.cpp b/src/mongo/db/exec/inclusion_projection_executor.cpp index d06cedf61b6..d7384be8109 100644 --- a/src/mongo/db/exec/inclusion_projection_executor.cpp +++ b/src/mongo/db/exec/inclusion_projection_executor.cpp @@ -67,7 +67,7 @@ void FastPathEligibleInclusionNode::_applyProjections(BSONObj bson, BSONObjBuild const auto bsonElement{it.next()}; const auto fieldName{bsonElement.fieldNameStringData()}; - if (_projectedFieldsSet.find(fieldName) != _projectedFieldsSet.end()) { + if (_projectedFields.find(fieldName) != _projectedFields.end()) { bob->append(bsonElement); --nFieldsNeeded; } else if (auto childIt = _children.find(fieldName); childIt != _children.end()) { @@ -99,55 +99,6 @@ 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'. Will return boost::none if - * any expression needs the whole document. - */ -boost::optional<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); - } - - if (deps.needWholeDocument) { - return boost::none; - } - - 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( @@ -158,14 +109,8 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( return {BSONObj{}, false}; } - boost::optional<std::vector<OrderedPathSet>> topLevelDeps = - getTopLevelDeps(_orderToProcessAdditionsAndChildren, _expressions, _children); - - // If one of the expression requires the whole document, then we should not extract the - // projection and topLevelDeps will not hold any field names. - if (!topLevelDeps) { - return {BSONObj{}, false}; - } + DepsTracker allDeps; + reportDependencies(&allDeps); // Auxiliary vector with extracted computed projections: <name, expression, replacement // strategy>. If the replacement strategy flag is true, the expression is replaced with a @@ -173,15 +118,19 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( std::vector<std::tuple<StringData, boost::intrusive_ptr<Expression>, bool>> addFieldsExpressions; bool replaceWithProjField = true; - for (size_t i = 0; i < _orderToProcessAdditionsAndChildren.size(); i++) { - auto&& field = _orderToProcessAdditionsAndChildren[i]; - + for (auto&& field : _orderToProcessAdditionsAndChildren) { 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 @@ -189,17 +138,13 @@ 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.get(), field, i)) { - replaceWithProjField = false; - continue; - } - - const auto& topLevelFieldNames = topLevelDeps.get()[i]; if (topLevelFieldNames.size() == 1 && topLevelFieldNames.count(oldName.toString()) == 1) { // Substitute newName for oldName in the expression. StringMap<std::string> renames; @@ -219,12 +164,11 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInProject( for (const auto& expressionSpec : addFieldsExpressions) { auto&& fieldName = std::get<0>(expressionSpec).toString(); auto oldExpr = std::get<1>(expressionSpec); - oldExpr->serialize().addToBsonObj(&bb, fieldName); + oldExpr->serialize(false).addToBsonObj(&bb, fieldName); if (std::get<2>(expressionSpec)) { // Replace the expression with an inclusion projected field. - auto it = _projectedFields.insert(_projectedFields.end(), fieldName); - _projectedFieldsSet.insert(StringData(*it)); + _projectedFields.insert(fieldName); _expressions.erase(fieldName); // Only computed projections at the beginning of the list were marked to become // projected fields. The new projected field is at the beginning of the @@ -253,40 +197,35 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInAddFields( return {BSONObj{}, false}; } - boost::optional<std::vector<OrderedPathSet>> topLevelDeps = - getTopLevelDeps(_orderToProcessAdditionsAndChildren, _expressions, _children); - - // If one of the expression requires the whole document, then we should not extract the - // projection and topLevelDeps will not hold any field names. - if (!topLevelDeps) { - return {BSONObj{}, false}; - } + DepsTracker allDeps; + reportDependencies(&allDeps); // 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 (size_t i = 0; i < _orderToProcessAdditionsAndChildren.size(); i++) { - auto&& field = _orderToProcessAdditionsAndChildren[i]; + for (auto&& field : _orderToProcessAdditionsAndChildren) { // Do not extract for pushdown computed projection with reserved name. if (reservedNames.count(field) > 0) { break; } - - auto expressionIt = _expressions.find(field); - if (expressionIt == _expressions.end()) { + 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; } - - // 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.get(), field, i)) { + 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"); - auto& topLevelFieldNames = topLevelDeps.get()[i]; if (topLevelFieldNames.size() == 1 && topLevelFieldNames.count(oldName.toString()) == 1) { // Substitute newName for oldName in the expression. StringMap<std::string> renames; @@ -303,7 +242,7 @@ std::pair<BSONObj, bool> InclusionNode::extractComputedProjectionsInAddFields( for (const auto& expressionSpec : addFieldsExpressions) { auto&& fieldName = expressionSpec.first.toString(); auto expr = expressionSpec.second; - expr->serialize().addToBsonObj(&bb, fieldName); + expr->serialize(false).addToBsonObj(&bb, fieldName); // Remove the expression from this inclusion node. _expressions.erase(fieldName); diff --git a/src/mongo/db/exec/inclusion_projection_executor.h b/src/mongo/db/exec/inclusion_projection_executor.h index 7f505f54797..a0429ff924f 100644 --- a/src/mongo/db/exec/inclusion_projection_executor.h +++ b/src/mongo/db/exec/inclusion_projection_executor.h @@ -133,9 +133,6 @@ protected: Value transformSkippedValueForOutput(const Value& value) const final { return Value(); } - bool isIncluded() const final { - return true; - } }; /** @@ -173,24 +170,19 @@ private: */ class InclusionProjectionExecutor : public ProjectionExecutor { public: - InclusionProjectionExecutor( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies, - std::unique_ptr<InclusionNode> root, - boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none) - : ProjectionExecutor(expCtx, policies, proj), _root(std::move(root)) {} - - InclusionProjectionExecutor( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies, - bool allowFastPath = false, - boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none) + InclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies, + std::unique_ptr<InclusionNode> root) + : ProjectionExecutor(expCtx, policies), _root(std::move(root)) {} + + InclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies, + bool allowFastPath = false) : InclusionProjectionExecutor( expCtx, policies, allowFastPath ? std::make_unique<FastPathEligibleInclusionNode>(policies) - : std::make_unique<InclusionNode>(policies), - proj) {} + : std::make_unique<InclusionNode>(policies)) {} TransformerType getType() const final { return TransformerType::kInclusionProjection; @@ -207,17 +199,16 @@ public: /** * Serialize the projection. */ - Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options = {}) const final { + Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument output; // The InclusionNode tree in '_root' will always have a top-level _id node if _id is to be // included. If the _id node is not present, then explicitly set {_id: false} to avoid // ambiguity in the expected behavior of the serialized projection. - _root->serialize(explain, &output, options); - auto idFieldName = options.serializeFieldPath("_id"); - if (output.peek()[idFieldName].missing()) { - output.addField(idFieldName, Value{false}); + _root->serialize(explain, &output); + if (output.peek()["_id"].missing()) { + output.addField("_id", Value{false}); } return output.freeze(); @@ -246,10 +237,10 @@ public: return {DocumentSource::GetModPathsReturn::Type::kAllPaths, {}, {}}; } - OrderedPathSet preservedPaths; + std::set<std::string> preservedPaths; _root->reportProjectedPaths(&preservedPaths); - OrderedPathSet computedPaths; + std::set<std::string> 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 b31899043ca..84ed31a9bb4 100644 --- a/src/mongo/db/exec/inclusion_projection_executor_test.cpp +++ b/src/mongo/db/exec/inclusion_projection_executor_test.cpp @@ -814,20 +814,18 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}}, " - "m: {$meta: 'vectorSearchScore'}")); + "l: {$meta: 'searchScoreDetails'}}")); DepsTracker deps; inclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 2UL); - // 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). + // 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). 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]); @@ -849,8 +847,7 @@ TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ShouldEvaluateMeta "i: {$meta: 'recordId'}, " "j: {$meta: 'indexKey'}, " "k: {$meta: 'sortKey'}, " - "l: {$meta: 'searchScoreDetails'}, " - "m: {$meta: 'vectorSearchScore'}}")); + "l: {$meta: 'searchScoreDetails'}}")); MutableDocument inputDocBuilder(Document{{"a", 1}}); inputDocBuilder.metadata().setTextScore(0.0); @@ -864,7 +861,6 @@ 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); @@ -872,7 +868,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'}, m: 9.0}")}); + "l: {scoreDetails: 'foo'}}")}); } // @@ -1094,62 +1090,6 @@ 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/index_scan.cpp b/src/mongo/db/exec/index_scan.cpp index c61fc452ec7..06399c4e33b 100644 --- a/src/mongo/db/exec/index_scan.cpp +++ b/src/mongo/db/exec/index_scan.cpp @@ -280,7 +280,9 @@ std::unique_ptr<PlanStageStats> IndexScan::getStats() { // Add a BSON representation of the filter to the stats tree, if there is one. if (nullptr != _filter) { - _commonStats.filter = _filter->serialize(); + BSONObjBuilder bob; + _filter->serialize(&bob); + _commonStats.filter = bob.obj(); } // These specific stats fields never change. diff --git a/src/mongo/db/exec/multi_plan.cpp b/src/mongo/db/exec/multi_plan.cpp index 00254cef133..def29719b63 100644 --- a/src/mongo/db/exec/multi_plan.cpp +++ b/src/mongo/db/exec/multi_plan.cpp @@ -190,13 +190,14 @@ PlanStage::StageState MultiPlanStage::doWork(WorkingSetID* out) { .getPlanCache() ->remove(plan_cache_key_factory::make<PlanCacheKey>(*_query, collection())); - switchToBackupPlan(); + _bestPlanIdx = _backupPlanIdx; + _backupPlanIdx = kNoSuchPlan; return _candidates[_bestPlanIdx].root->work(out); } if (hasBackupPlan() && PlanStage::ADVANCED == state) { LOGV2_DEBUG(20589, 5, "Best plan had a blocking stage, became unblocked"); - removeBackupPlan(); + _backupPlanIdx = kNoSuchPlan; } return state; @@ -293,7 +294,6 @@ Status MultiPlanStage::pickBestPlan(PlanYieldPolicy* yieldPolicy) { plan_cache_util::updatePlanCache( expCtx()->opCtx, collection(), _cachingMode, *_query, std::move(ranking), _candidates); - removeRejectedPlans(); return Status::OK(); } @@ -364,54 +364,6 @@ 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; } @@ -446,9 +398,6 @@ 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 12ab61659db..14d8ea413af 100644 --- a/src/mongo/db/exec/multi_plan.h +++ b/src/mongo/db/exec/multi_plan.h @@ -75,6 +75,7 @@ public: std::unique_ptr<PlanStageStats> getStats() final; + const SpecificStats* getSpecificStats() const final; boost::optional<double> getCandidateScore(size_t candidateIdx) const; @@ -162,19 +163,6 @@ 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. @@ -190,9 +178,6 @@ 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/or.cpp b/src/mongo/db/exec/or.cpp index 078765ffc84..ec0d680ac37 100644 --- a/src/mongo/db/exec/or.cpp +++ b/src/mongo/db/exec/or.cpp @@ -122,8 +122,10 @@ unique_ptr<PlanStageStats> OrStage::getStats() { _commonStats.isEOF = isEOF(); // Add a BSON representation of the filter to the stats tree, if there is one. - if (_filter) { - _commonStats.filter = _filter->serialize(); + if (nullptr != _filter) { + BSONObjBuilder bob; + _filter->serialize(&bob); + _commonStats.filter = bob.obj(); } unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_OR); diff --git a/src/mongo/db/exec/projection.h b/src/mongo/db/exec/projection.h index 236792ce64a..00e7fb33dbc 100644 --- a/src/mongo/db/exec/projection.h +++ b/src/mongo/db/exec/projection.h @@ -33,7 +33,6 @@ #include "mongo/db/exec/projection_executor.h" #include "mongo/db/jsobj.h" #include "mongo/db/matcher/expression.h" -#include "mongo/db/query/projection.h" #include "mongo/db/query/projection_ast.h" #include "mongo/db/record_id.h" diff --git a/src/mongo/db/exec/projection_executor.h b/src/mongo/db/exec/projection_executor.h index 449ef8c3a7b..ca8e0d3990c 100644 --- a/src/mongo/db/exec/projection_executor.h +++ b/src/mongo/db/exec/projection_executor.h @@ -38,7 +38,6 @@ #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/field_path.h" #include "mongo/db/pipeline/transformer_interface.h" -#include "mongo/db/query/projection_ast.h" #include "mongo/db/query/projection_policies.h" namespace mongo::projection_executor { @@ -98,20 +97,10 @@ public: */ virtual boost::optional<std::set<FieldRef>> extractExhaustivePaths() const = 0; - /** - * The query shape is made by serializing the first parsed representation of the query, which in - * the case of $project queries is a projection_ast::Projection. The ProjectionExecutor, holds - * onto the root node of the AST for only $project queries, so that the first parsed - * representation is accessible at serialization. - */ - boost::optional<projection_ast::ProjectionPathASTNode> projection = boost::none; - protected: ProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies, - boost::optional<projection_ast::ProjectionPathASTNode> proj = boost::none) - : projection(proj), - _expCtx(expCtx), + ProjectionPolicies policies) + : _expCtx(expCtx), _policies(policies), _projectionPostImageVarId{ _expCtx->variablesParseState.defineVariable(kProjectionPostImageVarName)} {} diff --git a/src/mongo/db/exec/projection_executor_builder.cpp b/src/mongo/db/exec/projection_executor_builder.cpp index f5e94be3f78..1b712685ea9 100644 --- a/src/mongo/db/exec/projection_executor_builder.cpp +++ b/src/mongo/db/exec/projection_executor_builder.cpp @@ -251,7 +251,7 @@ auto buildProjectionExecutor(boost::intrusive_ptr<ExpressionContext> expCtx, const ProjectionPolicies policies, const BuilderParamsBitSet params) { ProjectionExecutorVisitorContext<Executor> context{ - {std::make_unique<Executor>(expCtx, policies, params[kAllowFastPath], *root), expCtx}}; + {std::make_unique<Executor>(expCtx, policies, params[kAllowFastPath]), expCtx}}; ProjectionExecutorVisitor<Executor> executorVisitor{&context}; projection_ast::PathTrackingWalker walker{&context, {&executorVisitor}, {}}; tree_walker::walk<true, projection_ast::ASTNode>(root, &walker); diff --git a/src/mongo/db/exec/projection_executor_builder.h b/src/mongo/db/exec/projection_executor_builder.h index 12f683a1cba..476f2b63a25 100644 --- a/src/mongo/db/exec/projection_executor_builder.h +++ b/src/mongo/db/exec/projection_executor_builder.h @@ -32,7 +32,6 @@ #include <bitset> #include "mongo/db/exec/projection_executor.h" -#include "mongo/db/query/projection.h" #include "mongo/db/query/projection_ast.h" namespace mongo::projection_executor { diff --git a/src/mongo/db/exec/projection_executor_redaction_test.cpp b/src/mongo/db/exec/projection_executor_redaction_test.cpp deleted file mode 100644 index 70eb59855bb..00000000000 --- a/src/mongo/db/exec/projection_executor_redaction_test.cpp +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Copyright (C) 2023-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#include "document_value/document_value_test_util.h" -#include "mongo/db/exec/projection_executor.h" -#include "mongo/db/exec/projection_executor_builder.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/db/query/projection_ast_util.h" -#include "mongo/db/query/projection_parser.h" -#include "mongo/db/query/projection_policies.h" -#include "mongo/db/query/query_shape/serialization_options.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace { -std::unique_ptr<projection_executor::ProjectionExecutor> compileProjection(BSONObj proj) { - auto expCtx = make_intrusive<ExpressionContextForTest>(); - auto policies = ProjectionPolicies::findProjectionPolicies(); - auto ast = projection_ast::parseAndAnalyze(expCtx, proj, policies); - return projection_executor::buildProjectionExecutor( - expCtx, &ast, policies, projection_executor::kDefaultBuilderParams); -} -std::unique_ptr<projection_executor::ProjectionExecutor> compileProjection(BSONObj proj, - BSONObj query) { - auto expCtx = make_intrusive<ExpressionContextForTest>(); - auto match = uassertStatusOK(MatchExpressionParser::parse(query, expCtx)); - auto policies = ProjectionPolicies::findProjectionPolicies(); - auto ast = projection_ast::parseAndAnalyze(expCtx, proj, match.get(), query, policies); - auto exec = projection_executor::buildProjectionExecutor( - expCtx, &ast, policies, projection_executor::kDefaultBuilderParams); - return exec; -} - -TEST(Redaction, ProjectionTest) { - SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST; - auto redactProj = [&](std::string obj) { - return compileProjection(fromjson(obj))->serializeTransformation(boost::none, options); - }; - - /// Inclusion projections - - // Simple single inclusion - auto actual = redactProj("{a: 1}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":true})", - actual); - - actual = redactProj("{a: true}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":true})", - actual); - - // Dotted path - actual = redactProj("{\"a.b\": 1}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":{"HASH<b>":true}})", - actual); - - // Two fields - actual = redactProj("{a: 1, b: 1}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":true,"HASH<b>":true})", - actual); - - // Explicit _id: 1 - actual = redactProj("{b: 1, _id: 1}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<b>":true})", - actual); - - // Two nested fields - actual = redactProj("{\"b.d\": 1, \"b.c\": 1}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<b>":{"HASH<d>":true,"HASH<c>":true}})", - actual); - - actual = redactProj("{\"b.d\": 1, a: 1, \"b.c\": 1}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "HASH<_id>": true, - "HASH<a>": true, - "HASH<b>": { - "HASH<d>": true, - "HASH<c>": true - } - })", - actual); - - /// Exclusion projections - - // Simple single exclusion - actual = redactProj("{a: 0}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<a>":false,"HASH<_id>":true})", - actual); - - // Dotted path - actual = redactProj("{\"a.b\": 0}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<a>":{"HASH<b>":false},"HASH<_id>":true})", - actual); - - // Two fields - actual = redactProj("{a: 0, b: 0}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<a>":false,"HASH<b>":false,"HASH<_id>":true})", - actual); - - // Explicit _id: 0 - actual = redactProj("{b: 0, _id: 0}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":false,"HASH<b>":false})", - actual); - - // Two nested fields - actual = redactProj("{\"b.d\": 0, \"b.c\": 0}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<b>":{"HASH<d>":false,"HASH<c>":false},"HASH<_id>":true})", - actual); - - actual = redactProj("{\"b.d\": 0, a: 0, \"b.c\": 0}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "HASH<a>": false, - "HASH<b>": { - "HASH<d>": false, - "HASH<c>": false - }, - "HASH<_id>": true - })", - actual); - - /// Add fields projection - actual = redactProj("{a: \"hi\"}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":"?string"})", - actual); - - actual = redactProj("{a: '$field'}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":"$HASH<field>"})", - actual); - - // Dotted path - actual = redactProj("{\"a.b\": \"hi\"}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":{"HASH<b>":"?string"}})", - actual); - - // Two fields - actual = redactProj("{a: \"hi\", b: \"hello\"}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<a>":"?string","HASH<b>":"?string"})", - actual); - - // Explicit _id: 0 - actual = redactProj("{b: \"hi\", _id: \"hey\"}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<b>":"?string","HASH<_id>":"?string"})", - actual); - - // Two nested fields - actual = redactProj("{\"b.d\": \"hello\", \"b.c\": \"world\"}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"HASH<_id>":true,"HASH<b>":{"HASH<d>":"?string","HASH<c>":"?string"}})", - actual); - - actual = redactProj("{\"b.d\": \"hello\", a: \"world\", \"b.c\": \"mongodb\"}"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "HASH<_id>": true, - "HASH<b>": { - "HASH<d>": "?string", - "HASH<c>": "?string" - }, - "HASH<a>": "?string" - })", - actual); -} -} // namespace -} // namespace mongo diff --git a/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp b/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp index fc1e74a0c2e..bdf5dea8241 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 OrderedPathSet& stringPaths) { +std::set<FieldRef> toFieldRefs(const std::set<std::string>& 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 6b053c4593f..1ef569ecd75 100644 --- a/src/mongo/db/exec/projection_node.cpp +++ b/src/mongo/db/exec/projection_node.cpp @@ -48,8 +48,7 @@ void ProjectionNode::addProjectionForPath(const FieldPath& path) { void ProjectionNode::_addProjectionForPath(const FieldPath& path) { makeOptimizationsStale(); if (path.getPathLength() == 1) { - auto it = _projectedFields.insert(_projectedFields.end(), path.fullPath()); - _projectedFieldsSet.insert(StringData(*it)); + _projectedFields.insert(path.fullPath()); return; } // FieldPath can't be empty, so it is safe to obtain the first path component here. @@ -139,18 +138,12 @@ void ProjectionNode::applyProjections(const Document& inputDoc, MutableDocument* auto it = inputDoc.fieldIterator(); size_t projectedFields = 0; - bool isIncl = isIncluded(); - while (it.more()) { auto fieldName = it.fieldName(); - if (_projectedFieldsSet.find(fieldName) != _projectedFieldsSet.end()) { - if (isIncl) { - outputProjectedField(fieldName, it.next().second, outputDoc); - } else { - outputProjectedField(fieldName, Value(), outputDoc); - it.advance(); - } + if (_projectedFields.find(fieldName) != _projectedFields.end()) { + outputProjectedField( + fieldName, applyLeafProjectionToValue(it.next().second), outputDoc); ++projectedFields; } else if (auto childIt = _children.find(fieldName); childIt != _children.end()) { outputProjectedField( @@ -236,7 +229,7 @@ Value ProjectionNode::applyExpressionsToValue(const Document& root, Value inputV } } -void ProjectionNode::reportProjectedPaths(OrderedPathSet* projectedPaths) const { +void ProjectionNode::reportProjectedPaths(std::set<std::string>* projectedPaths) const { for (auto&& projectedField : _projectedFields) { projectedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, projectedField)); } @@ -246,7 +239,7 @@ void ProjectionNode::reportProjectedPaths(OrderedPathSet* projectedPaths) const } } -void ProjectionNode::reportComputedPaths(OrderedPathSet* computedPaths, +void ProjectionNode::reportComputedPaths(std::set<std::string>* 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 @@ -275,27 +268,25 @@ void ProjectionNode::optimize() { _maxFieldsToProject = maxFieldsToProject(); } -Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options) const { +Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument outputDoc; - serialize(explain, &outputDoc, options); + serialize(explain, &outputDoc); return outputDoc.freeze(); } void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain, - MutableDocument* output, - const SerializationOptions& options) const { + MutableDocument* output) const { // Determine the boolean value for projected fields in the explain output. - const bool projVal = isIncluded(); + const bool projVal = !applyLeafProjectionToValue(Value(true)).missing(); // Always put "_id" first if it was projected (implicitly or explicitly). - if (_projectedFieldsSet.find("_id") != _projectedFieldsSet.end()) { - output->addField(options.serializeFieldPath("_id"), Value(projVal)); + if (_projectedFields.find("_id") != _projectedFields.end()) { + output->addField("_id", Value(projVal)); } for (auto&& projectedField : _projectedFields) { if (projectedField != "_id") { - output->addField(options.serializeFieldPathFromString(projectedField), Value(projVal)); + output->addField(projectedField, Value(projVal)); } } @@ -303,14 +294,13 @@ void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explai auto childIt = _children.find(field); if (childIt != _children.end()) { MutableDocument subDoc; - childIt->second->serialize(explain, &subDoc, options); - output->addField(options.serializeFieldPathFromString(field), subDoc.freezeToValue()); + childIt->second->serialize(explain, &subDoc); + output->addField(field, subDoc.freezeToValue()); } else { invariant(_policies.computedFieldsPolicy == ComputedFieldsPolicy::kAllowComputedFields); auto expressionIt = _expressions.find(field); invariant(expressionIt != _expressions.end()); - output->addField(options.serializeFieldPathFromString(field), - expressionIt->second->serialize(options)); + output->addField(field, expressionIt->second->serialize(static_cast<bool>(explain))); } } } diff --git a/src/mongo/db/exec/projection_node.h b/src/mongo/db/exec/projection_node.h index cd744ef4cac..2a587580330 100644 --- a/src/mongo/db/exec/projection_node.h +++ b/src/mongo/db/exec/projection_node.h @@ -29,9 +29,8 @@ #pragma once -#include <list> - #include "mongo/db/exec/projection_executor.h" + #include "mongo/db/query/projection_policies.h" namespace mongo::projection_executor { @@ -103,7 +102,7 @@ public: /** * Recursively report all paths that are referenced by this projection. */ - void reportProjectedPaths(OrderedPathSet* preservedPaths) const; + void reportProjectedPaths(std::set<std::string>* preservedPaths) const; /** * Return an optional number, x, which indicates that it is safe to stop reading the document @@ -120,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(OrderedPathSet* computedPaths, + void reportComputedPaths(std::set<std::string>* computedPaths, StringMap<std::string>* renamedPaths) const; const std::string& getPath() const { @@ -129,12 +128,10 @@ public: void optimize(); - Document serialize(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options) const; + Document serialize(boost::optional<ExplainOptions::Verbosity> explain) const; void serialize(boost::optional<ExplainOptions::Verbosity> explain, - MutableDocument* output, - const SerializationOptions& options) const; + MutableDocument* output) const; protected: /** @@ -163,19 +160,9 @@ protected: // Writes the given value to the output doc, replacing the existing value of 'field' if present. virtual void outputProjectedField(StringData field, Value val, MutableDocument* outDoc) const; - // Used to determine if the node is an inclusion or exclusion node. - virtual bool isIncluded() const = 0; - StringMap<std::unique_ptr<ProjectionNode>> _children; StringMap<boost::intrusive_ptr<Expression>> _expressions; - - // List of the projected fields in the order in which they were specified. - std::list<std::string> _projectedFields; - - // Set of projected fields. Note that the _projectedFields list actually owns the strings, and - // this StringDataSet simply holds views of those strings. - StringDataSet _projectedFieldsSet; - + StringSet _projectedFields; ProjectionPolicies _policies; std::string _pathToNode; diff --git a/src/mongo/db/exec/sbe/SConscript b/src/mongo/db/exec/sbe/SConscript index f8246baf556..b99625b0833 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/index_access_method', + '$BUILD_DIR/mongo/db/index/key_generator', '$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,9 +59,8 @@ 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', @@ -90,6 +89,7 @@ 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,7 +106,6 @@ 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 7939d166a78..7da6a8e2cef 100644 --- a/src/mongo/db/exec/sbe/abt/abt_lower.cpp +++ b/src/mongo/db/exec/sbe/abt/abt_lower.cpp @@ -586,15 +586,14 @@ std::unique_ptr<sbe::PlanStage> SBENodeLowering::walk(const GroupByNode& n, auto& names = binderAgg->names(); auto& exprs = refsAgg->nodes(); - sbe::SlotExprPairVector aggs; - aggs.reserve(exprs.size()); + sbe::value::SlotMap<std::unique_ptr<sbe::EExpression>> aggs; 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.push_back({slot, std::move(expr)}); + aggs.emplace(slot, std::move(expr)); } // TODO: use collator slot. @@ -610,11 +609,6 @@ 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); } @@ -1011,7 +1005,6 @@ 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 bc5121407f9..72d9820d760 100644 --- a/src/mongo/db/exec/sbe/expression_test_base.h +++ b/src/mongo/db/exec/sbe/expression_test_base.h @@ -69,24 +69,6 @@ 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 65421e8b373..3a2b20a4657 100644 --- a/src/mongo/db/exec/sbe/expressions/expression.cpp +++ b/src/mongo/db/exec/sbe/expressions/expression.cpp @@ -429,8 +429,6 @@ 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", @@ -438,8 +436,6 @@ 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", @@ -471,10 +467,7 @@ 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{kAnyNumberOfArgs, vm::Builtin::concat, false}}, - {"concatArrays", BuiltinFn{kAnyNumberOfArgs, vm::Builtin::concatArrays, false}}, - {"aggConcatArraysCapped", - BuiltinFn{[](size_t n) { return n == 2; }, vm::Builtin::aggConcatArraysCapped, true}}, + {"concat", BuiltinFn{[](size_t n) { return n > 0; }, vm::Builtin::concat, false}}, {"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", @@ -493,11 +486,6 @@ 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 d5a2c0fdf0e..2b9032f257b 100644 --- a/src/mongo/db/exec/sbe/expressions/expression.h +++ b/src/mongo/db/exec/sbe/expressions/expression.h @@ -348,8 +348,6 @@ 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)...); @@ -366,30 +364,20 @@ inline auto makeEs(Ts&&... pack) { namespace detail { // base case -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)}); - } +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)); } // recursive case -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)...); +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)...); } } // namespace detail @@ -398,7 +386,7 @@ auto makeEM(Ts&&... pack) { value::SlotMap<std::unique_ptr<EExpression>> result; if constexpr (sizeof...(pack) > 0) { result.reserve(sizeof...(Ts) / 2); - detail::makeSlotExprPairHelper(result, std::forward<Ts>(pack)...); + detail::makeEM_unwind(result, std::forward<Ts>(pack)...); } return result; } @@ -411,16 +399,6 @@ 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 279f7a287b4..30eb61a7b2d 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,7 +28,6 @@ */ #include "mongo/db/exec/sbe/expression_test_base.h" -#include "mongo/db/query/sbe_stage_builder_helpers.h" namespace mongo::sbe { @@ -94,35 +93,6 @@ 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 89244fd4589..87fc8dbd1f2 100644 --- a/src/mongo/db/exec/sbe/sbe_hash_agg_test.cpp +++ b/src/mongo/db/exec/sbe/sbe_hash_agg_test.cpp @@ -69,23 +69,18 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), + makeEM(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)); @@ -171,21 +166,20 @@ TEST_F(HashAggStageTest, HashAggMinMaxTest) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(), - 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))), + 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))), makeSV(), true, boost::none, false /* allowDiskUse */, - makeSlotExprPairVec() /* mergingExprs */, kEmptyPlanNodeId); auto outSlot = generateSlotId(); @@ -236,15 +230,13 @@ TEST_F(HashAggStageTest, HashAggAddToSetTest) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(), - makeSlotExprPairVec(hashAggSlot, - stage_builder::makeFunction("collAddToSet", - std::move(collExpr), - makeE<EVariable>(scanSlot))), + makeEM(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)); @@ -335,16 +327,14 @@ TEST_F(HashAggStageTest, HashAggSeekKeysTest) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), + makeEM(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)); @@ -397,21 +387,17 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeEM(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. @@ -434,7 +420,6 @@ 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(); @@ -443,6 +428,7 @@ 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); @@ -460,21 +446,17 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeEM(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. @@ -497,14 +479,7 @@ TEST_F(HashAggStageTest, HashAggBasicCountSpill) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - // 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); + ASSERT_EQ(results.size() - expectedRowsToFitInMemory, stats->spilledRecords); stage->close(); } @@ -538,21 +513,17 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeEM(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. @@ -575,7 +546,6 @@ 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(); @@ -584,6 +554,7 @@ 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); @@ -601,21 +572,17 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeEM(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. @@ -638,14 +605,7 @@ TEST_F(HashAggStageTest, HashAggBasicCountSpillDouble) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - // 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); + ASSERT_EQ(results.size() - expectedRowsToFitInMemory, stats->spilledRecords); stage->close(); } @@ -667,21 +627,17 @@ 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(), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeEM(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. @@ -702,7 +658,6 @@ 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(); @@ -711,6 +666,7 @@ 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); @@ -729,27 +685,19 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1))), - sumsSlot, - stage_builder::makeFunction("sum", makeE<EVariable>(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))), 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. @@ -779,10 +727,7 @@ 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(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); + ASSERT_EQ(results.size() - expectedRowsToFitInMemory, stats->spilledRecords); stage->close(); } @@ -807,27 +752,19 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1))), - sumsSlot, - stage_builder::makeFunction("sum", makeE<EVariable>(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))), 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. @@ -857,9 +794,7 @@ TEST_F(HashAggStageTest, HashAggMultipleAccSpillAllToDisk) { // Check that the spilling behavior matches the expected. auto stats = static_cast<const HashAggStats*>(stage->getSpecificStats()); ASSERT_TRUE(stats->usedDisk); - // We expect each incoming value to result in a spill of a single record. - ASSERT_EQ(stats->numSpills, 9); - ASSERT_EQ(stats->spilledRecords, 9); + ASSERT_EQ(results.size(), stats->spilledRecords); stage->close(); } @@ -896,18 +831,14 @@ 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), - makeSlotExprPairVec(sumsSlot, - stage_builder::makeFunction("sum", makeE<EVariable>(scanSlot))), + makeEM(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. @@ -941,21 +872,17 @@ 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), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), + makeEM(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 f62b8a6229c..191f0ffe562 100644 --- a/src/mongo/db/exec/sbe/sbe_plan_size_test.cpp +++ b/src/mongo/db/exec/sbe/sbe_plan_size_test.cpp @@ -139,12 +139,11 @@ TEST_F(PlanSizeTest, Filter) { TEST_F(PlanSizeTest, HashAgg) { auto stage = makeS<HashAggStage>(mockS(), mockSV(), - makeSlotExprPairVec(generateSlotId(), mockE()), + makeEM(generateSlotId(), mockE()), makeSV(), true, generateSlotId(), false, - makeSlotExprPairVec(), kEmptyPlanNodeId); assertPlanSize(*stage); } @@ -169,7 +168,6 @@ TEST_F(PlanSizeTest, IndexScan) { generateSlotId(), generateSlotId(), generateSlotId(), - generateSlotId(), IndexKeysInclusionSet(1), mockSV(), generateSlotId(), diff --git a/src/mongo/db/exec/sbe/sbe_plan_stage_test.cpp b/src/mongo/db/exec/sbe/sbe_plan_stage_test.cpp index 4f3e6d44372..77b1997bdee 100644 --- a/src/mongo/db/exec/sbe/sbe_plan_stage_test.cpp +++ b/src/mongo/db/exec/sbe/sbe_plan_stage_test.cpp @@ -66,12 +66,7 @@ PlanStageTestFixture::generateVirtualScanMulti(int32_t numSlots, const BSONArray } void PlanStageTestFixture::prepareTree(CompileCtx* ctx, PlanStage* root) { - // We want to avoid recursive locking since this results in yield plans that don't yield when - // they should. - boost::optional<Lock::GlobalLock> globalLock; - if (!opCtx()->lockState()->isLocked()) { - globalLock.emplace(opCtx(), MODE_IS); - } + Lock::GlobalLock globalLock{opCtx(), MODE_IS}; root->attachToOperationContext(opCtx()); root->prepare(*ctx); root->open(false); 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 57d3ff4e05f..54fadbe1f15 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,16 +141,14 @@ TEST_F(TrialRunTrackerTest, TrialEndsDuringOpenPhaseOfBlockingStage) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), - makeSV(), /* Seek slot */ + makeEM(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}); @@ -212,16 +210,14 @@ TEST_F(TrialRunTrackerTest, OnlyDeepestNestedBlockingStageHasTrialRunTracker) { auto hashAggStage = makeS<HashAggStage>( std::move(unionStage), makeSV(unionSlot), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction( - "sum", - makeE<EConstant>(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(1)))), - makeSV(), /* Seek slot */ + makeEM(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); @@ -281,16 +277,14 @@ TEST_F(TrialRunTrackerTest, SiblingBlockingStagesBothGetTrialRunTracker) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), - makeSV(), /* Seek slot */ + makeEM(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)); @@ -413,16 +407,14 @@ TEST_F(TrialRunTrackerTest, DisablingTrackingForAChildStagePreventsEarlyExit) { auto hashAggStage = makeS<HashAggStage>( std::move(scanStage), makeSV(scanSlot), - makeSlotExprPairVec( - countsSlot, - stage_builder::makeFunction("sum", - makeE<EConstant>(value::TypeTags::NumberInt64, - value::bitcastFrom<int64_t>(1)))), - makeSV(), /* Seek slot */ + makeEM(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 bbb92328331..fb6684eea52 100644 --- a/src/mongo/db/exec/sbe/size_estimator.h +++ b/src/mongo/db/exec/sbe/size_estimator.h @@ -92,11 +92,6 @@ 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 1e8809f0858..bec12b12ee2 100644 --- a/src/mongo/db/exec/sbe/stages/branch.cpp +++ b/src/mongo/db/exec/sbe/stages/branch.cpp @@ -70,29 +70,31 @@ 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) { - auto slot = _outputVals[idx]; - auto [_, inserted] = dupCheck.insert(slot); - uassert(4822831, str::stream() << "duplicate field: " << slot, inserted); - } + std::vector<value::SlotAccessor*> accessors; + accessors.reserve(2); - for (size_t idx = 0; idx < _outputVals.size(); ++idx) { - auto thenSlot = _inputThenVals[idx]; - auto elseSlot = _inputElseVals[idx]; + { + auto slot = _inputThenVals[idx]; + auto [it, inserted] = dupCheck.insert(slot); + uassert(4822829, 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[0]->getAccessor(ctx, slot)); + } + { + auto slot = _inputElseVals[idx]; + auto [it, inserted] = dupCheck.insert(slot); + uassert(4822830, 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)); + 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); - _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 7471763a44a..4116f2980ff 100644 --- a/src/mongo/db/exec/sbe/stages/collection_helpers.h +++ b/src/mongo/db/exec/sbe/stages/collection_helpers.h @@ -40,7 +40,6 @@ 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 2514dedf1b0..14f40afeaa9 100644 --- a/src/mongo/db/exec/sbe/stages/hash_agg.cpp +++ b/src/mongo/db/exec/sbe/stages/hash_agg.cpp @@ -27,67 +27,47 @@ * it in the license file. */ -#include "mongo/db/exec/sbe/stages/hash_agg.h" +#include "mongo/platform/basic.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/exec/sbe/size_estimator.h" +#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/exec/sbe/stages/hash_agg.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, - SlotExprPairVector aggs, + value::SlotMap<std::unique_ptr<EExpression>> aggs, value::SlotVector seekKeysSlots, bool optimizedClose, boost::optional<value::SlotId> collatorSlot, bool allowDiskUse, - SlotExprPairVector mergingExprs, - PlanNodeId planNodeId, - bool forceIncreasedSpilling) + PlanNodeId planNodeId) : PlanStage("group"_sd, planNodeId), _gbs(std::move(gbs)), _aggs(std::move(aggs)), _collatorSlot(collatorSlot), _allowDiskUse(allowDiskUse), _seekKeysSlots(std::move(seekKeysSlots)), - _optimizedClose(optimizedClose), - _mergingExprs(std::move(mergingExprs)), - _forceIncreasedSpilling(forceIncreasedSpilling) { + _optimizedClose(optimizedClose) { _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 { - SlotExprPairVector aggs; - aggs.reserve(_aggs.size()); + value::SlotMap<std::unique_ptr<EExpression>> aggs; for (auto& [k, v] : _aggs) { - aggs.push_back({k, v->clone()}); + aggs.emplace(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), @@ -95,34 +75,24 @@ std::unique_ptr<PlanStage> HashAggStage::clone() const { _optimizedClose, _collatorSlot, _allowDiskUse, - std::move(mergingExprsClone), - _commonStats.nodeId, - _forceIncreasedSpilling); + _commonStats.nodeId); } void HashAggStage::doSaveState(bool relinquishCursor) { if (relinquishCursor) { if (_rsCursor) { - _recordStore->saveCursor(_opCtx, _rsCursor); + _rsCursor->save(); } } if (_rsCursor) { _rsCursor->setSaveStorageCursorOnDetachFromOperationContext(!relinquishCursor); } - - if (_recordStore) { - _recordStore->saveState(); - } } void HashAggStage::doRestoreState(bool relinquishCursor) { invariant(_opCtx); - if (_recordStore) { - _recordStore->restoreState(); - } - if (_rsCursor && relinquishCursor) { - auto couldRestore = _recordStore->restoreCursor(_opCtx, _rsCursor); + auto couldRestore = _rsCursor->restore(); uassert(6196500, "HashAggStage could not restore cursor", couldRestore); } } @@ -150,36 +120,34 @@ 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) { - throwIfDupSlot(slot); + auto [it, inserted] = dupCheck.emplace(slot); + uassert(4822827, str::stream() << "duplicate field: " << slot, inserted); _inKeyAccessors.emplace_back(_children[0]->getAccessor(ctx, slot)); - // Construct accessors for obtaining the key values from either the hash table '_ht' or the - // '_recordStore'. + // 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. _outHashKeyAccessors.emplace_back(std::make_unique<HashKeyAccessor>(_htIt, counter)); _outRecordStoreKeyAccessors.emplace_back( - std::make_unique<value::MaterializedSingleRowAccessor>(_outKeyRowRecordStore, counter)); + std::make_unique<value::MaterializedSingleRowAccessor>(_aggKeyRecordStore, counter)); + + 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(). 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. + // 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'. _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 @@ -190,18 +158,26 @@ void HashAggStage::prepare(CompileCtx& ctx) { counter = 0; for (auto& [slot, expr] : _aggs) { - 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'. + 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. _outRecordStoreAggAccessors.emplace_back( - std::make_unique<value::MaterializedSingleRowAccessor>(_outAggRowRecordStore, counter)); + std::make_unique<value::MaterializedSingleRowAccessor>(_aggValueRecordStore, counter)); _outHashAggAccessors.emplace_back(std::make_unique<HashAggAccessor>(_htIt, counter)); - - // 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. + 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. _outAggAccessors.emplace_back( std::make_unique<value::SwitchAccessor>(std::vector<value::SlotAccessor*>{ _outHashAggAccessors.back().get(), _outRecordStoreAggAccessors.back().get()})); @@ -211,32 +187,10 @@ 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; } @@ -246,15 +200,6 @@ 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); } @@ -270,117 +215,99 @@ void HashAggStage::makeTemporaryRecordStore() { "No storage engine so HashAggStage cannot spill to disk", _opCtx->getServiceContext()->getStorageEngine()); assertIgnorePrepareConflictsBehavior(_opCtx); - _recordStore = std::make_unique<SpillingStore>(_opCtx); + _recordStore = _opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore( + _opCtx, KeyFormat::String); _specificStats.usedDisk = true; } void HashAggStage::spillRowToDisk(const value::MaterializedRow& key, const value::MaterializedRow& val) { - CollatorInterface* collator = nullptr; - if (_collatorAccessor) { - auto [colTag, colVal] = _collatorAccessor->getViewOfValue(); - collator = value::getCollatorView(colVal); - } - KeyString::Builder kb{KeyString::Version::kLatestVersion}; - // Serialize the key that will be used as the record id (rid) when storing the record in the - // record store. Use a keystring for the spilled entry's rid such that partial aggregates are - // guaranteed to have identical keystrings when their keys are equal with respect to the - // collation. - key.serializeIntoKeyString(kb, collator); - // 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 rid = RecordId(kb.getBuffer(), kb.getSize()); + key.serializeIntoKeyString(kb); + auto typeBits = kb.getTypeBits(); - if (collator) { - // The keystring cannot always be deserialized back to the original keys when a collation is - // in use, so we also store the unmodified key in the data part of the spilled record. - _recordStore->upsertToRecordStore(_opCtx, rid, key, val, false /*update*/); - } else { - auto typeBits = kb.getTypeBits(); - _recordStore->upsertToRecordStore(_opCtx, rid, val, typeBits, false /*update*/); - } + 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); - _specificStats.spilledRecords++; + spillValueToDisk(rid, val, typeBits, false /*update*/); } -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(); - } - - for (auto&& it : *_ht) { - spillRowToDisk(it.first, it.second); +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++; } - - 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; + _specificStats.lastSpilledRecordSize = nBytes; } // 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. +// 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. void HashAggStage::checkMemoryUsageAndSpillIfNecessary(MemoryCheckData& mcd) { - invariant(!_ht->empty()); + // 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; + } // If the group-by key is empty we will only ever aggregate into a single row so no sense in - // spilling. + // spilling since we will just be moving a single row back and forth from disk to main memory. if (_inKeyAccessors.size() == 0) { return; } mcd.memoryCheckpointCounter++; - 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. + 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; 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 - : mcd.nextMemoryCheckpoint * 2; - + : (estimatedGainPerChildAdvance < -0.1) ? mcd.atMostCheckFrequency + : mcd.nextMemoryCheckpoint * 2; mcd.nextMemoryCheckpoint = std::min<long>(mcd.memoryCheckFrequency, std::max<long>(mcd.atMostCheckFrequency, nextCheckpointCandidate)); @@ -406,30 +333,17 @@ 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); - _keyEq = value::MaterializedRowEq(collatorView); - _ht.emplace(0, hasher, _keyEq); + const value::MaterializedRowEq equator(collatorView); + _ht.emplace(0, hasher, equator); } else { _ht.emplace(); } _seekKeys.resize(_seekKeysAccessors.size()); - // Reset state since this stage may have been previously opened. - for (auto&& accessor : _outKeyAccessors) { - accessor->setIndex(0); - } - for (auto&& accessor : _outAggAccessors) { - accessor->setIndex(0); - } - if (_recordStore) { - _recordStore->resetCursor(_opCtx, _rsCursor); - } - _recordStore.reset(); - _outKeyRowRecordStore = {0}; - _outAggRowRecordStore = {0}; - _spilledAggRow = {0}; - _stashedNextRow = {0, 0}; - + // A default value for spilling a key to the record store. + value::MaterializedRow defaultVal{_outAggAccessors.size()}; + bool updateAggStateHt = false; MemoryCheckData memoryCheckData; while (_children[0]->getNext() == PlanState::ADVANCED) { @@ -441,35 +355,57 @@ void HashAggStage::open(bool reOpen) { key.reset(idx++, false, tag, val); } - 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; + + 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. key.makeOwned(); auto [it, _] = _ht->emplace(std::move(key), value::MaterializedRow{0}); + // Initialize accumulators. it->second.resize(_outAggAccessors.size()); _htIt = it; } - - // 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); - } - - if (_forceIncreasedSpilling && !newKey) { - // If configured to spill more than usual, we spill after seeing the same key twice. - spill(memoryCheckData); + 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 { - // 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); + // 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); } + // Estimates how much memory is being used and might start spilling. + 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 // stage, like this one. The blocking stage tracks the number of documents it has @@ -485,34 +421,9 @@ 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->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. size_t idx = 0; @@ -523,103 +434,20 @@ 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, {nullptr /*collator*/}); - 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)}; -} - -HashAggStage::SpilledRow HashAggStage::deserializeSpilledRecord(const Record& record, - const CollatorInterface& collator) { - BufReader valReader(record.data.data(), record.data.size()); - // When a collator has been defined, both the key and the value are stored in the data part of - // the record. First read the key and then read the value. - auto key = value::MaterializedRow::deserializeForSorter(valReader, {&collator}); - auto val = value::MaterializedRow::deserializeForSorter(valReader, {&collator}); - return {std::move(key), std::move(val)}; -} - -PlanState HashAggStage::getNextSpilled() { - CollatorInterface* collator = nullptr; - if (_collatorAccessor) { - auto [colTag, colVal] = _collatorAccessor->getViewOfValue(); - collator = value::getCollatorView(colVal); - } - - // Use the appropriate method to deserialize the record based on whether a collator is used. - auto recoverSpilledRecord = - [this](const Record& record, BufBuilder& keyBuffer, const CollatorInterface* collator) { - if (collator) { - return deserializeSpilledRecord(record, *collator); - } - return deserializeSpilledRecord(record, keyBuffer); - }; - - 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 = recoverSpilledRecord(*nextRecord, _outKeyRowRSBuffer, collator); - - _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}; - } - - // 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 = recoverSpilledRecord(*nextRecord, _stashedKeyBuffer, collator); - 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); - } + // 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); } - - return trackPlanState(PlanState::ADVANCED); + _drainingRecordStore = false; } PlanState HashAggStage::getNext() { auto optTimer(getOptTimer(_opCtx)); - // 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 (_htIt == _ht->end() && !_drainingRecordStore) { + // First invocation of getNext() after open() when not draining the '_recordStore'. if (!_seekKeysAccessors.empty()) { _htIt = _ht->find(_seekKeys); } else { @@ -628,13 +456,53 @@ PlanState HashAggStage::getNext() { } else if (!_seekKeysAccessors.empty()) { // Subsequent invocation with seek keys. Return only 1 single row (if any). _htIt = _ht->end(); - } else { + } else if (!_drainingRecordStore) { + // Returning the results of the entire hash table first before draining the '_recordStore'. ++_htIt; } - if (_htIt == _ht->end()) { - // The hash table has been drained (and we never spilled to disk) so we're done. + if (_htIt == _ht->end() && !_recordStore) { + // The hash table has been drained and nothing was spilled to disk. 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); } @@ -654,19 +522,11 @@ 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("spilledDataStorageSize", _specificStats.spilledDataStorageSize); + bob.appendNumber("spilledBytesApprox", + _specificStats.lastSpilledRecordSize * _specificStats.spilledRecords); ret->debugInfo = bob.obj(); } @@ -684,15 +544,11 @@ void HashAggStage::close() { trackClose(); _ht = boost::none; - if (_recordStore && _opCtx) { - _recordStore->resetCursor(_opCtx, _rsCursor); + 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(); @@ -715,7 +571,7 @@ std::vector<DebugPrinter::Block> HashAggStage::debugPrint() const { ret.emplace_back(DebugPrinter::Block("[`")); bool first = true; - for (auto&& [slot, expr] : _aggs) { + value::orderedSlotMapTraverse(_aggs, [&](auto slot, auto&& expr) { if (!first) { ret.emplace_back(DebugPrinter::Block("`,")); } @@ -724,7 +580,7 @@ std::vector<DebugPrinter::Block> HashAggStage::debugPrint() const { ret.emplace_back("="); DebugPrinter::addBlocks(ret, expr->debugPrint()); first = false; - } + }); ret.emplace_back("`]"); if (!_seekKeysSlots.empty()) { @@ -739,28 +595,6 @@ 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"); } @@ -781,7 +615,6 @@ 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 91f91051363..19fbca9d1c7 100644 --- a/src/mongo/db/exec/sbe/stages/hash_agg.h +++ b/src/mongo/db/exec/sbe/stages/hash_agg.h @@ -29,9 +29,10 @@ #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/util/spilling.h" #include "mongo/db/exec/sbe/vm/vm.h" #include "mongo/db/query/query_knobs_gen.h" #include "mongo/db/storage/temporary_record_store.h" @@ -60,38 +61,21 @@ 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>]? - * spillSlots[slot_1, ..., slot_n] mergingExprs[expr_1, ..., expr_n] reopen? collatorSlot? - * childStage + * group [<group by slots>] [slot_1 = expr_1, ..., slot_n = expr_n] [<seek slots>]? reopen? + * collatorSlot? childStage */ class HashAggStage final : public PlanStage { public: HashAggStage(std::unique_ptr<PlanStage> input, value::SlotVector gbs, - SlotExprPairVector aggs, + value::SlotMap<std::unique_ptr<EExpression>> aggs, value::SlotVector seekKeysSlots, bool optimizedClose, boost::optional<value::SlotId> collatorSlot, bool allowDiskUse, - SlotExprPairVector mergingExprs, - PlanNodeId planNodeId, - bool forceIncreasedSpilling = false); + PlanNodeId planNodeId); std::unique_ptr<PlanStage> clone() const final; @@ -124,147 +108,99 @@ private: using HashKeyAccessor = value::MaterializedRowKeyAccessor<TableType::iterator>; using HashAggAccessor = value::MaterializedRowValueAccessor<TableType::iterator>; - using SpilledRow = std::pair<value::MaterializedRow, value::MaterializedRow>; + 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); /** * 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' (if it hasn't already been - * created) and spill the contents of the hash table into this record store. + * 'checkMemoryUsageAndSpillIfNecessary()' will create '_recordStore' and might spill some of + * the already accumulated data into it. */ struct MemoryCheckData { - MemoryCheckData() { - reset(); - } - - void reset() { - memoryCheckFrequency = std::min(atMostCheckFrequency, atLeastMemoryCheckFrequency); - nextMemoryCheckpoint = 0; - memoryCheckpointCounter = 0; - lastEstimatedMemoryUsage = 0; - } - const double checkpointMargin = internalQuerySBEAggMemoryUseCheckMargin.load(); - const int64_t atMostCheckFrequency = internalQuerySBEAggMemoryCheckPerAdvanceAtMost.load(); - const int64_t atLeastMemoryCheckFrequency = + const long atMostCheckFrequency = internalQuerySBEAggMemoryCheckPerAdvanceAtMost.load(); + const long 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. - int64_t memoryCheckFrequency = 1; + long memoryCheckFrequency = 1; // The number of incoming records to process before the next memory checkpoint. - int64_t nextMemoryCheckpoint = 0; + long nextMemoryCheckpoint = 0; // The counter of the incoming records between memory checkpoints. - int64_t memoryCheckpointCounter = 0; - - int64_t lastEstimatedMemoryUsage = 0; - }; + long memoryCheckpointCounter = 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); + long long lastEstimatedMemoryUsage = 0; + MemoryCheckData() { + memoryCheckFrequency = std::min(atMostCheckFrequency, atLeastMemoryCheckFrequency); + } + }; 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. - * - * This method is used when there is no collator. - */ - SpilledRow deserializeSpilledRecord(const Record& record, BufBuilder& keyBuffer); - - /** - * Given a 'record' from the record store and a 'collator', decodes it into a pair of - * materialized rows (one for the group-by key and another one for the agg value). - * Both the group-by key and the agg value are read from the data part of the record. - */ - SpilledRow deserializeSpilledRecord(const Record& record, const CollatorInterface& collator); - - PlanState getNextSpilled(); - - void makeTemporaryRecordStore(); const value::SlotVector _gbs; - const SlotExprPairVector _aggs; + const value::SlotMap<std::unique_ptr<EExpression>> _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; - // 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. + // Accessors for the key stored in '_ht', a SwitchAccessor is used so we can produce the key + // from either the '_ht' or the '_recordStore'. 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; - // 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}; + // 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; std::vector<std::unique_ptr<value::MaterializedSingleRowAccessor>> _outRecordStoreAggAccessors; - std::vector<std::unique_ptr<value::SwitchAccessor>> _outAggAccessors; + + // 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<value::SlotAccessor*> _seekKeysAccessors; value::MaterializedRow _seekKeys; - // Bytecode which gets executed to aggregate incoming rows into the hash table. + // 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; 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; @@ -276,31 +212,10 @@ 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<SpillingStore> _recordStore; + 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 02e95307c4b..0d976f1a875 100644 --- a/src/mongo/db/exec/sbe/stages/hash_lookup.cpp +++ b/src/mongo/db/exec/sbe/stages/hash_lookup.cpp @@ -187,22 +187,6 @@ value::SlotAccessor* HashLookupStage::getAccessor(CompileCtx& ctx, value::SlotId return outerChild()->getAccessor(ctx, slot); } } -void HashLookupStage::doSaveState(bool relinquishCursor) { - if (_recordStoreHt) { - _recordStoreHt->saveState(); - } - if (_recordStoreBuf) { - _recordStoreBuf->saveState(); - } -} -void HashLookupStage::doRestoreState(bool relinquishCursor) { - if (_recordStoreHt) { - _recordStoreHt->restoreState(); - } - if (_recordStoreBuf) { - _recordStoreBuf->restoreState(); - } -} void HashLookupStage::reset() { _ht = boost::none; @@ -275,7 +259,7 @@ void HashLookupStage::addHashTableEntry(value::SlotAccessor* keyAccessor, size_t auto val = std::vector<size_t>{valueIndex}; auto [tagKey, valKey] = keyAccessor->getViewOfValue(); - spillIndicesToRecordStore(_recordStoreHt.get(), tagKey, valKey, val); + spillIndicesToRecordStore(_recordStoreHt->rs(), tagKey, valKey, val); } } else { // The key is already present in '_ht' so the memory will only grow by one size_t. If we @@ -297,7 +281,7 @@ void HashLookupStage::addHashTableEntry(value::SlotAccessor* keyAccessor, size_t // Evict the hash table value. _computedTotalMemUsage -= htIt->second.size() * sizeof(size_t); htIt->second.push_back(valueIndex); - spillIndicesToRecordStore(_recordStoreHt.get(), tagKeyView, valKeyView, htIt->second); + spillIndicesToRecordStore(_recordStoreHt->rs(), tagKeyView, valKeyView, htIt->second); _ht->erase(htIt); } } @@ -313,15 +297,17 @@ void HashLookupStage::makeTemporaryRecordStore() { _opCtx->getServiceContext()->getStorageEngine()); assertIgnorePrepareConflictsBehavior(_opCtx); - _recordStoreBuf = std::make_unique<SpillingStore>(_opCtx, KeyFormat::Long); + _recordStoreBuf = _opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore( + _opCtx, KeyFormat::Long); - _recordStoreHt = std::make_unique<SpillingStore>(_opCtx, KeyFormat::String); + _recordStoreHt = _opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore( + _opCtx, KeyFormat::String); _specificStats.usedDisk = true; } void HashLookupStage::spillBufferedValueToDisk(OperationContext* opCtx, - SpillingStore* rs, + RecordStore* rs, size_t bufferIdx, const value::MaterializedRow& val) { auto rid = getValueRecordId(bufferIdx); @@ -329,7 +315,15 @@ void HashLookupStage::spillBufferedValueToDisk(OperationContext* opCtx, BufBuilder buf; val.serializeForSorter(buf); - rs->upsertToRecordStore(opCtx, rid, buf, false); + assertIgnorePrepareConflictsBehavior(opCtx); + WriteUnitOfWork wuow(opCtx); + + auto status = rs->insertRecord(opCtx, rid, buf.buf(), buf.len(), Timestamp{}); + wuow.commit(); + + tassert(6373906, + str::stream() << "Failed to write to disk because " << status.getStatus().reason(), + status.isOK()); _specificStats.spilledBuffRecords++; // Add size of record ID + size of buffer. @@ -340,14 +334,14 @@ 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 (!hasSpilledBufToDisk() && newMemUsage <= _memoryUseInBytesBeforeSpill) { + if (newMemUsage <= _memoryUseInBytesBeforeSpill) { _buffer.emplace_back(std::move(value)); _computedTotalMemUsage = newMemUsage; } else { if (!hasSpilledBufToDisk()) { makeTemporaryRecordStore(); } - spillBufferedValueToDisk(_opCtx, _recordStoreBuf.get(), bufferIndex, value); + spillBufferedValueToDisk(_opCtx, _recordStoreBuf->rs(), bufferIndex, value); } _valueId++; return bufferIndex; @@ -433,7 +427,7 @@ void HashLookupStage::accumulateFromValueIndices(const C& bufferIndices) { // We must shift the '_bufferIt' index by one when using it as a RecordId because a // RecordId of 0 is invalid. auto rid = getValueRecordId(_bufferIt); - auto rsValue = _recordStoreBuf->readFromRecordStore(_opCtx, rid); + auto rsValue = readFromRecordStore(_opCtx, _recordStoreBuf->rs(), rid); if (!rsValue) { tasserted(6373900, "bufferIdx not found in record store"); } @@ -449,7 +443,7 @@ void HashLookupStage::accumulateFromValueIndices(const C& bufferIndices) { } } -void HashLookupStage::writeIndicesToRecordStore(SpillingStore* rs, +void HashLookupStage::writeIndicesToRecordStore(RecordStore* rs, value::TypeTags tagKey, value::Value valKey, const std::vector<size_t>& value, @@ -464,7 +458,7 @@ void HashLookupStage::writeIndicesToRecordStore(SpillingStore* rs, key.reset(0, false, tagKey, valKey); auto [rid, typeBits] = serializeKeyForRecordStore(key); - rs->upsertToRecordStore(_opCtx, rid, buf, typeBits, update); + upsertToRecordStore(_opCtx, rs, rid, buf, typeBits, update); if (!update) { _specificStats.spilledHtRecords++; // Add the size of key (which comprises of the memory usage for the key + its type bits), @@ -477,7 +471,7 @@ void HashLookupStage::writeIndicesToRecordStore(SpillingStore* rs, } boost::optional<std::vector<size_t>> HashLookupStage::readIndicesFromRecordStore( - SpillingStore* rs, value::TypeTags tagKey, value::Value valKey) { + RecordStore* rs, value::TypeTags tagKey, value::Value valKey) { _probeKey.reset(0, false, tagKey, valKey); auto [rid, _] = serializeKeyForRecordStore(_probeKey); @@ -496,7 +490,7 @@ boost::optional<std::vector<size_t>> HashLookupStage::readIndicesFromRecordStore return boost::none; } -void HashLookupStage::spillIndicesToRecordStore(SpillingStore* rs, +void HashLookupStage::spillIndicesToRecordStore(RecordStore* rs, value::TypeTags tagKey, value::Value valKey, const std::vector<size_t>& value) { @@ -551,7 +545,7 @@ PlanState HashLookupStage::getNext() { normalizeStringIfCollator(tagElemView, valElemView); auto indicesFromRS = readIndicesFromRecordStore( - _recordStoreHt.get(), tagElemCollView, valElemCollView); + _recordStoreHt->rs(), tagElemCollView, valElemCollView); if (indicesFromRS) { indices.insert(indicesFromRS->begin(), indicesFromRS->end()); } @@ -573,7 +567,7 @@ PlanState HashLookupStage::getNext() { normalizeStringIfCollator(tagKeyView, valKeyView); auto indicesFromRS = readIndicesFromRecordStore( - _recordStoreHt.get(), tagKeyCollView, valKeyCollView); + _recordStoreHt->rs(), tagKeyCollView, valKeyCollView); if (indicesFromRS) { accumulateFromValueIndices(*indicesFromRS); } diff --git a/src/mongo/db/exec/sbe/stages/hash_lookup.h b/src/mongo/db/exec/sbe/stages/hash_lookup.h index b312e0a68f4..2e3f0b34816 100644 --- a/src/mongo/db/exec/sbe/stages/hash_lookup.h +++ b/src/mongo/db/exec/sbe/stages/hash_lookup.h @@ -33,7 +33,6 @@ #include "mongo/db/exec/sbe/expressions/expression.h" #include "mongo/db/exec/sbe/stages/stages.h" -#include "mongo/db/exec/sbe/util/spilling.h" #include "mongo/db/exec/sbe/vm/vm.h" #include "mongo/db/query/query_knobs_gen.h" @@ -102,10 +101,6 @@ public: std::vector<DebugPrinter::Block> debugPrint() const final; size_t estimateCompileTimeSize() const final; -protected: - void doSaveState(bool relinquishCursor) override; - void doRestoreState(bool relinquishCursor) override; - private: using HashTableType = std::unordered_map<value::MaterializedRow, // NOLINT std::vector<size_t>, @@ -124,23 +119,23 @@ private: // Spilling helpers. void addHashTableEntry(value::SlotAccessor* keyAccessor, size_t valueIndex); void spillBufferedValueToDisk(OperationContext* opCtx, - SpillingStore* rs, + RecordStore* rs, size_t bufferIdx, const value::MaterializedRow&); size_t bufferValueOrSpill(value::MaterializedRow& value); void setInnerProjectSwitchAccessor(int idx); - boost::optional<std::vector<size_t>> readIndicesFromRecordStore(SpillingStore* rs, + boost::optional<std::vector<size_t>> readIndicesFromRecordStore(RecordStore* rs, value::TypeTags tagKey, value::Value valKey); - void writeIndicesToRecordStore(SpillingStore* rs, + void writeIndicesToRecordStore(RecordStore* rs, value::TypeTags tagKey, value::Value valKey, const std::vector<size_t>& value, bool update); - void spillIndicesToRecordStore(SpillingStore* rs, + void spillIndicesToRecordStore(RecordStore* rs, value::TypeTags tagKey, value::Value valKey, const std::vector<size_t>& value); @@ -234,8 +229,8 @@ private: // rows in '_buffer'. long long _computedTotalMemUsage = 0; - std::unique_ptr<SpillingStore> _recordStoreHt; - std::unique_ptr<SpillingStore> _recordStoreBuf; + std::unique_ptr<TemporaryRecordStore> _recordStoreHt; + std::unique_ptr<TemporaryRecordStore> _recordStoreBuf; HashLookupStats _specificStats; }; diff --git a/src/mongo/db/exec/sbe/stages/ix_scan.cpp b/src/mongo/db/exec/sbe/stages/ix_scan.cpp index fe88d9c9095..bfad6d9a2ae 100644 --- a/src/mongo/db/exec/sbe/stages/ix_scan.cpp +++ b/src/mongo/db/exec/sbe/stages/ix_scan.cpp @@ -45,7 +45,6 @@ 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, @@ -59,7 +58,6 @@ IndexScanStage::IndexScanStage(UUID collUuid, _recordSlot(recordSlot), _recordIdSlot(recordIdSlot), _snapshotIdSlot(snapshotIdSlot), - _indexIdSlot(indexIdSlot), _indexKeysToInclude(indexKeysToInclude), _vars(std::move(vars)), _seekKeySlotLow(seekKeySlotLow), @@ -78,7 +76,6 @@ std::unique_ptr<PlanStage> IndexScanStage::clone() const { _recordSlot, _recordIdSlot, _snapshotIdSlot, - _indexIdSlot, _indexKeysToInclude, _vars, _seekKeySlotLow, @@ -131,17 +128,10 @@ 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) { - _latestSnapshotId = _opCtx->recoveryUnit()->getSnapshotId().toNumber(); + _snapshotIdAccessor->reset( + value::TypeTags::NumberInt64, + value::bitcastFrom<uint64_t>(_opCtx->recoveryUnit()->getSnapshotId().toNumber())); } } @@ -158,10 +148,6 @@ 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; } @@ -233,7 +219,9 @@ 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) { - _latestSnapshotId = _opCtx->recoveryUnit()->getSnapshotId().toNumber(); + _snapshotIdAccessor->reset( + value::TypeTags::NumberInt64, + value::bitcastFrom<uint64_t>(_opCtx->recoveryUnit()->getSnapshotId().toNumber())); } } @@ -397,12 +385,6 @@ 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( @@ -441,9 +423,6 @@ 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)); } @@ -492,12 +471,6 @@ 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 33fcd352d0c..ce00ef17128 100644 --- a/src/mongo/db/exec/sbe/stages/ix_scan.h +++ b/src/mongo/db/exec/sbe/stages/ix_scan.h @@ -51,8 +51,7 @@ 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, - * - 'indexIdSlot': the name of the index being read from, and + * - 'snapshotIdSlot': the storage snapshot that this index scan is reading 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 @@ -64,11 +63,10 @@ namespace mongo::sbe { * * Debug string representation: * - * ixscan recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? - * [slot_1 = fieldNo_1, ..., slot2 = fieldNo_n] + * ixscan recordSlot? recordIdSlot? snapshotIdSlot? [slot_1 = fieldNo_1, ..., slot2 = fieldNo_n] * collectionUuid indexName forward * - * ixseek lowKey highKey recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? + * ixseek lowKey highKey recordSlot? recordIdSlot? snapshotIdSlot? * [slot_1 = fieldNo_1, ..., slot2 = fieldNo_n] * collectionUuid indexName forward */ @@ -80,7 +78,6 @@ 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, @@ -128,7 +125,6 @@ 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; @@ -145,14 +141,6 @@ 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 f487c7a45d2..263a4f86660 100644 --- a/src/mongo/db/exec/sbe/stages/plan_stats.h +++ b/src/mongo/db/exec/sbe/stages/plan_stats.h @@ -278,13 +278,8 @@ 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}; - // An estimate, in bytes, of the size of the final spill table after all spill events have taken - // place. - long long spilledDataStorageSize{0}; + long long lastSpilledRecordSize{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 3d601d8779e..678d3f84ef9 100644 --- a/src/mongo/db/exec/sbe/stages/scan.cpp +++ b/src/mongo/db/exec/sbe/stages/scan.cpp @@ -209,7 +209,6 @@ void ScanStage::doSaveState(bool relinquishCursor) { cursor->setSaveStorageCursorOnDetachFromOperationContext(!relinquishCursor); } - _indexCatalogEntryMap.clear(); _coll.reset(); } @@ -392,14 +391,9 @@ PlanState ScanStage::getNext() { } // Return EOF if the index key is found to be inconsistent. - if (_scanCallbacks.indexKeyConsistencyCheckCallback && - !_scanCallbacks.indexKeyConsistencyCheckCallback(_opCtx, - _indexCatalogEntryMap, - _snapshotIdAccessor, - _indexIdAccessor, - _indexKeyAccessor, - _coll, - *nextRecord)) { + if (_scanCallbacks.indexKeyConsistencyCheckCallBack && + !_scanCallbacks.indexKeyConsistencyCheckCallBack( + _opCtx, _snapshotIdAccessor, _indexIdAccessor, _indexKeyAccessor, _coll, *nextRecord)) { return trackPlanState(PlanState::IS_EOF); } @@ -463,7 +457,6 @@ void ScanStage::close() { auto optTimer(getOptTimer(_opCtx)); trackClose(); - _indexCatalogEntryMap.clear(); _cursor.reset(); _randomCursor.reset(); _coll.reset(); @@ -749,7 +742,6 @@ void ParallelScanStage::doSaveState(bool relinquishCursor) { _cursor->save(); } - _indexCatalogEntryMap.clear(); _coll.reset(); } @@ -907,9 +899,8 @@ PlanState ParallelScanStage::getNext() { } // Return EOF if the index key is found to be inconsistent. - if (_scanCallbacks.indexKeyConsistencyCheckCallback && - !_scanCallbacks.indexKeyConsistencyCheckCallback(_opCtx, - _indexCatalogEntryMap, + if (_scanCallbacks.indexKeyConsistencyCheckCallBack && + !_scanCallbacks.indexKeyConsistencyCheckCallBack(_opCtx, _snapshotIdAccessor, _indexIdAccessor, _indexKeyAccessor, @@ -965,7 +956,6 @@ 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 6f4980f52e1..37462ac5e14 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? recordIdSlot? snapshotIdSlot? indexIdSlot? indexKeySlot? - * indexKeyPatternSlot? [slot1 = fieldName1, ... slot_n = fieldName_n] collectionUuid + * scan recordSlot|none recordIdSlot|none snapshotIdSlot|none indexIdSlot|none indexKeySlot|none + * indexKeyPatternSlot|none [slot1 = fieldName1, ... slot_n = fieldName_n] collectionUuid * forward needOplogSlotForTs * - * seek seekKeySlot recordSlot? recordIdSlot? snapshotIdSlot? indexIdSlot? indexKeySlot? - * indexKeyPatternSlot? [slot1 = fieldName1, ... slot_n = fieldName_n] + * seek seekKeySlot recordSlot|none recordIdSlot|none snapshotIdSlot|none indexIdSlot|none + * indexKeySlot|none indexKeyPatternSlot|none [slot1 = fieldName1, ... slot_n = fieldName_n] * collectionUuid forward needOplogSlotForTs */ class ScanStage final : public PlanStage { @@ -197,8 +197,6 @@ 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. @@ -313,8 +311,6 @@ 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 eebdc0c2443..5acf73afe8d 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->stats().spilledRanges(); + _specificStats.spills += _sorter->numSpills(); _specificStats.keysSorted += _sorter->numSorted(); auto& metricsCollector = ResourceConsumption::MetricsCollector::get(_opCtx); metricsCollector.incrementKeysSorted(_sorter->numSorted()); - metricsCollector.incrementSorterSpills(_sorter->stats().spilledRanges()); + metricsCollector.incrementSorterSpills(_sorter->numSpills()); _children[0]->close(); } diff --git a/src/mongo/db/exec/sbe/stages/union.cpp b/src/mongo/db/exec/sbe/stages/union.cpp index 3ddadb8c912..a661e6c579f 100644 --- a/src/mongo/db/exec/sbe/stages/union.cpp +++ b/src/mongo/db/exec/sbe/stages/union.cpp @@ -62,30 +62,28 @@ 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]; - bool slotFound = dupCheck.count(slot); - uassert(4822806, str::stream() << "duplicate field: " << slot, !slotFound); + auto [it, inserted] = dupCheck.insert(slot); + uassert(4822806, str::stream() << "duplicate field: " << slot, inserted); 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 7675fad6846..c54f3bfe956 100644 --- a/src/mongo/db/exec/sbe/util/spilling.cpp +++ b/src/mongo/db/exec/sbe/util/spilling.cpp @@ -29,18 +29,6 @@ #include "mongo/db/exec/sbe/util/spilling.h" -#include "mongo/base/status.h" -#include "mongo/base/status_with.h" -#include "mongo/base/string_data.h" -#include "mongo/bson/timestamp.h" -#include "mongo/db/query/query_knobs_gen.h" -#include "mongo/db/storage/record_data.h" -#include "mongo/db/storage/recovery_unit.h" -#include "mongo/db/storage/write_unit_of_work.h" -#include "mongo/util/assert_util.h" -#include "mongo/util/bufreader.h" -#include "mongo/util/str.h" - namespace mongo { namespace sbe { @@ -69,136 +57,56 @@ KeyString::Value decodeKeyString(const RecordId& rid, KeyString::TypeBits typeBi return kb.getValueCopy(); } -SpillingStore::SpillingStore(OperationContext* opCtx, KeyFormat format) { - _recordStore = - opCtx->getServiceContext()->getStorageEngine()->makeTemporaryRecordStore(opCtx, format); - - _spillingUnit = std::unique_ptr<RecoveryUnit>( - opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit()); - _spillingUnit->setCacheMaxWaitTimeout(Milliseconds(internalQuerySpillingMaxWaitTimeout.load())); - _spillingState = WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork; -} - -SpillingStore::~SpillingStore() {} - -int SpillingStore::upsertToRecordStore(OperationContext* opCtx, - const RecordId& recordKey, - const value::MaterializedRow& key, - const value::MaterializedRow& val, - bool update) { - BufBuilder buf; - key.serializeForSorter(buf); - val.serializeForSorter(buf); - return upsertToRecordStore(opCtx, recordKey, buf, update); +boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* opCtx, + RecordStore* rs, + const RecordId& rid) { + RecordData record; + if (rs->findRecord(opCtx, rid, &record)) { + auto valueReader = BufReader(record.data(), record.size()); + return value::MaterializedRow::deserializeForSorter(valueReader, {}); + } + return boost::none; } -int SpillingStore::upsertToRecordStore( - OperationContext* opCtx, - const RecordId& key, - const value::MaterializedRow& val, - const KeyString::TypeBits& typeBits, // recover type of value. - bool update) { +int upsertToRecordStore(OperationContext* opCtx, + RecordStore* rs, + const RecordId& key, + const value::MaterializedRow& val, + const KeyString::TypeBits& typeBits, // recover type of value. + bool update) { BufBuilder bufValue; val.serializeForSorter(bufValue); - // Append the 'typeBits' to the end of the val's buffer so the 'key' can be reconstructed when - // draining HashAgg. - bufValue.appendBuf(typeBits.getBuffer(), typeBits.getSize()); - - return upsertToRecordStore(opCtx, key, bufValue, update); + return upsertToRecordStore(opCtx, rs, key, bufValue, typeBits, update); } -int SpillingStore::upsertToRecordStore( - OperationContext* opCtx, - const RecordId& key, - BufBuilder& buf, - const KeyString::TypeBits& typeBits, // recover type of value. - bool update) { +int upsertToRecordStore(OperationContext* opCtx, + RecordStore* rs, + const RecordId& key, + BufBuilder& buf, + const KeyString::TypeBits& typeBits, // recover type of value. + bool update) { + // Append the 'typeBits' to the end of the val's buffer so the 'key' can be reconstructed when // draining HashAgg. buf.appendBuf(typeBits.getBuffer(), typeBits.getSize()); - return upsertToRecordStore(opCtx, key, buf, update); -} - -int SpillingStore::upsertToRecordStore(OperationContext* opCtx, - const RecordId& key, - BufBuilder& buf, - bool update) { assertIgnorePrepareConflictsBehavior(opCtx); - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); WriteUnitOfWork wuow(opCtx); auto result = mongo::Status::OK(); if (update) { - result = rs()->updateRecord(opCtx, key, buf.buf(), buf.len()); + result = rs->updateRecord(opCtx, key, buf.buf(), buf.len()); } else { - auto status = rs()->insertRecord(opCtx, key, buf.buf(), buf.len(), Timestamp{}); + auto status = rs->insertRecord(opCtx, key, buf.buf(), buf.len(), Timestamp{}); result = status.getStatus(); } wuow.commit(); - if (!result.isOK()) { tasserted(5843600, str::stream() << "Failed to write to disk because " << result.reason()); return 0; } return buf.len(); } - -Status SpillingStore::insertRecords(OperationContext* opCtx, - std::vector<Record>* inOutRecords, - const std::vector<Timestamp>& timestamps) { - assertIgnorePrepareConflictsBehavior(opCtx); - - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - WriteUnitOfWork wuow(opCtx); - auto status = rs()->insertRecords(opCtx, inOutRecords, timestamps); - wuow.commit(); - - return status; -} - -boost::optional<value::MaterializedRow> SpillingStore::readFromRecordStore(OperationContext* opCtx, - const RecordId& rid) { - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - - RecordData record; - if (rs()->findRecord(opCtx, rid, &record)) { - auto valueReader = BufReader(record.data(), record.size()); - return value::MaterializedRow::deserializeForSorter(valueReader, {}); - } - return boost::none; -} - -bool SpillingStore::findRecord(OperationContext* opCtx, const RecordId& loc, RecordData* out) { - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - - return rs()->findRecord(opCtx, loc, out); -} - -void SpillingStore::switchToSpilling(OperationContext* opCtx) { - invariant(!_originalUnit); - _originalUnit = opCtx->releaseRecoveryUnit(); - _originalState = opCtx->setRecoveryUnit(std::move(_spillingUnit), _spillingState); -} -void SpillingStore::switchToOriginal(OperationContext* opCtx) { - invariant(!_spillingUnit); - _spillingUnit = opCtx->releaseRecoveryUnit(); - _spillingState = opCtx->setRecoveryUnit(std::move(_originalUnit), _originalState); - invariant(!(_spillingUnit->getState() == RecoveryUnit::State::kInactiveInUnitOfWork || - _spillingUnit->getState() == RecoveryUnit::State::kActive)); -} - -void SpillingStore::saveState() { - _spillingUnit->abandonSnapshot(); -} -void SpillingStore::restoreState() { - // We do not have to do anything. -} - } // namespace sbe } // namespace mongo diff --git a/src/mongo/db/exec/sbe/util/spilling.h b/src/mongo/db/exec/sbe/util/spilling.h index 205d6f1a031..95f73e2b02e 100644 --- a/src/mongo/db/exec/sbe/util/spilling.h +++ b/src/mongo/db/exec/sbe/util/spilling.h @@ -29,14 +29,9 @@ #pragma once -#include <boost/optional/optional.hpp> -#include <utility> +#include "mongo/platform/basic.h" -#include "mongo/bson/util/builder.h" #include "mongo/db/exec/sbe/values/slot.h" -#include "mongo/db/operation_context.h" -#include "mongo/db/record_id.h" -#include "mongo/db/storage/record_store.h" #include "mongo/db/storage/temporary_record_store.h" namespace mongo { @@ -55,104 +50,27 @@ std::pair<RecordId, KeyString::TypeBits> encodeKeyString(KeyString::Builder&, // Reconstructs the KeyString carried in RecordId using 'typeBits'. KeyString::Value decodeKeyString(const RecordId& rid, KeyString::TypeBits typeBits); -/** - * SpillingStore is a wrapper around a temporary record store than maintains its own transaction as - * we do not want to intermingle operations running in the main query with spill reads and writes. - */ -class SpillingStore { -public: - SpillingStore(OperationContext* opCtx, KeyFormat format = KeyFormat::String); - ~SpillingStore(); - - /** - * When a collator is provided, the key is encoded using the collator before being converted to - * a record id. In this case, it is not possible to recover the key from the record id, thus we - * need to store the original value of the key as well. - */ - int upsertToRecordStore(OperationContext* opCtx, - const RecordId& recordKey, - const value::MaterializedRow& key, - const value::MaterializedRow& val, - bool update); - /** - * Inserts or updates a key/value into 'rs'. The 'update' flag controls whether or not an update - * will be performed. If a key/value pair is inserted into the 'rs' that already exists and - * 'update' is false, this function will tassert. - * - * Returns the size of the new record in bytes, including the record id and value portions. - */ - int upsertToRecordStore(OperationContext* opCtx, - const RecordId& key, - const value::MaterializedRow& val, - const KeyString::TypeBits& typeBits, - bool update); - int upsertToRecordStore(OperationContext* opCtx, - const RecordId& key, - BufBuilder& buf, - const KeyString::TypeBits& typeBits, // recover type of value. - bool update); - int upsertToRecordStore(OperationContext* opCtx, - const RecordId& key, - BufBuilder& buf, - bool update); - - - Status insertRecords(OperationContext* opCtx, - std::vector<Record>* inOutRecords, - const std::vector<Timestamp>& timestamps); - - // Reads a materialized row from the record store. - boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* opCtx, - const RecordId& rid); - - bool findRecord(OperationContext* opCtx, const RecordId& loc, RecordData* out); - - auto rs() { - return _recordStore->rs(); - } - - auto getCursor(OperationContext* opCtx) { - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - return rs()->getCursor(opCtx); - } - - void resetCursor(OperationContext* opCtx, std::unique_ptr<SeekableRecordCursor>& cursor) { - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - cursor.reset(); - } +// Reads a materialized row from the record store. +boost::optional<value::MaterializedRow> readFromRecordStore(OperationContext* opCtx, + RecordStore* rs, + const RecordId& rid); - auto saveCursor(OperationContext* opCtx, std::unique_ptr<SeekableRecordCursor>& cursor) { - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - - return cursor->save(); - } - - auto restoreCursor(OperationContext* opCtx, std::unique_ptr<SeekableRecordCursor>& cursor) { - switchToSpilling(opCtx); - ON_BLOCK_EXIT([&] { switchToOriginal(opCtx); }); - - return cursor->restore(); - } - - void saveState(); - void restoreState(); - -private: - void switchToSpilling(OperationContext* opCtx); - void switchToOriginal(OperationContext* opCtx); - - std::unique_ptr<TemporaryRecordStore> _recordStore; - - std::unique_ptr<RecoveryUnit> _originalUnit; - WriteUnitOfWork::RecoveryUnitState _originalState; - - std::unique_ptr<RecoveryUnit> _spillingUnit; - WriteUnitOfWork::RecoveryUnitState _spillingState; - - size_t _counter{0}; -}; +/** 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. + */ +int upsertToRecordStore(OperationContext* opCtx, + RecordStore* rs, + const RecordId& key, + const value::MaterializedRow& val, + const KeyString::TypeBits& typeBits, + bool update); + +int upsertToRecordStore(OperationContext* opCtx, + RecordStore* rs, + const RecordId& key, + BufBuilder& buf, + const KeyString::TypeBits& typeBits, // recover type of value. + bool update); } // namespace sbe } // namespace mongo diff --git a/src/mongo/db/exec/sbe/values/slot.cpp b/src/mongo/db/exec/sbe/values/slot.cpp index 93605a9abfa..45cbc977980 100644 --- a/src/mongo/db/exec/sbe/values/slot.cpp +++ b/src/mongo/db/exec/sbe/values/slot.cpp @@ -42,9 +42,7 @@ #include "mongo/util/bufreader.h" namespace mongo::sbe::value { - -static std::pair<TypeTags, Value> deserializeValue(BufReader& buf, - const CollatorInterface* collator) { +static std::pair<TypeTags, Value> deserializeValue(BufReader& buf) { auto tag = static_cast<TypeTags>(buf.read<uint8_t>()); Value val; @@ -109,7 +107,7 @@ static std::pair<TypeTags, Value> deserializeValue(BufReader& buf, if (cnt) { arr->reserve(cnt); for (size_t idx = 0; idx < cnt; ++idx) { - auto [tag, val] = deserializeValue(buf, collator); + auto [tag, val] = deserializeValue(buf); arr->push_back(tag, val); } } @@ -118,16 +116,13 @@ static std::pair<TypeTags, Value> deserializeValue(BufReader& buf, break; } case TypeTags::ArraySet: { - // The first byte is a flag to tell us whether the ArraySet had a collation prior to - // serialization. - auto collated = buf.read<char>(); - auto [arrTag, arrVal] = makeNewArraySet(collated ? collator : nullptr); auto cnt = buf.read<LittleEndian<size_t>>(); + auto [arrTag, arrVal] = makeNewArraySet(); auto arr = getArraySetView(arrVal); if (cnt) { arr->reserve(cnt); for (size_t idx = 0; idx < cnt; ++idx) { - auto [tag, val] = deserializeValue(buf, collator); + auto [tag, val] = deserializeValue(buf); arr->push_back(tag, val); } } @@ -143,7 +138,7 @@ static std::pair<TypeTags, Value> deserializeValue(BufReader& buf, obj->reserve(cnt); for (size_t idx = 0; idx < cnt; ++idx) { auto fieldName = buf.readCStr(); - auto [tag, val] = deserializeValue(buf, collator); + auto [tag, val] = deserializeValue(buf); obj->push_back(fieldName, tag, val); } } @@ -218,12 +213,12 @@ static std::pair<TypeTags, Value> deserializeValue(BufReader& buf, } MaterializedRow MaterializedRow::deserializeForSorter(BufReader& buf, - const SorterDeserializeSettings& settings) { + const SorterDeserializeSettings&) { auto cnt = buf.read<LittleEndian<size_t>>(); MaterializedRow result{cnt}; for (size_t idx = 0; idx < cnt; ++idx) { - auto [tag, val] = deserializeValue(buf, settings.collator); + auto [tag, val] = deserializeValue(buf); result.reset(idx, true, tag, val); } @@ -293,12 +288,6 @@ static void serializeValue(BufBuilder& buf, TypeTags tag, Value val) { } case TypeTags::ArraySet: { auto arr = getArraySetView(val); - // If an ArraySet has a collation, we serialize a byte which acts as a flag as to - // whether the set should be created with a collation upon deserialization. Also, we - // assume that the caller which does deserialization will have the context about what - // the collation is, and therefore we can save space by not serializing the full - // description of the collation. - buf.appendChar(arr->getCollator() ? 1 : 0); buf.appendNum(arr->size()); for (auto& kv : arr->values()) { serializeValue(buf, kv.first, kv.second); @@ -382,22 +371,7 @@ static void serializeValue(BufBuilder& buf, TypeTags tag, Value val) { } } -/** - * If non-null 'collator' is provided during serialization, then the encoding guarantees that values - * which are equal up to the collation will encode to the same result, allowing for collation-aware - * equality comparisons. However, the collator-aware encoded values are not always decodable. Groups - * store an original copy of the group key alongside the encoded key string when there is a - * collation, so the key string does not need to be decodable. - */ -static void serializeValueIntoKeyString(KeyString::Builder& buf, - TypeTags tag, - Value val, - const CollatorInterface* collator) { - - const auto stringTransformFn = [&](StringData stringData) { - return collator->getComparisonString(stringData); - }; - +static void serializeValueIntoKeyString(KeyString::Builder& buf, TypeTags tag, Value val) { switch (tag) { case TypeTags::Nothing: { buf.appendBool(false); @@ -460,21 +434,19 @@ static void serializeValueIntoKeyString(KeyString::Builder& buf, buf.appendUndefined(); break; } - case TypeTags::StringSmall: + case TypeTags::StringSmall: { + // Small strings cannot contain null bytes, so it is safe to serialize them as plain + // C-strings with a null terminator. + buf.appendBool(true); + buf.appendString(getStringView(tag, val)); + break; + } case TypeTags::StringBig: case TypeTags::bsonString: { buf.appendBool(true); - if (collator) { - buf.appendString(getStringView(tag, val), stringTransformFn); - } else { - buf.appendString(getStringView(tag, val)); - } + buf.appendString(getStringOrSymbolView(tag, val)); break; } - // Note that the collation would have to apply to bsonSymbol values to match the - // behavior of the Classic engine when spilling groups to disk. bsonSymbol is - // deprecated, however, and SBE no longer provides strict correctness guarantees about - // computations on bsonSymbol values. case TypeTags::bsonSymbol: { buf.appendBool(true); buf.appendSymbol(getStringOrSymbolView(tag, val)); @@ -485,13 +457,9 @@ static void serializeValueIntoKeyString(KeyString::Builder& buf, // TODO SERVER-61629: convert this to serialize the 'arr' directly instead of // constructing a BSONArray. BSONArrayBuilder builder; - bson::convertToBsonObj(builder, value::ArrayEnumerator{tag, val}); + bson::convertToBsonObj(builder, getArrayView(val)); buf.appendBool(true); - if (collator) { - buf.appendArray(builder.arr(), stringTransformFn); - } else { - buf.appendArray(builder.arr()); - } + buf.appendArray(BSONArray(builder.done())); break; } case TypeTags::Object: { @@ -500,37 +468,27 @@ static void serializeValueIntoKeyString(KeyString::Builder& buf, BSONObjBuilder builder; bson::convertToBsonObj(builder, getObjectView(val)); buf.appendBool(true); - if (collator) { - buf.appendObject(builder.done(), stringTransformFn); - } else { - buf.appendObject(builder.done()); - } + buf.appendObject(builder.done()); break; } - case TypeTags::bsonObjectId: case TypeTags::ObjectId: { buf.appendBool(true); - buf.appendOID(OID::from(getRawPointerView(val))); + buf.appendBytes(getObjectIdView(val), sizeof(ObjectIdType)); break; } case TypeTags::bsonObject: { - BSONObj bson{getRawPointerView(val)}; buf.appendBool(true); - if (collator) { - buf.appendObject(bson, stringTransformFn); - } else { - buf.appendObject(bson); - } + buf.appendObject(BSONObj(getRawPointerView(val))); break; } case TypeTags::bsonArray: { - BSONObj bson{getRawPointerView(val)}; buf.appendBool(true); - if (collator) { - buf.appendArray(BSONArray(BSONObj(bson)), stringTransformFn); - } else { - buf.appendArray(BSONArray(BSONObj(bson))); - } + buf.appendArray(BSONArray(BSONObj(getRawPointerView(val)))); + break; + } + case TypeTags::bsonObjectId: { + buf.appendBool(true); + buf.appendOID(OID::from(getRawPointerView(val))); break; } case TypeTags::bsonBinData: { @@ -599,18 +557,15 @@ void MaterializedRow::serializeForSorter(BufBuilder& buf) const { } } -void MaterializedRow::serializeIntoKeyString(KeyString::Builder& buf, - const CollatorInterface* collator) const { +void MaterializedRow::serializeIntoKeyString(KeyString::Builder& buf) const { for (size_t idx = 0; idx < size(); ++idx) { auto [tag, val] = getViewOfValue(idx); - serializeValueIntoKeyString(buf, tag, val, collator); + serializeValueIntoKeyString(buf, tag, val); } } -MaterializedRow MaterializedRow::deserializeFromKeyString( - const KeyString::Value& keyString, - BufBuilder* valueBufferBuilder, - boost::optional<size_t> numPrefixValsToRead) { +MaterializedRow MaterializedRow::deserializeFromKeyString(const KeyString::Value& keyString, + BufBuilder* valueBufferBuilder) { BufReader reader(keyString.getBuffer(), keyString.getSize()); KeyString::TypeBits typeBits(keyString.getTypeBits()); KeyString::TypeBits::Reader typeBitsReader(typeBits); @@ -622,8 +577,7 @@ MaterializedRow MaterializedRow::deserializeFromKeyString( &reader, &typeBitsReader, false /* inverted */, typeBits.version, &valBuilder); } while (keepReading); - size_t sizeOfRow = numPrefixValsToRead ? *numPrefixValsToRead : valBuilder.numValues(); - MaterializedRow result{sizeOfRow}; + MaterializedRow result{valBuilder.numValues()}; 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 e682b2bfeb8..f853f816d4d 100644 --- a/src/mongo/db/exec/sbe/values/slot.h +++ b/src/mongo/db/exec/sbe/values/slot.h @@ -468,11 +468,8 @@ public: } // The following methods are used by the sorter only. - struct SorterDeserializeSettings { - const CollatorInterface* collator{nullptr}; - }; - static MaterializedRow deserializeForSorter(BufReader& buf, - const SorterDeserializeSettings& settings); + struct SorterDeserializeSettings {}; + static MaterializedRow deserializeForSorter(BufReader& buf, const SorterDeserializeSettings&); void serializeForSorter(BufBuilder& buf) const; int memUsageForSorter() const; auto getOwned() const { @@ -486,21 +483,11 @@ 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. - * - * If non-null 'collator' is provided during serialization, then any strings in the row are - * encoded as ICU collation keys prior to being KeyString-encoded. */ - static MaterializedRow deserializeFromKeyString( - const KeyString::Value& keyString, - BufBuilder* valueBufferBuilder, - boost::optional<size_t> numPrefixValsToRead = boost::none); + static MaterializedRow deserializeFromKeyString(const KeyString::Value& keyString, - void serializeIntoKeyString(KeyString::Builder& builder, - const CollatorInterface* collator = nullptr) const; + BufBuilder* valueBufferBuilder); + void serializeIntoKeyString(KeyString::Builder& builder) const; private: static size_t sizeInBytes(size_t count) { @@ -590,9 +577,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 { @@ -610,7 +597,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 e0f73c87ddf..6c21f4f6e5d 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()); } -bool ArraySet::push_back(TypeTags tag, Value val) { +void ArraySet::push_back(TypeTags tag, Value val) { if (tag != TypeTags::Nothing) { ValueGuard guard{tag, val}; auto [it, inserted] = _values.insert({tag, val}); @@ -868,11 +868,7 @@ bool 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 ae207106d8b..26ba3e5e1de 100644 --- a/src/mongo/db/exec/sbe/values/value.h +++ b/src/mongo/db/exec/sbe/values/value.h @@ -844,14 +844,7 @@ public: } } - /** - * 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); + void 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 00333e9f824..9ad2b511242 100644 --- a/src/mongo/db/exec/sbe/values/value_builder.h +++ b/src/mongo/db/exec/sbe/values/value_builder.h @@ -191,11 +191,8 @@ 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 < _tagList.size()); + invariant(index < _numValues); auto tag = _tagList[index]; auto val = _valList[index]; @@ -227,8 +224,9 @@ protected: } void appendValue(TypeTags tag, Value val) noexcept { - _tagList.push_back(tag); - _valList.push_back(val); + _tagList[_numValues] = tag; + _valList[_numValues] = val; + ++_numValues; } void appendValue(std::pair<TypeTags, Value> in) noexcept { @@ -243,12 +241,14 @@ 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.push_back(tag); - _valList.push_back(value::bitcastFrom<int32_t>(_valueBufferBuilder->len())); + _tagList[_numValues] = tag; + _valList[_numValues] = value::bitcastFrom<int32_t>(_valueBufferBuilder->len()); + ++_numValues; } - absl::InlinedVector<TypeTags, kInlinedVectorSize> _tagList; - absl::InlinedVector<Value, kInlinedVectorSize> _valList; + std::array<TypeTags, Ordering::kMaxCompoundIndexKeys> _tagList; + std::array<Value, Ordering::kMaxCompoundIndexKeys> _valList; + size_t _numValues = 0; BufBuilder* _valueBufferBuilder; }; @@ -270,12 +270,11 @@ 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. - _tagList.pop_back(); - _valList.pop_back(); + --_numValues; } size_t numValues() const override { - return _tagList.size(); + return _numValues; } /** @@ -285,7 +284,7 @@ public: */ void readValues(std::vector<OwnedValueAccessor>* accessors) { auto bufferLen = _valueBufferBuilder->len(); - for (size_t i = 0; i < _tagList.size(); ++i) { + for (size_t i = 0; i < _numValues; ++i) { auto [tag, val] = getValue(i, bufferLen); invariant(i < accessors->size()); (*accessors)[i].reset(false, tag, val); @@ -305,7 +304,7 @@ public: size_t numValues() const override { size_t nVals = 0; size_t bufIdx = 0; - while (bufIdx < _tagList.size()) { + while (bufIdx < _numValues) { auto tag = _tagList[bufIdx]; auto val = _valList[bufIdx]; if (tag == TypeTags::Boolean && !bitcastTo<bool>(val)) { @@ -324,10 +323,7 @@ public: auto bufferLen = _valueBufferBuilder->len(); size_t bufIdx = 0; size_t rowIdx = 0; - // 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()) { + while (bufIdx < _numValues) { 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 ceacf610a4a..5b44bef6549 100644 --- a/src/mongo/db/exec/sbe/values/value_serialization_test.cpp +++ b/src/mongo/db/exec/sbe/values/value_serialization_test.cpp @@ -268,18 +268,6 @@ 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)}}); @@ -454,30 +442,4 @@ 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); -} - -// Test that roundtripping through KeyString works for ObjectIdType: ObjectId; bsonObjectId. -TEST_F(ValueSerializeForKeyString, RoundtripObjectIdType) { - auto [objectIdTag, objectIdVal] = value::makeNewObjectId(); - - auto oid = OID::gen(); - auto obj = BSON("" << oid); - auto oidStorage = obj.firstElement().value(); - - sbe::value::ValueGuard testDataGuard{objectIdTag, objectIdVal}; - runTest({{objectIdTag, objectIdVal}, - {value::TypeTags::bsonObjectId, value::bitcastFrom<const char*>(oidStorage)}}); -} } // namespace mongo::sbe diff --git a/src/mongo/db/exec/sbe/vm/arith.cpp b/src/mongo/db/exec/sbe/vm/arith.cpp index e6c9d380ffd..e4c67775ad8 100644 --- a/src/mongo/db/exec/sbe/vm/arith.cpp +++ b/src/mongo/db/exec/sbe/vm/arith.cpp @@ -503,57 +503,6 @@ 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; @@ -602,67 +551,6 @@ 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); @@ -1027,7 +915,7 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::genericLn(value::TypeT if (!operand.isGreater(Decimal128::kNormalizedZero) && !operand.isNaN()) { return {false, value::TypeTags::Nothing, 0}; } - auto operandLn = operand.naturalLogarithm(); + auto operandLn = operand.logarithm(); auto [tag, value] = value::makeCopyDecimal(operandLn); return {true, tag, value}; diff --git a/src/mongo/db/exec/sbe/vm/vm.cpp b/src/mongo/db/exec/sbe/vm/vm.cpp index 812de5232af..7eb8eb7149e 100644 --- a/src/mongo/db/exec/sbe/vm/vm.cpp +++ b/src/mongo/db/exec/sbe/vm/vm.cpp @@ -1072,40 +1072,35 @@ 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 newArrGuard{accTag, accValue}; + value::ValueGuard guard{accTag, accValue}; auto arr = value::getArrayView(accValue); arr->reserve(AggSumValueElems::kMaxSizeOfArray); - // 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. + // The order of the following three elements should match to 'AggSumValueElems'. 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)); - newArrGuard.reset(); + // 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}; } - - 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}; } @@ -1240,37 +1235,31 @@ 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) { - std::tie(accTag, accValue) = value::makeNewArray(); - value::ValueGuard newArrGuard{accTag, accValue}; - auto arr = value::getArrayView(accValue); + auto [newAccTag, newAccValue] = value::makeNewArray(); + value::ValueGuard newGuard{newAccTag, newAccValue}; + auto arr = value::getArrayView(newAccValue); 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)); - newArrGuard.reset(); + aggStdDevImpl(arr, fieldTag, fieldValue); + newGuard.reset(); + return {true, newAccTag, newAccValue}; } - - 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}; } @@ -3030,120 +3019,6 @@ 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, @@ -3521,166 +3396,6 @@ 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); @@ -4667,7 +4382,7 @@ std::tuple<bool, value::TypeTags, value::Value> ByteCode::dispatchBuiltin(Builti case Builtin::doubleDoubleSum: return builtinDoubleDoubleSum(arity); case Builtin::aggDoubleDoubleSum: - return builtinAggDoubleDoubleSum<false /*merging*/>(arity); + return builtinAggDoubleDoubleSum(arity); case Builtin::doubleDoubleSumFinalize: return builtinDoubleDoubleSumFinalize<>(arity); case Builtin::doubleDoubleMergeSumFinalize: @@ -4676,12 +4391,8 @@ 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<false /*merging*/>(arity); - case Builtin::aggMergeStdDevs: - return builtinAggStdDev<true /*merging*/>(arity); + return builtinAggStdDev(arity); case Builtin::stdDevPopFinalize: return builtinStdDevPopFinalize(arity); case Builtin::stdDevSampFinalize: @@ -4734,16 +4445,6 @@ 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 e5148e8d4b2..255a1a497c2 100644 --- a/src/mongo/db/exec/sbe/vm/vm.h +++ b/src/mongo/db/exec/sbe/vm/vm.h @@ -511,33 +511,12 @@ 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 - - // Special double summation. - doubleDoubleSum, - // A variant of the standard sum aggregate function which maintains a DoubleDouble as the - // accumulator's underlying state. + doubleDoubleSum, // special double summation 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 @@ -548,18 +527,6 @@ 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, @@ -1013,19 +980,11 @@ private: value::TypeTags fieldTag, value::Value fieldValue); - void aggDoubleDoubleSumImpl(value::Array* accumulator, - value::TypeTags rhsTag, - value::Value rhsValue); - void aggMergeDoubleDoubleSumsImpl(value::Array* accumulator, - value::TypeTags rhsTag, - value::Value rhsValue); + void aggDoubleDoubleSumImpl(value::Array* arr, 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* accumulator, value::TypeTags rhsTag, value::Value rhsValue); - void aggMergeStdDevsImpl(value::Array* accumulator, - value::TypeTags rhsTag, - value::Value rhsValue); + void aggStdDevImpl(value::Array* arr, value::TypeTags rhsTag, value::Value rhsValue); std::tuple<bool, value::TypeTags, value::Value> aggStdDevFinalizeImpl(value::Value fieldValue, bool isSamp); @@ -1144,24 +1103,14 @@ 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); @@ -1188,16 +1137,6 @@ 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/scoped_timer.cpp b/src/mongo/db/exec/scoped_timer.cpp index bd53003faa5..f33c498a57a 100644 --- a/src/mongo/db/exec/scoped_timer.cpp +++ b/src/mongo/db/exec/scoped_timer.cpp @@ -32,8 +32,6 @@ #include "mongo/db/exec/scoped_timer.h" #include "mongo/util/clock_source.h" -#include "mongo/bson/bsonobjbuilder.h" - namespace mongo { ScopedTimer::ScopedTimer(ClockSource* cs, long long* counter) @@ -44,25 +42,4 @@ ScopedTimer::~ScopedTimer() { *_counter += elapsed; } - -TimeElapsedBuilderScopedTimer::TimeElapsedBuilderScopedTimer(ClockSource* clockSource, - StringData description, - BSONObjBuilder* builder) - : _clockSource(clockSource), - _description(description), - _beginTime(clockSource->now()), - _builder(builder) {} - -TimeElapsedBuilderScopedTimer::~TimeElapsedBuilderScopedTimer() { - mongo::Milliseconds elapsedTime = _clockSource->now() - _beginTime; - _builder->append(_description, elapsedTime.toString()); -} - -boost::optional<TimeElapsedBuilderScopedTimer> createTimeElapsedBuilderScopedTimer( - ClockSource* clockSource, StringData description, BSONObjBuilder* builder) { - if (builder == nullptr) { - return boost::none; - } - return TimeElapsedBuilderScopedTimer(clockSource, description, builder); -} } // namespace mongo diff --git a/src/mongo/db/exec/scoped_timer.h b/src/mongo/db/exec/scoped_timer.h index c3c52243369..c11c424e59a 100644 --- a/src/mongo/db/exec/scoped_timer.h +++ b/src/mongo/db/exec/scoped_timer.h @@ -59,28 +59,4 @@ private: const Date_t _start; }; -/** - * The timer appends the time elapsed since its construction to a BSON Object. - */ -class TimeElapsedBuilderScopedTimer { -public: - explicit TimeElapsedBuilderScopedTimer(ClockSource* clockSource, - StringData description, - BSONObjBuilder* builder); - ~TimeElapsedBuilderScopedTimer(); - -private: - ClockSource* _clockSource; - StringData _description; - Date_t _beginTime; - BSONObjBuilder* _builder; -}; - -/* - * This helper function only creates a TimeElapsedBuilderScopedTimer when a valid pointer to a - * builder is passed in. This is used when timing startup tasks, so that tasks that run during - * startup and outside of startup will only be timed when they are called during startup. - */ -boost::optional<TimeElapsedBuilderScopedTimer> createTimeElapsedBuilderScopedTimer( - ClockSource* clockSource, StringData description, BSONObjBuilder* builder); } // namespace mongo diff --git a/src/mongo/db/exec/skip.cpp b/src/mongo/db/exec/skip.cpp index 755e9278fe0..d3d0fc48afd 100644 --- a/src/mongo/db/exec/skip.cpp +++ b/src/mongo/db/exec/skip.cpp @@ -47,7 +47,7 @@ SkipStage::SkipStage(ExpressionContext* expCtx, long long toSkip, WorkingSet* ws, std::unique_ptr<PlanStage> child) - : PlanStage(kStageType, expCtx), _ws(ws), _leftToSkip(toSkip), _skipAmount(toSkip) { + : PlanStage(kStageType, expCtx), _ws(ws), _toSkip(toSkip) { _children.emplace_back(std::move(child)); } @@ -63,9 +63,9 @@ PlanStage::StageState SkipStage::doWork(WorkingSetID* out) { if (PlanStage::ADVANCED == status) { // If we're still skipping results... - if (_leftToSkip > 0) { + if (_toSkip > 0) { // ...drop the result. - --_leftToSkip; + --_toSkip; _ws->free(id); return PlanStage::NEED_TIME; } @@ -82,7 +82,7 @@ PlanStage::StageState SkipStage::doWork(WorkingSetID* out) { unique_ptr<PlanStageStats> SkipStage::getStats() { _commonStats.isEOF = isEOF(); - _specificStats.skip = _skipAmount; + _specificStats.skip = _toSkip; unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_SKIP); ret->specific = std::make_unique<SkipStats>(_specificStats); ret->children.emplace_back(child()->getStats()); diff --git a/src/mongo/db/exec/skip.h b/src/mongo/db/exec/skip.h index 5d0a764bd8a..24937662d02 100644 --- a/src/mongo/db/exec/skip.h +++ b/src/mongo/db/exec/skip.h @@ -66,13 +66,8 @@ public: private: WorkingSet* _ws; - // The number of results left to skip. This number is decremented during query execution as we - // successfully skip a document. - long long _leftToSkip; - - // Represents the number of results to skip. Unlike '_leftToSkip', this remains constant and - // is used when gathering statistics in explain. - const long long _skipAmount; + // We drop the first _toSkip results that we would have returned. + long long _toSkip; // Stats SkipStats _specificStats; diff --git a/src/mongo/db/exec/sort_executor.h b/src/mongo/db/exec/sort_executor.h index 8120f52caa9..6509c767e9a 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->stats().spilledRanges(); + _stats.spills += _sorter->numSpills(); _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 a4548ce4a4c..d83af04df07 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, IndexCatalog::InclusionPolicy::kReady, &indexes); + opCtx, keyPatternObj, false, &indexes); uassert(16890, str::stream() << "Can't find index: " << keyPatternObj, !indexes.empty()); diff --git a/src/mongo/db/exec/text_or.cpp b/src/mongo/db/exec/text_or.cpp index f9e0745ca16..a0ba3eb347e 100644 --- a/src/mongo/db/exec/text_or.cpp +++ b/src/mongo/db/exec/text_or.cpp @@ -104,7 +104,9 @@ std::unique_ptr<PlanStageStats> TextOrStage::getStats() { _commonStats.isEOF = isEOF(); if (_filter) { - _commonStats.filter = _filter->serialize(); + BSONObjBuilder bob; + _filter->serialize(&bob); + _commonStats.filter = bob.obj(); } unique_ptr<PlanStageStats> ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_TEXT_OR); diff --git a/src/mongo/db/exec/update_stage.cpp b/src/mongo/db/exec/update_stage.cpp index 32887ab1f19..5a273dc2d89 100644 --- a/src/mongo/db/exec/update_stage.cpp +++ b/src/mongo/db/exec/update_stage.cpp @@ -278,9 +278,6 @@ BSONObj UpdateStage::transformAndUpdate(const Snapshotted<BSONObj>& oldObj, // Ensure we set the type correctly args.source = writeToOrphan ? OperationSource::kFromMigrate : request->source(); - args.mustCheckExistenceForInsertOperations = - driver->getUpdateExecutor()->getCheckExistenceForDiffInsertOperations(); - if (inPlace) { if (!request->explain()) { newObj = oldObj.value(); @@ -462,41 +459,24 @@ PlanStage::StageState UpdateStage::doWork(WorkingSetID* out) { bool writeToOrphan = false; if (!_params.request->explain() && _isUserInitiatedWrite) { - 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; + 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; } } @@ -526,18 +506,6 @@ 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 dfb54a99082..423e300fcf7 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/exception_util.h" +#include "mongo/db/concurrency/write_conflict_exception.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,13 +273,8 @@ void UpsertStage::_generateNewDocumentFromSuppliedDoc(const FieldRefSet& immutab UpdateDriver replacementDriver(nullptr); // Create a new replacement-style update from the supplied document. - replacementDriver.parse( - write_ops::UpdateModification( - suppliedDoc, write_ops::UpdateModification::ClassicTag{}, true /* isReplacement */), - {}); + replacementDriver.parse(write_ops::UpdateModification::parseFromClassicUpdate(suppliedDoc), {}); replacementDriver.setLogOp(false); - replacementDriver.setBypassEmptyTsReplacement( - static_cast<bool>(_params.request->getBypassEmptyTsReplacement())); // We do not validate for storage, as we will validate the full document before inserting. // However, we ensure that no immutable fields are modified. diff --git a/src/mongo/db/exec/working_set_common.cpp b/src/mongo/db/exec/working_set_common.cpp index 3920e6ad79a..1a1a04cade6 100644 --- a/src/mongo/db/exec/working_set_common.cpp +++ b/src/mongo/db/exec/working_set_common.cpp @@ -37,8 +37,6 @@ #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/catalog/collection.h" -#include "mongo/db/catalog/health_log_gen.h" -#include "mongo/db/catalog/health_log_interface.h" #include "mongo/db/exec/working_set.h" #include "mongo/db/index/index_access_method.h" #include "mongo/db/query/canonical_query.h" @@ -46,7 +44,6 @@ #include "mongo/db/storage/execution_context.h" #include "mongo/db/storage/index_entry_comparison.h" #include "mongo/logv2/log.h" -#include "mongo/util/stacktrace.h" namespace mongo { @@ -109,30 +106,6 @@ bool WorkingSetCommon::fetch(OperationContext* opCtx, builder.append("pattern"_sd, ikd.indexKeyPattern); return builder.obj(); }; - - HealthLogEntry entry; - entry.setNss(ns); - entry.setTimestamp(Date_t::now()); - entry.setSeverity(SeverityEnum::Error); - entry.setScope(ScopeEnum::Index); - entry.setOperation("Index scan"); - entry.setMsg("Erroneous index key found with reference to non-existent record id"); - - BSONObjBuilder bob; - bob.append("recordId", member->recordId.toString()); - - const BSONArray indexKeyData = - logv2::seqLog( - boost::make_transform_iterator(member->keyData.begin(), indexKeyEntryToObjFn), - boost::make_transform_iterator(member->keyData.end(), indexKeyEntryToObjFn)) - .toBSONArray(); - bob.append("indexKeyData", indexKeyData); - - bob.appendElements(getStackTrace().getBSONRepresentation()); - entry.setData(bob.obj()); - - HealthLogInterface::get(opCtx)->log(entry); - LOGV2_ERROR_OPTIONS( 4615603, {logv2::UserAssertAfterLog(ErrorCodes::DataCorruptionDetected)}, @@ -141,7 +114,9 @@ bool WorkingSetCommon::fetch(OperationContext* opCtx, "on the collection.", "namespace"_attr = ns, "recordId"_attr = member->recordId, - "indexKeyData"_attr = indexKeyData); + "indexKeyData"_attr = logv2::seqLog( + boost::make_transform_iterator(member->keyData.begin(), indexKeyEntryToObjFn), + boost::make_transform_iterator(member->keyData.end(), indexKeyEntryToObjFn))); } return false; } diff --git a/src/mongo/db/exec/write_stage_common.cpp b/src/mongo/db/exec/write_stage_common.cpp index b0a8db18564..9b8aeb371e3 100644 --- a/src/mongo/db/exec/write_stage_common.cpp +++ b/src/mongo/db/exec/write_stage_common.cpp @@ -31,6 +31,7 @@ #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" @@ -55,15 +56,14 @@ PreWriteFilter::PreWriteFilter(OperationContext* opCtx, NamespaceString nss) feature_flags::gFeatureFlagNoChangeStreamEventsDueToOrphans.isEnabled(fcv); }()), _skipFiltering([&] { - // Allow writes on standalone and replica set. + // Always allow writes on replica sets. if (serverGlobalParams.clusterRole == ClusterRole::None) { return true; } - // 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); + // Always allow writes on standalone and secondary nodes. + const auto replCoord{repl::ReplicationCoordinator::get(opCtx)}; + return !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 489afa62f2f..5628822efff 100644 --- a/src/mongo/db/exec/write_stage_common.h +++ b/src/mongo/db/exec/write_stage_common.h @@ -29,6 +29,8 @@ #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" |
