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/pipeline | |
| 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/pipeline')
327 files changed, 4400 insertions, 15562 deletions
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index f3b54ed67a3..16e39fc4832 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -63,10 +63,12 @@ env.Library( ) env.Library( - target='aggregation_request_helper', source=[ - 'aggregate_command.idl', + target='aggregation_request_helper', + source=[ 'aggregation_request_helper.cpp', - ], LIBDEPS=[ + 'aggregate_command.idl', + ], + LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/exec/document_value/document_value', '$BUILD_DIR/mongo/db/namespace_string', @@ -78,7 +80,8 @@ env.Library( '$BUILD_DIR/mongo/db/write_concern_options', '$BUILD_DIR/mongo/idl/idl_parser', 'document_sources_idl', - ]) + ] +) env.Library( target='variable_validation', @@ -92,7 +95,6 @@ env.Library( env.Library( target='expression_context', source=[ - '$BUILD_DIR/mongo/db/query/query_shape/cmd_with_let_shape.cpp', 'expression.cpp', 'expression_context.cpp', 'expression_function.cpp', @@ -102,7 +104,6 @@ env.Library( 'expression_trigonometric.cpp', 'javascript_execution.cpp', 'make_js_function.cpp', - 'monotonic_expression.cpp', 'variables.cpp', ], LIBDEPS=[ @@ -113,7 +114,6 @@ env.Library( '$BUILD_DIR/mongo/db/query/collation/collator_factory_interface', '$BUILD_DIR/mongo/db/query/datetime/date_time_support', '$BUILD_DIR/mongo/db/query/query_knobs', - '$BUILD_DIR/mongo/db/query/query_shape/query_shape_common', '$BUILD_DIR/mongo/db/server_options_core', '$BUILD_DIR/mongo/db/service_context', '$BUILD_DIR/mongo/db/stats/counters', @@ -165,7 +165,6 @@ env.Library( 'accumulator_rank.cpp', 'accumulator_std_dev.cpp', 'accumulator_sum.cpp', - 'map_reduce_options.idl', 'window_function/window_bounds.cpp', 'window_function/window_function_covariance.cpp', 'window_function/window_function_count.cpp', @@ -184,9 +183,8 @@ env.Library( ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/exec/sort_executor', - '$BUILD_DIR/mongo/db/index/index_access_method', - '$BUILD_DIR/mongo/idl/idl_parser', - ], + '$BUILD_DIR/mongo/db/index/key_generator' + ] ) env.Library( @@ -273,8 +271,6 @@ pipelineEnv.Library( 'abt/field_map_builder.cpp', 'abt/match_expression_visitor.cpp', 'abt/utils.cpp', - '$BUILD_DIR/mongo/db/query/query_shape/agg_cmd_shape.cpp', - '$BUILD_DIR/mongo/db/query/query_stats/agg_key.cpp', 'document_source.cpp', 'document_source_add_fields.cpp', 'document_source_bucket.cpp', @@ -291,15 +287,11 @@ pipelineEnv.Library( 'document_source_geo_near.cpp', 'document_source_graph_lookup.cpp', 'document_source_group.cpp', - 'document_source_group_base.cpp', 'document_source_index_stats.cpp', - 'document_source_internal_all_collection_stats.cpp', 'document_source_internal_compute_geo_near_distance.cpp', 'document_source_internal_convert_bucket_index_stats.cpp', 'document_source_internal_inhibit_optimization.cpp', 'document_source_internal_shard_filter.cpp', - 'document_source_internal_shred_documents.cpp', - 'document_source_internal_shardserver_info.cpp', 'document_source_internal_split_pipeline.cpp', 'document_source_internal_unpack_bucket.cpp', 'document_source_limit.cpp', @@ -314,7 +306,6 @@ pipelineEnv.Library( 'document_source_out.cpp', 'document_source_plan_cache_stats.cpp', 'document_source_project.cpp', - 'document_source_query_stats.cpp', 'document_source_queue.cpp', 'document_source_redact.cpp', 'document_source_replace_root.cpp', @@ -323,16 +314,13 @@ pipelineEnv.Library( 'document_source_sequential_document_cache.cpp', 'document_source_set_variable_from_subpipeline.cpp', 'document_source_set_window_fields.cpp', - 'document_source_sharded_data_distribution.cpp', 'document_source_single_document_transformation.cpp', 'document_source_skip.cpp', 'document_source_sort.cpp', 'document_source_sort_by_count.cpp', - 'document_source_streaming_group.cpp', 'document_source_tee_consumer.cpp', 'document_source_union_with.cpp', 'document_source_unwind.cpp', - 'group_from_first_document_transformation.cpp', 'pipeline.cpp', 'search_helper.cpp', 'semantic_analysis.cpp', @@ -353,6 +341,7 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/client/clientdriver_minimal', '$BUILD_DIR/mongo/db/auth/auth', '$BUILD_DIR/mongo/db/bson/dotted_path_support', + '$BUILD_DIR/mongo/db/curop', '$BUILD_DIR/mongo/db/curop_failpoint_helpers', '$BUILD_DIR/mongo/db/exec/bucket_unpacker', '$BUILD_DIR/mongo/db/exec/document_value/document_value', @@ -360,7 +349,7 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/db/exec/scoped_timer', '$BUILD_DIR/mongo/db/exec/sort_executor', '$BUILD_DIR/mongo/db/generic_cursor', - '$BUILD_DIR/mongo/db/index/index_access_method', + '$BUILD_DIR/mongo/db/index/key_generator', '$BUILD_DIR/mongo/db/logical_session_cache', '$BUILD_DIR/mongo/db/logical_session_id_helpers', '$BUILD_DIR/mongo/db/matcher/expressions', @@ -388,7 +377,6 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/s/is_mongos', '$BUILD_DIR/third_party/shim_snappy', 'accumulator', - 'change_stream_helpers', 'dependencies', 'document_path_support', 'document_sources_idl', @@ -396,7 +384,6 @@ pipelineEnv.Library( 'granularity_rounder', ], LIBDEPS_PRIVATE=[ - '$BUILD_DIR/mongo/db/commands/fsync_locked', '$BUILD_DIR/mongo/db/commands/test_commands_enabled', '$BUILD_DIR/mongo/db/fts/base_fts', '$BUILD_DIR/mongo/db/mongohasher', @@ -404,7 +391,6 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/db/query/projection_ast', '$BUILD_DIR/mongo/db/repl/image_collection_entry', '$BUILD_DIR/mongo/db/sorter/sorter_idl', - '$BUILD_DIR/mongo/db/sorter/sorter_stats', '$BUILD_DIR/mongo/db/timeseries/timeseries_conversion_util', '$BUILD_DIR/mongo/db/timeseries/timeseries_options', '$BUILD_DIR/mongo/rpc/command_status', @@ -434,7 +420,6 @@ env.Library( 'change_stream_filter_helpers.cpp', 'change_stream_helpers_legacy.cpp', 'change_stream_rewrite_helpers.cpp', - 'change_stream_split_event_helpers.cpp', 'document_source_change_stream.cpp', 'document_source_change_stream_add_post_image.cpp', 'document_source_change_stream_check_invalidate.cpp', @@ -444,7 +429,6 @@ env.Library( 'document_source_change_stream_handle_topology_change.cpp', 'document_source_change_stream_add_pre_image.cpp', 'document_source_change_stream_oplog_match.cpp', - 'document_source_change_stream_split_large_event.cpp', 'document_source_change_stream_transform.cpp', 'document_source_change_stream_unwind_transaction.cpp', ], @@ -455,7 +439,6 @@ env.Library( '$BUILD_DIR/mongo/db/pipeline/sharded_agg_helpers', '$BUILD_DIR/mongo/db/update/update_driver', '$BUILD_DIR/mongo/s/query/router_exec_stage', - 'change_stream_helpers', 'change_stream_preimage', ], ) @@ -478,21 +461,17 @@ env.Library( 'document_source_coll_stats.idl', 'document_source_densify.idl', 'document_source_fill.idl', - 'document_source_internal_all_collection_stats.idl', 'document_source_internal_apply_oplog_update.idl', 'document_source_list_sessions.idl', 'document_source_merge.idl', 'document_source_merge_modes.idl', 'document_source_merge_spec.cpp', 'document_source_parsing_validators.cpp', - 'document_source_query_stats.idl', - 'document_source_query_stats_validators.cpp', 'document_source_replace_root.idl', 'document_source_set_window_fields.idl', 'document_source_union_with.idl', 'document_source_set_variable_from_subpipeline.idl', 'exchange_spec.idl', - 'resume_token.idl', 'resume_token.cpp', 'storage_stats_spec.idl', 'value.idl', @@ -500,8 +479,6 @@ env.Library( LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/exec/document_value/document_value', - '$BUILD_DIR/mongo/db/query/query_shape/query_shape', - '$BUILD_DIR/mongo/db/query/query_stats/query_stats_parse', '$BUILD_DIR/mongo/db/storage/key_string', '$BUILD_DIR/mongo/idl/idl_parser', '$BUILD_DIR/mongo/s/common_s', @@ -516,7 +493,6 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/db/change_stream_options_manager', - '$BUILD_DIR/mongo/db/concurrency/exception_util', '$BUILD_DIR/mongo/db/db_raii', '$BUILD_DIR/mongo/db/query_exec', '$BUILD_DIR/mongo/db/record_id_helpers', @@ -545,17 +521,6 @@ env.Library( ) env.Library( - target="change_stream_helpers", - source=[ - "change_stream_helpers.cpp", - ], - LIBDEPS=[ - "document_sources_idl", - ], - LIBDEPS_PRIVATE=[], -) - -env.Library( target="change_stream_test_helpers", source=[ "change_stream_test_helpers.cpp", @@ -577,21 +542,17 @@ env.CppUnitTest( 'change_stream_event_transform_test.cpp', 'change_stream_expired_pre_image_remover_test.cpp', 'change_stream_rewrites_test.cpp', - 'change_stream_split_event_helpers_test.cpp', 'dependencies_test.cpp', 'dispatch_shard_pipeline_test.cpp', 'document_path_support_test.cpp', 'document_source_add_fields_test.cpp', 'document_source_bucket_auto_test.cpp', 'document_source_bucket_test.cpp', - 'document_source_change_stream_add_post_image_test.cpp', 'document_source_change_stream_test.cpp', 'document_source_check_resume_token_test.cpp', - 'document_source_coll_stats_test.cpp', 'document_source_count_test.cpp', 'document_source_current_op_test.cpp', 'document_source_densify_test.cpp', - 'document_source_documents_test.cpp', 'document_source_exchange_test.cpp', 'document_source_facet_test.cpp', 'document_source_find_and_modify_image_lookup_test.cpp', @@ -602,6 +563,7 @@ env.CppUnitTest( 'document_source_internal_shard_filter_test.cpp', 'document_source_internal_split_pipeline_test.cpp', 'document_source_limit_test.cpp', + 'document_source_change_stream_add_post_image_test.cpp', 'document_source_lookup_test.cpp', 'document_source_match_test.cpp', 'document_source_merge_cursors_test.cpp', @@ -620,11 +582,8 @@ env.CppUnitTest( 'document_source_skip_test.cpp', 'document_source_sort_by_count_test.cpp', 'document_source_sort_test.cpp', - 'document_source_query_stats_test.cpp', 'document_source_union_with_test.cpp', - 'document_source_internal_all_collection_stats_test.cpp', 'document_source_internal_compute_geo_near_distance_test.cpp', - 'document_source_internal_convert_bucket_index_stats_test.cpp', 'document_source_internal_unpack_bucket_test/extract_or_build_project_to_internalize_test.cpp', 'document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp', 'document_source_internal_unpack_bucket_test/extract_project_for_pushdown_test.cpp', @@ -645,9 +604,7 @@ env.CppUnitTest( 'expression_date_test.cpp', 'expression_field_path_test.cpp', 'expression_find_internal_test.cpp', - 'expression_function_test.cpp', 'expression_javascript_test.cpp' if get_option('js-engine') != 'none' else [], - 'expression_let_test.cpp', 'expression_nary_test.cpp', 'expression_object_test.cpp', 'expression_or_test.cpp', @@ -662,7 +619,6 @@ env.CppUnitTest( 'granularity_rounder_preferred_numbers_test.cpp', 'lookup_set_cache_test.cpp', 'memory_usage_tracker_test.cpp', - 'monotonic_expression_test.cpp', 'partition_key_comparator_test.cpp', 'pipeline_metadata_tree_test.cpp', 'pipeline_test.cpp', diff --git a/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp b/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp index a2bf9a28656..582f3263bfb 100644 --- a/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp +++ b/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp @@ -59,7 +59,6 @@ #include "mongo/db/pipeline/document_source_operation_metrics.h" #include "mongo/db/pipeline/document_source_out.h" #include "mongo/db/pipeline/document_source_plan_cache_stats.h" -#include "mongo/db/pipeline/document_source_query_stats.h" #include "mongo/db/pipeline/document_source_queue.h" #include "mongo/db/pipeline/document_source_redact.h" #include "mongo/db/pipeline/document_source_replace_root.h" @@ -180,7 +179,7 @@ private: } void processProjectedPaths(const projection_executor::InclusionNode& node) { - OrderedPathSet preservedPaths; + std::set<std::string> preservedPaths; node.reportProjectedPaths(&preservedPaths); for (const std::string& preservedPathStr : preservedPaths) { @@ -195,7 +194,7 @@ private: void processComputedPaths(const projection_executor::InclusionNode& node, const std::string& rootProjection, const bool isAddingFields) { - OrderedPathSet computedPaths; + std::set<std::string> computedPaths; StringMap<std::string> renamedPaths; node.reportComputedPaths(&computedPaths, &renamedPaths); @@ -269,7 +268,7 @@ private: } void visitExclusionNode(const projection_executor::ExclusionNode& node) { - OrderedPathSet preservedPaths; + std::set<std::string> preservedPaths; node.reportProjectedPaths(&preservedPaths); for (const std::string& preservedPathStr : preservedPaths) { @@ -607,10 +606,6 @@ public: unsupportedStage(source); } - void visit(const DocumentSourceQueryStats* source) override { - unsupportedStage(source); - } - void visit(const DocumentSourceUnionWith* source) override { auto entry = _ctx.getNode(); ProjectionName unionProjName = entry._rootProjection; diff --git a/src/mongo/db/pipeline/abt/agg_expression_visitor.cpp b/src/mongo/db/pipeline/abt/agg_expression_visitor.cpp index 70e44feef97..55b44e4d37b 100644 --- a/src/mongo/db/pipeline/abt/agg_expression_visitor.cpp +++ b/src/mongo/db/pipeline/abt/agg_expression_visitor.cpp @@ -735,10 +735,6 @@ public: unsupportedExpression("tsIncrement"); } - void visit(const ExpressionInternalKeyStringValue* expr) override final { - unsupportedExpression(expr->getOpName()); - } - private: /** * Shared logic for $and, $or. Converts each child into an EExpression that evaluates to Boolean diff --git a/src/mongo/db/pipeline/abt/field_map_builder.h b/src/mongo/db/pipeline/abt/field_map_builder.h index 4eedf150644..af593d38cd3 100644 --- a/src/mongo/db/pipeline/abt/field_map_builder.h +++ b/src/mongo/db/pipeline/abt/field_map_builder.h @@ -57,7 +57,7 @@ struct FieldMapEntry { bool _hasDrop = false; std::string _constVarName; - OrderedPathSet _childPaths; + std::set<std::string> _childPaths; }; class FieldMapBuilder { diff --git a/src/mongo/db/pipeline/accumulation_statement.cpp b/src/mongo/db/pipeline/accumulation_statement.cpp index cb5efcf81ec..d5afb1f83d0 100644 --- a/src/mongo/db/pipeline/accumulation_statement.cpp +++ b/src/mongo/db/pipeline/accumulation_statement.cpp @@ -38,7 +38,6 @@ #include "mongo/db/exec/document_value/value.h" #include "mongo/db/pipeline/accumulator.h" #include "mongo/db/query/allowed_contexts.h" -#include "mongo/db/stats/counters.h" #include "mongo/util/assert_util.h" #include "mongo/util/str.h" #include "mongo/util/string_map.h" @@ -64,7 +63,6 @@ void AccumulationStatement::registerAccumulator( str::stream() << "Duplicate accumulator (" << name << ") registered.", it == parserMap.end()); parserMap[name] = {parser, allowedWithApiStrict, allowedWithClientType, requiredMinVersion}; - operatorCountersGroupAccumulatorExpressions.addCounter(name); } AccumulationStatement::ParserRegistration& AccumulationStatement::getParser(StringData name) { @@ -120,8 +118,6 @@ AccumulationStatement AccumulationStatement::parseAccumulationStatement( tassert(5837900, "Accumulators should only appear in a user operation", expCtx->opCtx); assertLanguageFeatureIsAllowed( expCtx->opCtx, accName.toString(), allowedWithApiStrict, allowedWithClientType); - - expCtx->incrementGroupAccumulatorExprCounter(accName); auto accExpr = parser(expCtx, specElem, vps); return AccumulationStatement(fieldName.toString(), std::move(accExpr)); diff --git a/src/mongo/db/pipeline/accumulator.h b/src/mongo/db/pipeline/accumulator.h index 0fbc3c60baf..114ccc260ce 100644 --- a/src/mongo/db/pipeline/accumulator.h +++ b/src/mongo/db/pipeline/accumulator.h @@ -42,7 +42,6 @@ #include "mongo/db/exec/document_value/value_comparator.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/query/query_shape/serialization_options.h" #include "mongo/stdx/unordered_set.h" #include "mongo/util/summation.h" @@ -135,12 +134,12 @@ public: */ virtual Document serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options = {}) const { + bool explain) const { ExpressionConstant const* ec = dynamic_cast<ExpressionConstant const*>(initializer.get()); invariant(ec); invariant(ec->getValue().nullish()); - return DOC(getOpName() << argument->serialize(options)); + return DOC(getOpName() << argument->serialize(explain)); } virtual AccumulatorDocumentsNeeded documentsNeeded() const { diff --git a/src/mongo/db/pipeline/accumulator_js_reduce.cpp b/src/mongo/db/pipeline/accumulator_js_reduce.cpp index bd8c5ccf882..71075978d6d 100644 --- a/src/mongo/db/pipeline/accumulator_js_reduce.cpp +++ b/src/mongo/db/pipeline/accumulator_js_reduce.cpp @@ -32,7 +32,6 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/pipeline/accumulator_js_reduce.h" #include "mongo/db/pipeline/make_js_function.h" -#include "mongo/db/pipeline/map_reduce_options_gen.h" namespace mongo { @@ -121,52 +120,47 @@ Value AccumulatorInternalJsReduce::getValue(bool toBeMerged) { if (_values.size() < 1) { return Value{}; } - Value result; - if (mrSingleReduceOptimizationEnabled && _values.size() == 1) { - // This optimization existed in the old Pre-4.4 MapReduce implementation. If the flag is - // set, then we should replicate the optimization. See SERVER-68766 for more details. - result = std::move(_values[0]); - } else { - const auto keySize = _key.getApproximateSize(); - - // Keep reducing until we have exactly one value. - while (true) { - BSONArrayBuilder bsonValues; - size_t numLeft = _values.size(); - for (; numLeft > 0; numLeft--) { - Value val = _values[numLeft - 1]; - - // Do not insert if doing so would exceed the the maximum allowed BSONObj size. - if (bsonValues.len() + keySize + val.getApproximateSize() > BSONObjMaxUserSize) { - // If we have reached the threshold for maximum allowed BSONObj size and only - // have a single value then no progress will be made on reduce. We must fail - // when this scenario is encountered. - size_t numNextReduce = _values.size() - numLeft; - uassert(31392, "Value too large to reduce", numNextReduce > 1); - break; - } - bsonValues << val; - } - auto expCtx = getExpressionContext(); - auto reduceFunc = makeJsFunc(expCtx, _funcSource); - - // Function signature: reduce(key, values). - BSONObj params = BSON_ARRAY(_key << bsonValues.arr()); - // For reduce, the key and values are both passed as 'params' so there's no need to set - // 'this'. - BSONObj thisObj; - Value reduceResult = - expCtx->getJsExecWithScope()->callFunction(reduceFunc, params, thisObj); - if (numLeft == 0) { - result = reduceResult; + const auto keySize = _key.getApproximateSize(); + + Value result; + // Keep reducing until we have exactly one value. + while (true) { + BSONArrayBuilder bsonValues; + size_t numLeft = _values.size(); + for (; numLeft > 0; numLeft--) { + Value val = _values[numLeft - 1]; + + // Do not insert if doing so would exceed the the maximum allowed BSONObj size. + if (bsonValues.len() + keySize + val.getApproximateSize() > BSONObjMaxUserSize) { + // If we have reached the threshold for maximum allowed BSONObj size and only have a + // single value then no progress will be made on reduce. We must fail when this + // scenario is encountered. + size_t numNextReduce = _values.size() - numLeft; + uassert(31392, "Value too large to reduce", numNextReduce > 1); break; - } else { - // Remove all values which have been reduced. - _values.resize(numLeft); - // Include most recent result in the set of values to be reduced. - _values.push_back(reduceResult); } + bsonValues << val; + } + + auto expCtx = getExpressionContext(); + auto reduceFunc = makeJsFunc(expCtx, _funcSource); + + // Function signature: reduce(key, values). + BSONObj params = BSON_ARRAY(_key << bsonValues.arr()); + // For reduce, the key and values are both passed as 'params' so there's no need to set + // 'this'. + BSONObj thisObj; + Value reduceResult = + expCtx->getJsExecWithScope()->callFunction(reduceFunc, params, thisObj); + if (numLeft == 0) { + result = reduceResult; + break; + } else { + // Remove all values which have been reduced. + _values.resize(numLeft); + // Include most recent result in the set of values to be reduced. + _values.push_back(reduceResult); } } @@ -196,9 +190,8 @@ void AccumulatorInternalJsReduce::reset() { // Returns this accumulator serialized as a Value along with the reduce function. Document AccumulatorInternalJsReduce::serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options) const { - return DOC(kName << DOC("data" << argument->serialize(options) << "eval" - << options.serializeLiteral(_funcSource))); + bool explain) const { + return DOC(kName << DOC("data" << argument->serialize(explain) << "eval" << _funcSource)); } REGISTER_ACCUMULATOR(accumulator, AccumulatorJs::parse); @@ -236,16 +229,15 @@ std::string parseFunction(StringData fieldName, Document AccumulatorJs::serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options) const { + bool explain) const { MutableDocument args; - - args.addField("init", options.serializeLiteral(_init)); - args.addField("initArgs", initializer->serialize(options)); - args.addField("accumulate", options.serializeLiteral(_accumulate)); - args.addField("accumulateArgs", argument->serialize(options)); - args.addField("merge", options.serializeLiteral(_merge)); + args.addField("init", Value(_init)); + args.addField("initArgs", Value(initializer->serialize(explain))); + args.addField("accumulate", Value(_accumulate)); + args.addField("accumulateArgs", Value(argument->serialize(explain))); + args.addField("merge", Value(_merge)); if (_finalize) { - args.addField("finalize", options.serializeLiteral(*_finalize)); + args.addField("finalize", Value(*_finalize)); } args.addField("lang", Value("js"_sd)); return DOC(kName << args.freeze()); diff --git a/src/mongo/db/pipeline/accumulator_js_reduce.h b/src/mongo/db/pipeline/accumulator_js_reduce.h index 9e3a6e63d40..af025132ee0 100644 --- a/src/mongo/db/pipeline/accumulator_js_reduce.h +++ b/src/mongo/db/pipeline/accumulator_js_reduce.h @@ -66,7 +66,7 @@ public: virtual Document serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options = {}) const override; + bool explain) const override; private: static std::string parseReduceFunction(BSONElement func); @@ -101,9 +101,10 @@ public: void processInternal(const Value& input, bool merging) final; void reduceMemoryConsumptionIfAble() final; + Document serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options = {}) const final; + bool explain) const final; void startNewGroup(Value const& input) final; private: diff --git a/src/mongo/db/pipeline/accumulator_js_test.cpp b/src/mongo/db/pipeline/accumulator_js_test.cpp index 2a44e8e967c..953b4ce475a 100644 --- a/src/mongo/db/pipeline/accumulator_js_test.cpp +++ b/src/mongo/db/pipeline/accumulator_js_test.cpp @@ -38,7 +38,6 @@ #include "mongo/db/pipeline/process_interface/standalone_process_interface.h" #include "mongo/db/service_context_d_test_fixture.h" #include "mongo/dbtests/dbtests.h" -#include "mongo/idl/server_parameter_test_util.h" #include "mongo/scripting/engine.h" namespace mongo { @@ -198,38 +197,6 @@ TEST_F(MapReduceFixture, InternalJsReduceFailsWhenEvalContainsInvalidJavascript) } } -TEST_F( - MapReduceFixture, - InternalJsReduceFailsDependentOnDocumentCountWhenEvalIsInvalidJavascriptWithSingleReduceOpt) { - RAIIServerParameterControllerForTest flag("mrEnableSingleReduceOptimization", true); - std::string eval("INVALID_JAVASCRIPT"); - // Multiple source documents should evaluate the passed in function and return an error with - // invalid javascript. - { - auto accum = AccumulatorInternalJsReduce::create(getExpCtx(), "INVALID_JAVASCRIPT"); - auto input = Value(DOC("k" << Value(1) << "v" << Value(2))); - accum->process(input, false); - accum->process(input, false); - - ASSERT_THROWS_CODE(accum->getValue(false), DBException, ErrorCodes::JSInterpreterFailure); - } - - // Single source document. With the reduce optimization, we simply return this document rather - // than executing the JS engine at all, so no error is thrown. - { - auto accum = AccumulatorInternalJsReduce::create(getExpCtx(), "INVALID_JAVASCRIPT"); - - auto input = Value(DOC("k" << Value(1) << "v" << Value(2))); - auto expectedResult = Value(2); - - accum->process(input, false); - Value result = accum->getValue(false); - - ASSERT_VALUE_EQ(expectedResult, result); - ASSERT_EQUALS(expectedResult.getType(), result.getType()); - } -} - TEST_F(MapReduceFixture, InternalJsReduceFailsIfArgumentNotDocument) { auto argument = Value(2); assertProcessFailsWithCode<AccumulatorInternalJsReduce>( diff --git a/src/mongo/db/pipeline/accumulator_multi.cpp b/src/mongo/db/pipeline/accumulator_multi.cpp index 26cfa570d91..2f6971e1f22 100644 --- a/src/mongo/db/pipeline/accumulator_multi.cpp +++ b/src/mongo/db/pipeline/accumulator_multi.cpp @@ -178,9 +178,9 @@ const char* AccumulatorMinMaxN::getOpName() const { Document AccumulatorMinMaxN::serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options) const { + bool explain) const { MutableDocument args; - AccumulatorN::serializeHelper(initializer, argument, options, args); + AccumulatorN::serializeHelper(initializer, argument, explain, args); return DOC(getOpName() << args.freeze()); } @@ -230,10 +230,10 @@ void AccumulatorN::updateAndCheckMemUsage(size_t memAdded) { void AccumulatorN::serializeHelper(const boost::intrusive_ptr<Expression>& initializer, const boost::intrusive_ptr<Expression>& argument, - const SerializationOptions& options, + bool explain, MutableDocument& md) { - md.addField(kFieldNameN, Value(initializer->serialize(options))); - md.addField(kFieldNameInput, Value(argument->serialize(options))); + md.addField(kFieldNameN, Value(initializer->serialize(explain))); + md.addField(kFieldNameInput, Value(argument->serialize(explain))); } template <MinMaxSense s> @@ -385,9 +385,9 @@ const char* AccumulatorFirstLastN::getOpName() const { Document AccumulatorFirstLastN::serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options) const { + bool explain) const { MutableDocument args; - AccumulatorN::serializeHelper(initializer, argument, options, args); + AccumulatorN::serializeHelper(initializer, argument, explain, args); return DOC(getOpName() << args.freeze()); } @@ -497,7 +497,7 @@ AccumulatorTopBottomN<sense, single>::AccumulatorTopBottomN(ExpressionContext* c int sortOrder = 0; for (auto part : _sortPattern) { const auto newFieldName = - (StringBuilder() << AccumulatorN::kFieldNameSortFields << sortOrder).str(); + (StringBuilder() << AccumulatorN::kFieldNameSortFields << "." << sortOrder).str(); part.fieldPath.reset(FieldPath(newFieldName)); if (part.expression) { @@ -531,46 +531,32 @@ template <TopBottomSense sense, bool single> Document AccumulatorTopBottomN<sense, single>::serialize( boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options) const { + bool explain) const { MutableDocument args; if constexpr (!single) { - args.addField(kFieldNameN, Value(initializer->serialize(options))); + args.addField(kFieldNameN, Value(initializer->serialize(explain))); } - - // If 'argument' is either an ExpressionObject or an ExpressionConstant of object type, then - // we are serializing the original expression under the 'output' field of the object. Otherwise, - // we're serializing a custom expression for merging group. - if (auto argObj = dynamic_cast<ExpressionObject*>(argument.get())) { - bool foundOutputField = false; - for (auto& child : argObj->getChildExpressions()) { - if (child.first == kFieldNameOutput) { - auto output = child.second->serialize(options); - args.addField(kFieldNameOutput, output); - foundOutputField = true; - break; - } - } - tassert(7773700, "'output' field should be present.", foundOutputField); - } else if (auto argConst = dynamic_cast<ExpressionConstant*>(argument.get())) { - auto output = argConst->getValue().getDocument()[kFieldNameOutput]; - tassert(7773701, "'output' field should be present.", !output.missing()); - args.addField(kFieldNameOutput, output); + auto serializedArg = argument->serialize(explain); + + // If 'argument' contains a field named 'output', this means that we are serializing the + // accumulator's original output expression under the field name 'output'. Otherwise, we are + // serializing a custom argument under the field name 'output'. For instance, a merging $group + // will provide an argument that merges multiple partial groups. + if (auto output = serializedArg[kFieldNameOutput]; !output.missing()) { + args.addField(kFieldNameOutput, Value(output)); } else { - auto serializedArg = argument->serialize(options); args.addField(kFieldNameOutput, serializedArg); } - args.addField(kFieldNameSortBy, Value(_sortPattern.serialize( - SortPattern::SortKeySerialization::kForPipelineSerialization, options))); + SortPattern::SortKeySerialization::kForPipelineSerialization))); return DOC(getOpName() << args.freeze()); } template <TopBottomSense sense> std::pair<SortPattern, BSONArray> parseAccumulatorTopBottomNSortBy(ExpressionContext* const expCtx, BSONObj sortBy) { - SortPattern sortPattern(sortBy, expCtx); BSONArrayBuilder sortFieldsExpBab; BSONObjIterator sortByBoi(sortBy); @@ -582,7 +568,7 @@ std::pair<SortPattern, BSONArray> parseAccumulatorTopBottomNSortBy(ExpressionCon // since the evaluated argument wouldn't have the same metadata as the original // document. Instead we use [{$meta: "textScore"}] as the sortFields expression so the // sortFields array contains the data we need for sorting. - const auto serialized = part.expression->serialize(); + const auto serialized = part.expression->serialize(false); sortFieldsExpBab.append(serialized.getDocument().toBson()); } else { sortFieldsExpBab.append((StringBuilder() << "$" << fieldName).str()); @@ -595,27 +581,18 @@ template <TopBottomSense sense, bool single> AccumulationExpression AccumulatorTopBottomN<sense, single>::parseTopBottomN( ExpressionContext* const expCtx, BSONElement elem, VariablesParseState vps) { auto name = AccumulatorTopBottomN<sense, single>::getName(); + const auto [n, output, sortBy] = accumulatorNParseArgs<single>(expCtx, elem, name.rawData(), true, vps); - auto [sortPattern, sortFieldsExp] = parseAccumulatorTopBottomNSortBy<sense>(expCtx, *sortBy); - expCtx->sbeGroupCompatible = false; + auto [sortPattern, sortFieldsExp] = parseAccumulatorTopBottomNSortBy<sense>(expCtx, *sortBy); // Construct argument expression. If given sortBy: {field1: 1, field2: 1} it will be shaped like - // {output: <output expression>, sortFields0: "$field1", sortFields1: "$field2"}. This projects - // out only the fields we need for sorting so we can use SortKeyComparator without copying the - // entire document. This argument expression will be evaluated and become the input to - // _processValue. - BSONObjBuilder argumentBuilder; - argumentBuilder.append(output); - int sortOrder = 0; - for (const auto& sortField : sortFieldsExp) { - argumentBuilder.appendAs( - sortField, (StringBuilder() << AccumulatorN::kFieldNameSortFields << sortOrder).str()); - sortOrder++; - } - boost::intrusive_ptr<Expression> argument = - Expression::parseObject(expCtx, argumentBuilder.obj(), vps); + // {output: <output expression>, sortFields: ["$field1", "$field2"]}. This projects out only the + // fields we need for sorting so we can use SortKeyComparator without copying the entire + // document. This argument expression will be evaluated and become the input to _processValue. + boost::intrusive_ptr<Expression> argument = Expression::parseObject( + expCtx, BSON(output << AccumulatorN::kFieldNameSortFields << sortFieldsExp), vps); auto factory = [expCtx, sortPattern = std::move(sortPattern)] { return make_intrusive<AccumulatorTopBottomN<sense, single>>( expCtx, sortPattern, /* isRemovable */ false); @@ -694,7 +671,14 @@ void AccumulatorTopBottomN<sense, single>::_processValue(const Value& val) { } } - const auto memUsage = keyOutPair.first.shred().getApproximateSize() + + // TODO SERVER-61281 consider removing this call to fillCache(). + // Since Document caches fields the size of this cache and getApproximateSize() can vary + // depending on access. In order to avoid this and make sure we subtract the right amount if + // remove() ever gets called, we can fill the cache to get a consistent view of the size. + // Normally the outer window function code handles this, but _genKeyOutPair() makes a new + // document for sortKey, so its cache get reset. + keyOutPair.first.fillCache(); + const auto memUsage = keyOutPair.first.getApproximateSize() + keyOutPair.second.getApproximateSize() + sizeof(KeyOutPair); updateAndCheckMemUsage(memUsage); _map->emplace(keyOutPair); @@ -714,7 +698,10 @@ void AccumulatorTopBottomN<sense, single>::remove(const Value& val) { auto it = _map->lower_bound(keyOutPair.first); _map->erase(it); - _memUsageBytes -= keyOutPair.first.shred().getApproximateSize() + + // TODO SERVER-61281 consider removing this comment if its no longer relevant. + // After calling lower_bound() it uses SortKeyComparator and the sortKey's field cache should be + // fully populated so no need to call fillCache() again. + _memUsageBytes -= keyOutPair.first.getApproximateSize() + keyOutPair.second.getApproximateSize() + sizeof(KeyOutPair); } diff --git a/src/mongo/db/pipeline/accumulator_multi.h b/src/mongo/db/pipeline/accumulator_multi.h index 32ced5b8c4e..13e7971ee0c 100644 --- a/src/mongo/db/pipeline/accumulator_multi.h +++ b/src/mongo/db/pipeline/accumulator_multi.h @@ -34,7 +34,6 @@ #include "mongo/db/exec/sort_key_comparator.h" #include "mongo/db/index/sort_key_generator.h" #include "mongo/db/pipeline/accumulation_statement.h" -#include "mongo/db/query/query_shape/serialization_options.h" namespace mongo { @@ -86,7 +85,7 @@ public: */ static void serializeHelper(const boost::intrusive_ptr<Expression>& initializer, const boost::intrusive_ptr<Expression>& argument, - const SerializationOptions& options, + bool explain, MutableDocument& md); protected: @@ -137,7 +136,7 @@ public: Document serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options = {}) const final; + bool explain) const final; void reset() final; @@ -208,7 +207,7 @@ public: Document serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options = {}) const final; + bool explain) const final; void reset() final; @@ -323,7 +322,7 @@ public: Document serialize(boost::intrusive_ptr<Expression> initializer, boost::intrusive_ptr<Expression> argument, - const SerializationOptions& options = {}) const final; + bool explain) const final; void reset() final; diff --git a/src/mongo/db/pipeline/accumulator_test.cpp b/src/mongo/db/pipeline/accumulator_test.cpp index 3f3a4ba9385..d0e40c192ca 100644 --- a/src/mongo/db/pipeline/accumulator_test.cpp +++ b/src/mongo/db/pipeline/accumulator_test.cpp @@ -39,12 +39,10 @@ #include "mongo/db/pipeline/accumulation_statement.h" #include "mongo/db/pipeline/accumulator.h" #include "mongo/db/pipeline/accumulator_for_window_functions.h" -#include "mongo/db/pipeline/accumulator_js_reduce.h" #include "mongo/db/pipeline/accumulator_multi.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/collation/collator_interface_mock.h" -#include "mongo/db/query/query_shape/serialization_options.h" #include "mongo/dbtests/dbtests.h" #include "mongo/idl/server_parameter_test_util.h" #include "mongo/logv2/log.h" @@ -605,8 +603,8 @@ TEST(Accumulators, TopBottomNRespectsCollation) { expCtx->setCollator(std::move(collator)); const auto n = Value(2); auto mkdoc = [](Value a) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << a << (AccumulatorN::kFieldNameSortFields + "0") << a)); + return Value(BSON(AccumulatorN::kFieldNameOutput << a << AccumulatorN::kFieldNameSortFields + << BSON_ARRAY(a))); }; OperationsType bottomCasesAscending{ @@ -667,12 +665,12 @@ TEST(Accumulators, TopNDescendingBottomNAscending) { const auto n3 = Value(3); const auto n1 = Value(1); auto mkdoc = [](Value a) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << a << (AccumulatorN::kFieldNameSortFields + "0") << a)); + return Value(BSON(AccumulatorN::kFieldNameOutput << a << AccumulatorN::kFieldNameSortFields + << BSON_ARRAY(a))); }; auto mkdoc2 = [](int a, Value b) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << b << (AccumulatorN::kFieldNameSortFields + "0") << a)); + return Value(BSON(AccumulatorN::kFieldNameOutput << b << AccumulatorN::kFieldNameSortFields + << BSON_ARRAY(a))); }; OperationsType cases{ // Basic tests. @@ -806,12 +804,12 @@ TEST(Accumulators, TopNAscendingBottomNDescending) { const auto n3 = Value(3); const auto n1 = Value(1); auto mkdoc = [](Value a) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << a << (AccumulatorN::kFieldNameSortFields + "0") << a)); + return Value(BSON(AccumulatorN::kFieldNameOutput << a << AccumulatorN::kFieldNameSortFields + << BSON_ARRAY(a))); }; auto mkdoc2 = [](int a, Value b) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << b << (AccumulatorN::kFieldNameSortFields + "0") << a)); + return Value(BSON(AccumulatorN::kFieldNameOutput << b << AccumulatorN::kFieldNameSortFields + << BSON_ARRAY(a))); }; OperationsType cases{ // Basic tests. @@ -947,133 +945,6 @@ TEST(Accumulators, TopNAscendingBottomNDescending) { } } -TEST(Accumulators, TopBottomNMultiSortPattern) { - auto expCtx = make_intrusive<ExpressionContextForTest>(); - const auto n = Value(3); - auto mkdoc = [](int32_t a, int32_t b) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << (a * 10 + b) << (AccumulatorN::kFieldNameSortFields + "0") << a - << (AccumulatorN::kFieldNameSortFields + "1") << b)); - }; - auto mkdoc2 = [](Value output, Value a, Value b) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << output << (AccumulatorN::kFieldNameSortFields + "0") << a - << (AccumulatorN::kFieldNameSortFields + "1") << b)); - }; - - OperationsType cases{ - // Basic tests. - {{mkdoc(3, 3), mkdoc(4, 4), mkdoc(4, 5), mkdoc(100, 10)}, - {Value(std::vector<Value>{Value(45), Value(44), Value(33)})}}, - {{mkdoc(9, 5), mkdoc(8, 2), mkdoc(9, 1), mkdoc(8, 1), mkdoc(1, 0)}, - {Value(std::vector<Value>{Value(82), Value(81), Value(10)})}}, - // 3 or fewer values results in those values being returned. - {{mkdoc(9, 9), mkdoc(8, 8), mkdoc(9, 8)}, - {Value(std::vector<Value>{Value(99), Value(98), Value(88)})}}, - {{mkdoc(9, 9)}, {Value(std::vector<Value>{Value(99)})}}, - - // Ties are broken arbitrarily. - {{mkdoc(9, 9), mkdoc(9, 9), mkdoc(1, 0), mkdoc(9, 9), mkdoc(1, 0), mkdoc(9, 9)}, - {Value(std::vector<Value>{Value(99), Value(10), Value(10)})}}, - - // Null/missing cases (missing and null are NOT ignored, but missing is upconverted to - // null). - {{mkdoc(9, 9), - mkdoc2(Value(BSONNULL), Value(BSONNULL), Value(BSONNULL)), - mkdoc2(Value(BSONNULL), Value(), Value(BSONNULL)), - mkdoc2(Value(), Value(BSONNULL), Value()), - mkdoc2(Value(), Value(BSONNULL), Value(BSONNULL))}, - {Value(std::vector<Value>{Value(BSONNULL), Value(BSONNULL), Value(BSONNULL)})}}}; - - try { - auto accInit = [&](ExpressionContext* const expCtx) -> intrusive_ptr<AccumulatorState> { - auto acc = AccumulatorTopBottomN<TopBottomSense::kBottom, false>::create( - expCtx, BSON("a" << -1 << "b" << -1)); - acc->startNewGroup(n); - return acc; - }; - assertExpectedResults(expCtx.get(), cases, accInit); - } catch (...) { - LOGV2(8236800, "bottom3 a: -1, b: -1"); - throw; - } - - // topN ascending will return same results, but in reverse order. - for (auto& [input, expected] : cases) { - tassert(8236803, "expected should be an array", expected.isArray()); - auto arr = expected.getArray(); - std::reverse(std::begin(arr), std::end(arr)); - expected = Value(arr); - } - - try { - auto accInit = [&](ExpressionContext* const expCtx) -> intrusive_ptr<AccumulatorState> { - auto acc = AccumulatorTopBottomN<TopBottomSense::kTop, false>::create( - expCtx, BSON("a" << 1 << "b" << 1)); - acc->startNewGroup(n); - return acc; - }; - assertExpectedResults(expCtx.get(), cases, accInit); - } catch (...) { - LOGV2(8236801, "top3 a: 1, b: 1"); - throw; - } -} - -template <TopBottomSense Sense> -void runTopBottomAccumulatorTest(ExpressionContext* const expCtx, - int32_t dir, - Value n, - OperationsType cases) { - try { - auto accInit = [&](ExpressionContext* const expCtx) -> intrusive_ptr<AccumulatorState> { - auto acc = AccumulatorTopBottomN<Sense, false>::create(expCtx, BSON("a" << dir)); - acc->startNewGroup(n); - return acc; - }; - assertExpectedResults(expCtx, cases, accInit); - } catch (...) { - LOGV2(8236802, - "failed top/bottom accumulator test", - "n"_attr = n, - "sort_dir"_attr = dir, - "sense"_attr = Sense); - throw; - } -} - -TEST(Accumulators, TopBottomNSortArray) { - auto expCtx = make_intrusive<ExpressionContextForTest>(); - Value n{2}; - Value arr1{std::vector<Value>{ - Value(6), - Value(1), - }}; - Value arr2{std::vector<Value>{ - Value(5), - Value(2), - }}; - Value arr3{std::vector<Value>{ - Value(4), - Value(3), - }}; - - auto mkdoc = [](int32_t id, Value arr) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << id << (AccumulatorN::kFieldNameSortFields + "0") << arr)); - }; - OperationsType topCases{{{mkdoc(1, arr1), mkdoc(2, arr2), mkdoc(3, arr3)}, - {Value(std::vector<Value>{Value(1), Value(2)})}}}; - OperationsType bottomCases{{{mkdoc(1, arr1), mkdoc(2, arr2), mkdoc(3, arr3)}, - {Value(std::vector<Value>{Value(2), Value(3)})}}}; - - runTopBottomAccumulatorTest<TopBottomSense::kTop>(expCtx.get(), 1, n, topCases); - runTopBottomAccumulatorTest<TopBottomSense::kTop>(expCtx.get(), -1, n, topCases); - - runTopBottomAccumulatorTest<TopBottomSense::kBottom>(expCtx.get(), 1, n, bottomCases); - runTopBottomAccumulatorTest<TopBottomSense::kBottom>(expCtx.get(), -1, n, bottomCases); -} - // Utility to test the single counterparts of the topN/bottomN accumulators. template <TopBottomSense s> void testSingle(OperationsType cases, ExpressionContext* const expCtx, const BSONObj& sortPattern) { @@ -1104,8 +975,8 @@ TEST(Accumulators, TopBottomSingle) { auto expCtx = make_intrusive<ExpressionContextForTest>(); const auto n = Value(1); auto mkdoc = [](Value a) { - return Value(BSON(AccumulatorN::kFieldNameOutput - << a << (AccumulatorN::kFieldNameSortFields + "0") << a)); + return Value(BSON(AccumulatorN::kFieldNameOutput << a << AccumulatorN::kFieldNameSortFields + << BSON_ARRAY(a))); }; const BSONObj ascSort = BSON("a" << 1); @@ -1211,8 +1082,9 @@ struct TopBottomNRemoveTest : public AggregationContextFixture { template <typename SortKeyType> void add(SortKeyType sortKey, int output) { - auto v = Value(BSON(AccumulatorN::kFieldNameOutput - << output << (AccumulatorN::kFieldNameSortFields + "0") << sortKey)); + auto v = + Value(BSON(AccumulatorN::kFieldNameOutput + << output << AccumulatorN::kFieldNameSortFields << BSON_ARRAY(sortKey))); _acc->process(v, false); _q.push(v); } @@ -1871,199 +1743,6 @@ TEST(Accumulators, CovarianceWithRandomVariables) { assertCovariance<AccumulatorCovarianceSamp>(&expCtx, randomVariables, boost::none); } -Value parseAndSerializeAccumExpr( - const BSONObj& obj, - std::function<boost::intrusive_ptr<Expression>( - ExpressionContext* expCtx, BSONElement, const VariablesParseState&)> func) { - SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST; - auto expCtx = make_intrusive<ExpressionContextForTest>(); - auto expr = func(expCtx.get(), obj.firstElement(), expCtx->variablesParseState); - return expr->serialize(options); -} - -Document parseAndSerializeAccum( - const BSONElement elem, - std::function<AccumulationExpression( - ExpressionContext* const expCtx, BSONElement, VariablesParseState)> func) { - SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST; - auto expCtx = make_intrusive<ExpressionContextForTest>(); - VariablesParseState vps = expCtx->variablesParseState; - - auto expr = func(expCtx.get(), elem, vps); - auto accum = expr.factory(); - return accum->serialize(expr.initializer, expr.argument, options); -} - -Document parseAndSerializeAccumRepresentative( - const BSONElement elem, - std::function<AccumulationExpression( - ExpressionContext* const expCtx, BSONElement, VariablesParseState)> func) { - SerializationOptions options = SerializationOptions::kRepresentativeQueryShapeSerializeOptions; - auto expCtx = make_intrusive<ExpressionContextForTest>(); - VariablesParseState vps = expCtx->variablesParseState; - - auto expr = func(expCtx.get(), elem, vps); - auto accum = expr.factory(); - return accum->serialize(expr.initializer, expr.argument, options); -} - -TEST(Accumulators, SerializeWithRedaction) { - auto jsReduce = - BSON("$accumulator" << BSON("init" - << "function() {}" - << "accumulateArgs" - << BSON_ARRAY("$a" - << "$b") - << "accumulate" - << "function(state, str1, str2) {return str1 + str2;}" - << "merge" - << "function(s1, s2) {return s1 || s2;}" - << "lang" - << "js")); - auto actual = parseAndSerializeAccum(jsReduce.firstElement(), &AccumulatorJs::parse); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$accumulator": { - "init": "?string", - "initArgs": "[]", - "accumulate": "?string", - "accumulateArgs": [ - "$HASH<a>", - "$HASH<b>" - ], - "merge": "?string", - "lang": "js" - } - })", - actual); - - auto topN = BSON("$topN" << BSON("n" << 3 << "output" - << "$output" - << "sortBy" << BSON("sortKey" << 1))); - actual = parseAndSerializeAccum( - topN.firstElement(), &AccumulatorTopBottomN<TopBottomSense::kTop, false>::parseTopBottomN); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$topN": { - "n": "?number", - "output": "$HASH<output>", - "sortBy": { - "HASH<sortKey>": 1 - } - } - })", - actual); - - auto addToSet = BSON("$addToSet" << BSON("a" << 5)); - actual = parseAndSerializeAccum(addToSet.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorAddToSet>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$addToSet":"?object"})", - actual); - - auto sum = BSON("$sum" << BSON_ARRAY(4 << 6)); - actual = parseAndSerializeAccum(sum.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorSum>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$sum": "?array<?number>"})", - actual); - - sum = BSON("$sum" << BSON_ARRAY("$a" << 5 << 3 << BSON("$sum" << BSON_ARRAY(4 << 6)))); - actual = parseAndSerializeAccum(sum.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorSum>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$sum":["$HASH<a>","?number","?number",{"$sum":"?array<?number>"}]})", - actual); - - auto mergeObjs = BSON("$mergeObjects" << BSON_ARRAY("$a" << BSON("b" - << "null"))); - actual = - parseAndSerializeAccum(mergeObjs.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorMergeObjects>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$mergeObjects":["$HASH<a>","?object"]})", - actual); - - auto push = BSON("$push" << BSON("$eq" << BSON_ARRAY("$str" - << "str2"))); - actual = parseAndSerializeAccum(push.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorPush>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$push":{"$eq":["$HASH<str>","?string"]}})", - actual); - - auto top = BSON("$top" << BSON("output" - << "$b" - << "sortBy" << BSON("sales" << 1))); - actual = parseAndSerializeAccum( - top.firstElement(), &AccumulatorTopBottomN<TopBottomSense::kTop, true>::parseTopBottomN); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$top": { - "output": "$HASH<b>", - "sortBy": { - "HASH<sales>": 1 - } - } - })", - actual); - - auto max = BSON("$max" << BSON_ARRAY( - "$a" << 2 << 3 << BSON("$max" << BSON_ARRAY(BSON_ARRAY("$b" << 4 << 5))))); - actual = parseAndSerializeAccum(max.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorMax>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$max": [ - "$HASH<a>", - "?number", - "?number", - { - "$max": [ - [ - "$HASH<b>", - "?number", - "?number" - ] - ] - } - ] - })", - actual); - - auto internalJsReduce = BSON( - "$_internalJsReduce" << BSON("data" - << "$emits" - << "eval" - << "function(key, values) {\n return Array.sum(values);\n")); - actual = parseAndSerializeAccum(internalJsReduce.firstElement(), - &AccumulatorInternalJsReduce::parseInternalJsReduce); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$_internalJsReduce":{"data":"$HASH<emits>","eval":"?string"}})", - actual); -} - -TEST(AccumulatorsToExpression, SerializeWithRedaction) { - auto maxN = BSON("$maxN" << BSON("n" << 3 << "input" << BSON_ARRAY(19 << 7 << 28 << 3 << 5))); - using Sense = AccumulatorMinMax::Sense; - auto actual = - parseAndSerializeAccumExpr(maxN, &AccumulatorMinMaxN::parseExpression<Sense::kMax>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$maxN":{"n":"?number","input":"?array<?number>"}})", - actual.getDocument()); - - auto firstN = BSON("$firstN" << BSON("input" - << "$sales" - << "n" - << "\'string\'")); - using FirstLastSense = AccumulatorFirstLastN::Sense; - actual = parseAndSerializeAccumExpr( - firstN, &AccumulatorFirstLastN::parseExpression<FirstLastSense::kFirst>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$firstN":{"n":"?string","input":"$HASH<sales>"}})", - actual.getDocument()); -} - /* ------------------------- AccumulatorMergeObjects -------------------------- */ TEST(AccumulatorMergeObjects, MergingZeroObjectsShouldReturnEmptyDocument) { @@ -2115,21 +1794,4 @@ TEST(AccumulatorMergeObjects, MergingWithEmptyDocumentShouldIgnore) { assertExpectedResults<AccumulatorMergeObjects>(&expCtx, {{{first, second}, expected}}); } -TEST(AccumulatorMergeObjects, RoundTripSerializationLiteral) { - auto mergeObjs = BSON("$mergeObjects" << BSON("$literal" << BSON_ARRAY(5 << true))); - auto actual = parseAndSerializeAccumRepresentative( - mergeObjs.firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorMergeObjects>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$mergeObjects":{"$const":[2,"or more types"]}})", - actual); - - auto roundTrip = parseAndSerializeAccumRepresentative( - actual.toBson().firstElement(), - &genericParseSingleExpressionAccumulator<AccumulatorMergeObjects>); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$mergeObjects":{"$const":[2,"or more types"]}})", - roundTrip); -} - } // namespace AccumulatorTests diff --git a/src/mongo/db/pipeline/aggregate_command.idl b/src/mongo/db/pipeline/aggregate_command.idl index 67c997dcd5d..b84513a30e6 100644 --- a/src/mongo/db/pipeline/aggregate_command.idl +++ b/src/mongo/db/pipeline/aggregate_command.idl @@ -66,13 +66,11 @@ types: structs: PassthroughToShardOptions: description: "options for commands requesting a per shard cursor" - query_shape_component: true fields: shard: description: "id of the shard to passthrough to" type: string unstable: true - query_shape: anonymize commands: aggregate: @@ -96,14 +94,6 @@ commands: - privilege: # $planCacheStats resource_pattern: exact_namespace action_type: planCacheRead - - privilege: # $queryStats - agg_stage: queryStats - resource_pattern: cluster - action_type: queryStatsRead - - privilege: # $queryStats - agg_stage: queryStats - resource_pattern: cluster - action_type: queryStatsReadTransformed - privilege: # $changeStream resource_pattern: exact_namespace action_type: changeStream @@ -166,18 +156,6 @@ commands: - privilege: # $backupCursorExtend, backupCursor resource_pattern: cluster action_type: fsync - - privilege: # $_internalAllCollectionStats - agg_stage: _internalAllCollectionStats - resource_pattern: cluster - action_type: allCollectionStats - - privilege: # $shardedDataDistribution - agg_stage: shardedDataDistribution - resource_pattern: cluster - action_type: shardedDataDistribution - - privilege: # $listSearchIndexes - agg_stage: listSearchIndexes - resource_pattern: database - action_type: listSearchIndexes # Note that the 'CursorInitialReply' is not the only response that an aggregate command # could return. With 'explain' or 'exchange', the response would not include the fields in # 'CursorInitialReply'. But using 'explain' or 'exchange' is unstable, but otherwise the diff --git a/src/mongo/db/pipeline/aggregation_context_fixture.h b/src/mongo/db/pipeline/aggregation_context_fixture.h index cce0283314c..a4dfcfeb769 100644 --- a/src/mongo/db/pipeline/aggregation_context_fixture.h +++ b/src/mongo/db/pipeline/aggregation_context_fixture.h @@ -31,10 +31,8 @@ #include <boost/intrusive_ptr.hpp> #include <memory> -#include <vector> #include "mongo/db/concurrency/locker_noop_client_observer.h" -#include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/service_context_test_fixture.h" #include "mongo/unittest/temp_dir.h" @@ -73,42 +71,6 @@ public: return _opCtx.get(); } - /* - * Serialize and redact a document source. - */ - BSONObj redact(const DocumentSource& docSource, - bool performRedaction = true, - boost::optional<ExplainOptions::Verbosity> verbosity = boost::none) { - SerializationOptions options; - options.verbosity = verbosity; - if (performRedaction) { - options.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString; - options.transformIdentifiersCallback = [](StringData s) -> std::string { - return str::stream() << "HASH<" << s << ">"; - }; - options.transformIdentifiers = true; - } - std::vector<Value> serialized; - docSource.serializeToArray(serialized, options); - ASSERT_EQ(1, serialized.size()); - return serialized[0].getDocument().toBson().getOwned(); - } - - std::vector<Value> redactToArray(const DocumentSource& docSource, - bool performRedaction = true) { - SerializationOptions options; - if (performRedaction) { - options.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString; - options.transformIdentifiersCallback = [](StringData s) -> std::string { - return str::stream() << "HASH<" << s << ">"; - }; - options.transformIdentifiers = true; - } - std::vector<Value> serialized; - docSource.serializeToArray(serialized, options); - return serialized; - } - private: ServiceContext::UniqueOperationContext _opCtx; boost::intrusive_ptr<ExpressionContextForTest> _expCtx; diff --git a/src/mongo/db/pipeline/change_stream_constants.h b/src/mongo/db/pipeline/change_stream_constants.h index 7b5104e93cd..94fe6487b52 100644 --- a/src/mongo/db/pipeline/change_stream_constants.h +++ b/src/mongo/db/pipeline/change_stream_constants.h @@ -34,17 +34,8 @@ namespace mongo { namespace change_stream_constants { -namespace stage_names { -static constexpr StringData kEnsureResumeTokenPresent = - "$_internalChangeStreamEnsureResumeTokenPresent"_sd; -static constexpr StringData kHandleTopologyChange = "$_internalChangeStreamHandleTopologyChange"_sd; -} // namespace stage_names -static const BSONObj kSortSpec = BSON("_id._data" << 1); - -// Internal change stream stages that can appear in a router (mongoS) pipeline. -static const StringDataSet kChangeStreamRouterPipelineStages = { - stage_names::kEnsureResumeTokenPresent, stage_names::kHandleTopologyChange}; +const BSONObj kSortSpec = BSON("_id._data" << 1); } // namespace change_stream_constants } // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_event_transform.cpp b/src/mongo/db/pipeline/change_stream_event_transform.cpp index 1a302ea8ab1..9e8a7d8ac19 100644 --- a/src/mongo/db/pipeline/change_stream_event_transform.cpp +++ b/src/mongo/db/pipeline/change_stream_event_transform.cpp @@ -31,8 +31,6 @@ #include "mongo/db/pipeline/change_stream_document_diff_parser.h" #include "mongo/db/pipeline/change_stream_filter_helpers.h" -#include "mongo/db/pipeline/change_stream_helpers.h" -#include "mongo/db/pipeline/change_stream_helpers_legacy.h" #include "mongo/db/pipeline/change_stream_preimage_gen.h" #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/document_source_change_stream_add_post_image.h" @@ -45,7 +43,6 @@ namespace mongo { namespace { constexpr auto checkValueType = &DocumentSourceChangeStream::checkValueType; -constexpr auto resolveResumeToken = &change_stream::resolveResumeTokenFromSpec; Document copyDocExceptFields(const Document& source, const std::set<StringData>& fieldNames) { MutableDocument doc(source); @@ -82,7 +79,8 @@ ChangeStreamEventTransformation::ChangeStreamEventTransformation( const DocumentSourceChangeStreamSpec& spec) : _changeStreamSpec(spec), _expCtx(expCtx) { // Extract the resume token from the spec and store it. - _resumeToken = resolveResumeToken(_expCtx, _changeStreamSpec); + _resumeToken = + DocumentSourceChangeStream::resolveResumeTokenFromSpec(_expCtx, _changeStreamSpec); // Determine whether the user requested a point-in-time pre-image, which will affect this // stage's output. @@ -108,11 +106,9 @@ ResumeTokenData ChangeStreamEventTransformation::makeResumeToken(Value tsVal, auto clusterTime = tsVal.getTimestamp(); // If we have a resume token, we need to match the version with which it was generated until we - // have surpassed all events against which it may have been compared in the original stream, at - // which point we can begin generating tokens with our default version. - auto version = (clusterTime > _resumeToken.clusterTime || txnOpIndex > _resumeToken.txnOpIndex) - ? _expCtx->changeStreamTokenVersion - : _resumeToken.version; + // have surpassed it, at which point we can begin generating tokens with our default version. + auto version = (clusterTime > _resumeToken.clusterTime) ? _expCtx->changeStreamTokenVersion + : _resumeToken.version; // Construct and return the final resume token. return {clusterTime, version, txnOpIndex, uuid, operationType, documentKey, opDescription}; @@ -341,24 +337,6 @@ Document ChangeStreamDefaultEventTransformation::applyTransformation(const Docum break; } - // Check whether this is a refineCollectionShardKey oplog entry. - if (!input.getNestedField("o2.refineCollectionShardKey").missing()) { - const auto o2Field = input[repl::OplogEntry::kObject2FieldName].getDocument(); - operationType = DocumentSourceChangeStream::kRefineCollectionShardKeyOpType; - operationDescription = - Value(copyDocExceptFields(o2Field, {"refineCollectionShardKey"_sd})); - break; - } - - // Check whether this is a reshardCollection oplog entry. - if (!input.getNestedField("o2.reshardCollection").missing()) { - const auto o2Field = input[repl::OplogEntry::kObject2FieldName].getDocument(); - operationType = DocumentSourceChangeStream::kReshardCollectionOpType; - operationDescription = - Value(copyDocExceptFields(o2Field, {"reshardCollection"_sd})); - break; - } - // Otherwise, o2.type determines the message type. auto o2Type = input.getNestedField("o2.type"); tassert(5052200, "o2.type is missing from noop oplog event", !o2Type.missing()); diff --git a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp index 78e945a4ae8..2b16d4dacda 100644 --- a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp +++ b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp @@ -37,7 +37,7 @@ #include "mongo/db/catalog/collection.h" #include "mongo/db/change_stream_options_manager.h" #include "mongo/db/client.h" -#include "mongo/db/concurrency/exception_util.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/change_stream_preimage_gen.h" @@ -58,6 +58,18 @@ MONGO_FAIL_POINT_DEFINE(changeStreamPreImageRemoverCurrentTime); namespace preImageRemoverInternal { +bool PreImageAttributes::isExpiredPreImage(const boost::optional<Date_t>& preImageExpirationTime, + const Timestamp& earliestOplogEntryTimestamp) { + // Pre-image oplog entry is no longer present in the oplog if its timestamp is smaller + // than the 'earliestOplogEntryTimestamp'. + const bool preImageOplogEntryIsDeleted = ts < earliestOplogEntryTimestamp; + const auto expirationTime = preImageExpirationTime.get_value_or(Date_t::min()); + + // Pre-image is expired if its corresponding oplog entry is deleted or its operation + // time is less than or equal to the expiration time. + return preImageOplogEntryIsDeleted || operationTime <= expirationTime; +} + // Get the 'expireAfterSeconds' from the 'ChangeStreamOptions' if not 'off', boost::none otherwise. boost::optional<std::int64_t> getExpireAfterSecondsFromChangeStreamOptions( ChangeStreamOptions& changeStreamOptions) { @@ -96,48 +108,18 @@ RecordId toRecordId(ChangeStreamPreImageId id) { } /** - * Finds the next collection UUID in the change stream pre-images collection 'preImagesCollPtr' for - * which collection UUID is greater than 'collectionUUID'. Returns boost::none if the next - * collection is not found. - */ -boost::optional<UUID> findNextCollectionUUID(OperationContext* opCtx, - const CollectionPtr* preImagesCollPtr, - boost::optional<UUID> collectionUUID - -) { - BSONObj preImageObj; - auto minRecordId = collectionUUID - ? boost::make_optional(RecordIdBound(toRecordId(ChangeStreamPreImageId( - *collectionUUID, Timestamp::max(), std::numeric_limits<int64_t>::max())))) - : boost::none; - auto planExecutor = - InternalPlanner::collectionScan(opCtx, - preImagesCollPtr, - PlanYieldPolicy::YieldPolicy::INTERRUPT_ONLY, - InternalPlanner::Direction::FORWARD, - boost::none /* resumeAfterRecordId */, - std::move(minRecordId)); - if (planExecutor->getNext(&preImageObj, nullptr) == PlanExecutor::IS_EOF) { - return boost::none; - } - auto parsedUUID = UUID::parse(preImageObj["_id"].Obj()["nsUUID"]); - tassert(7027400, "Pre-image collection UUID must be of UUID type", parsedUUID.isOK()); - return {std::move(parsedUUID.getValue())}; -} - -/** * Scans the 'config.system.preimages' collection and deletes the expired pre-images from it. * * Pre-images are ordered by collection UUID, ie. if UUID of collection A is ordered before UUID of * collection B, then pre-images of collection A will be stored before pre-images of collection B. * - * Pre-images are considered expired based on expiration parameter. In case when expiration - * parameter is not set a pre-image is considered expired if its timestamp is smaller than the - * timestamp of the earliest oplog entry. In case when expiration parameter is specified, aside from - * timestamp check a check on the wall clock time of the pre-image recording ('operationTime') is - * performed. If the difference between 'currentTimeForTimeBasedExpiration' and 'operationTime' is - * larger than expiration parameter, the pre-image is considered expired. One of those two - * conditions must be true for a pre-image to be eligible for deletion. + * While scanning the collection for expired pre-images, each pre-image timestamp is compared + * against the 'earliestOplogEntryTimestamp' value. Any pre-image that has a timestamp greater than + * the 'earliestOplogEntryTimestamp' value is not considered for deletion and the cursor seeks to + * the next UUID in the collection. + * + * Seek to the next UUID is done by setting the values of 'Timestamp' and 'ApplyOpsIndex' fields to + * max, ie. (currentPreImage.nsUUID, Timestamp::max(), ApplyOpsIndex::max()). * * +-------------------------+ * | config.system.preimages | @@ -152,96 +134,267 @@ boost::optional<UUID> findNextCollectionUUID(OperationContext* opCtx, * | applyIndex: 0 | | applyIndex: 0 | | applyIndex: 0 | | applyIndex: 1 | * +-------------------+ +-------------------+ +-------------------+ +-------------------+ */ -size_t deleteExpiredChangeStreamPreImages(OperationContext* opCtx, - Date_t currentTimeForTimeBasedExpiration) { +class ChangeStreamExpiredPreImageIterator { +public: + // Iterator over the range of pre-image documents, where each range defines a set of expired + // pre-image documents of one collection eligible for deletion due to expiration. Lower and + // upper bounds of a range are inclusive. + class Iterator { + public: + using RecordIdRange = std::pair<RecordId, RecordId>; + + Iterator(OperationContext* opCtx, + const CollectionPtr* preImagesCollPtr, + Timestamp earliestOplogEntryTimestamp, + boost::optional<Date_t> preImageExpirationTime, + bool isEndIterator = false) + : _opCtx(opCtx), + _preImagesCollPtr(preImagesCollPtr), + _earliestOplogEntryTimestamp(earliestOplogEntryTimestamp), + _preImageExpirationTime(preImageExpirationTime) { + if (!isEndIterator) { + advance(); + } + } + + const RecordIdRange& operator*() const { + return _currentExpiredPreImageRange; + } + + const RecordIdRange* operator->() const { + return &_currentExpiredPreImageRange; + } + + Iterator& operator++() { + advance(); + return *this; + } + + // Both iterators are equal if they are both pointing to the same expired pre-image range. + friend bool operator==(const Iterator& a, const Iterator& b) { + return a._currentExpiredPreImageRange == b._currentExpiredPreImageRange; + }; + + friend bool operator!=(const Iterator& a, const Iterator& b) { + return !(a == b); + }; + + private: + // Scans the pre-images collection and gets the next expired pre-image range or sets + // '_currentExpiredPreImageRange' to the range with empty record ids in case there are no + // more expired pre-images left. + void advance() { + const auto getNextPreImageAttributes = + [&](std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>& planExecutor) + -> boost::optional<preImageRemoverInternal::PreImageAttributes> { + BSONObj preImageObj; + if (planExecutor->getNext(&preImageObj, nullptr) == PlanExecutor::IS_EOF) { + return boost::none; + } + + auto preImage = + ChangeStreamPreImage::parse(IDLParserErrorContext("pre-image"), preImageObj); + return {{std::move(preImage.getId().getNsUUID()), + std::move(preImage.getId().getTs()), + std::move(preImage.getOperationTime())}}; + }; + + while (true) { + // Fetch the first pre-image from the next collection, that has pre-images enabled. + auto planExecutor = _previousCollectionUUID + ? createCollectionScan(RecordIdBound( + toRecordId(ChangeStreamPreImageId(*_previousCollectionUUID, + Timestamp::max(), + std::numeric_limits<int64_t>::max())))) + : createCollectionScan(boost::none); + auto preImageAttributes = getNextPreImageAttributes(planExecutor); + + // If there aren't any pre-images left, set the range to the empty record ids and + // return. + if (!preImageAttributes) { + _currentExpiredPreImageRange = std::pair(RecordId(), RecordId()); + return; + } + const auto currentCollectionUUID = preImageAttributes->collectionUUID; + _previousCollectionUUID = currentCollectionUUID; + + // If the first pre-image in the current collection is not expired, fetch the first + // pre-image from the next collection. + if (!preImageAttributes->isExpiredPreImage(_preImageExpirationTime, + _earliestOplogEntryTimestamp)) { + continue; + } + + // If an expired pre-image is found, compute the max expired pre-image RecordId for + // this collection depending on the expiration parameter being set. + const auto minKey = + toRecordId(ChangeStreamPreImageId(currentCollectionUUID, Timestamp(), 0)); + RecordId maxKey; + if (_preImageExpirationTime) { + // Reset the collection scan to start one increment before the + // '_earliestOplogEntryTimestamp', as the pre-images with smaller or equal + // timestamp are guaranteed to be expired. + Timestamp lastExpiredPreimageTs(_earliestOplogEntryTimestamp.asULL() - 1); + auto planExecutor = createCollectionScan(RecordIdBound( + toRecordId(ChangeStreamPreImageId(currentCollectionUUID, + lastExpiredPreimageTs, + std::numeric_limits<int64_t>::max())))); + + // Iterate over all the expired pre-images in the collection in order to find + // the max RecordId. + while ((preImageAttributes = getNextPreImageAttributes(planExecutor)) && + preImageAttributes->isExpiredPreImage(_preImageExpirationTime, + _earliestOplogEntryTimestamp) && + preImageAttributes->collectionUUID == currentCollectionUUID) { + lastExpiredPreimageTs = preImageAttributes->ts; + } + + maxKey = + toRecordId(ChangeStreamPreImageId(currentCollectionUUID, + lastExpiredPreimageTs, + std::numeric_limits<int64_t>::max())); + } else { + // If the expiration parameter is not set, then the last expired pre-image + // timestamp equals to one increment before the '_earliestOplogEntryTimestamp'. + maxKey = toRecordId( + ChangeStreamPreImageId(currentCollectionUUID, + Timestamp(_earliestOplogEntryTimestamp.asULL() - 1), + std::numeric_limits<int64_t>::max())); + } + tassert(6138300, + "Max key of the expired pre-image range has to be valid", + maxKey.isValid()); + _currentExpiredPreImageRange = std::pair(minKey, maxKey); + return; + } + } + + // Set up the new collection scan to start from the 'minKey'. + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> createCollectionScan( + boost::optional<RecordIdBound> minKey) const { + return InternalPlanner::collectionScan(_opCtx, + _preImagesCollPtr, + PlanYieldPolicy::YieldPolicy::INTERRUPT_ONLY, + InternalPlanner::Direction::FORWARD, + boost::none, + minKey); + } + + OperationContext* _opCtx; + const CollectionPtr* _preImagesCollPtr; + RecordIdRange _currentExpiredPreImageRange; + boost::optional<UUID> _previousCollectionUUID; + const Timestamp _earliestOplogEntryTimestamp; + + // The pre-images with operation time less than or equal to the '_preImageExpirationTime' + // are considered expired. + const boost::optional<Date_t> _preImageExpirationTime; + }; + + ChangeStreamExpiredPreImageIterator( + OperationContext* opCtx, + const CollectionPtr* preImagesCollPtr, + const Timestamp earliestOplogEntryTimestamp, + const boost::optional<Date_t> preImageExpirationTime = boost::none) + : _opCtx(opCtx), + _preImagesCollPtr(preImagesCollPtr), + _earliestOplogEntryTimestamp(earliestOplogEntryTimestamp), + _preImageExpirationTime(preImageExpirationTime) {} + + Iterator begin() const { + return Iterator( + _opCtx, _preImagesCollPtr, _earliestOplogEntryTimestamp, _preImageExpirationTime); + } + + Iterator end() const { + return Iterator(_opCtx, + _preImagesCollPtr, + _earliestOplogEntryTimestamp, + _preImageExpirationTime, + true /*isEndIterator*/); + } + +private: + OperationContext* _opCtx; + const CollectionPtr* _preImagesCollPtr; + const Timestamp _earliestOplogEntryTimestamp; + const boost::optional<Date_t> _preImageExpirationTime; +}; + +void deleteExpiredChangeStreamPreImages(Client* client, Date_t currentTimeForTimeBasedExpiration) { + const auto startTime = Date_t::now(); + auto opCtx = client->makeOperationContext(); + // Acquire intent-exclusive lock on the pre-images collection. Early exit if the collection // doesn't exist. - AutoGetCollection autoColl(opCtx, NamespaceString::kChangeStreamPreImagesNamespace, MODE_IX); + AutoGetCollection autoColl( + opCtx.get(), NamespaceString::kChangeStreamPreImagesNamespace, MODE_IX); const auto& preImagesColl = autoColl.getCollection(); if (!preImagesColl) { - return 0; + return; } // Do not run the job on secondaries. - if (!repl::ReplicationCoordinator::get(opCtx)->canAcceptWritesForDatabase( - opCtx, NamespaceString::kAdminDb)) { - return 0; + if (!repl::ReplicationCoordinator::get(opCtx.get()) + ->canAcceptWritesForDatabase(opCtx.get(), NamespaceString::kAdminDb)) { + return; } - // Get the timestamp of the earliest oplog entry. + // Get the timestamp of the ealiest oplog entry. const auto currentEarliestOplogEntryTs = - repl::StorageInterface::get(opCtx->getServiceContext())->getEarliestOplogTimestamp(opCtx); + repl::StorageInterface::get(client->getServiceContext()) + ->getEarliestOplogTimestamp(opCtx.get()); const bool isBatchedRemoval = gBatchedExpiredChangeStreamPreImageRemoval.load(); size_t numberOfRemovals = 0; - const auto preImageExpirationTime = ::mongo::preImageRemoverInternal::getPreImageExpirationTime( - opCtx, currentTimeForTimeBasedExpiration); - - // Configure the filter for the case when expiration parameter is set. - OrMatchExpression filter; - const MatchExpression* filterPtr = nullptr; - if (preImageExpirationTime) { - filter.add( - std::make_unique<LTMatchExpression>("_id.ts"_sd, Value(currentEarliestOplogEntryTs))); - filter.add(std::make_unique<LTEMatchExpression>("operationTime"_sd, - Value(*preImageExpirationTime))); - filterPtr = &filter; - } - const bool shouldReturnEofOnFilterMismatch = preImageExpirationTime.has_value(); - boost::optional<UUID> currentCollectionUUID = boost::none; - while ((currentCollectionUUID = - findNextCollectionUUID(opCtx, &preImagesColl, currentCollectionUUID))) { + ChangeStreamExpiredPreImageIterator expiredPreImages( + opCtx.get(), + &preImagesColl, + currentEarliestOplogEntryTs, + ::mongo::preImageRemoverInternal::getPreImageExpirationTime( + opCtx.get(), currentTimeForTimeBasedExpiration)); + + for (const auto& collectionRange : expiredPreImages) { writeConflictRetry( - opCtx, + opCtx.get(), "ChangeStreamExpiredPreImagesRemover", NamespaceString::kChangeStreamPreImagesNamespace.ns(), [&] { auto params = std::make_unique<DeleteStageParams>(); params->isMulti = true; - std::unique_ptr<BatchedDeleteStageBatchParams> batchedDeleteParams; + boost::optional<std::unique_ptr<BatchedDeleteStageBatchParams>> batchParams; if (isBatchedRemoval) { - batchedDeleteParams = std::make_unique<BatchedDeleteStageBatchParams>(); + batchParams = std::make_unique<BatchedDeleteStageBatchParams>(); } - RecordIdBound minRecordId( - toRecordId(ChangeStreamPreImageId(*currentCollectionUUID, Timestamp(), 0))); - - // If the expiration parameter is set, the 'maxRecord' is set to the maximum - // RecordId for this collection. Whether the pre-image has to be deleted will be - // determined by the filtering MatchExpression. - // - // If the expiration parameter is not set, then the last expired pre-image timestamp - // equals to one increment before the 'currentEarliestOplogEntryTs'. - RecordIdBound maxRecordId = RecordIdBound(toRecordId(ChangeStreamPreImageId( - *currentCollectionUUID, - preImageExpirationTime ? Timestamp::max() - : Timestamp(currentEarliestOplogEntryTs.asULL() - 1), - std::numeric_limits<int64_t>::max()))); auto exec = InternalPlanner::deleteWithCollectionScan( - opCtx, + opCtx.get(), &preImagesColl, std::move(params), PlanYieldPolicy::YieldPolicy::YIELD_AUTO, InternalPlanner::Direction::FORWARD, - std::move(minRecordId), - std::move(maxRecordId), + RecordIdBound(collectionRange.first), + RecordIdBound(collectionRange.second), CollectionScanParams::ScanBoundInclusion::kIncludeBothStartAndEndRecords, - std::move(batchedDeleteParams), - filterPtr, - shouldReturnEofOnFilterMismatch); + std::move(batchParams)); numberOfRemovals += exec->executeDelete(); }); } - return numberOfRemovals; + + if (numberOfRemovals > 0) { + LOGV2_DEBUG(5869104, + 3, + "Periodic expired pre-images removal job finished executing", + "numberOfRemovals"_attr = numberOfRemovals, + "jobDuration"_attr = (Date_t::now() - startTime).toString()); + } } void performExpiredChangeStreamPreImagesRemovalPass(Client* client) { - ServiceContext::UniqueOperationContext opCtx; try { Date_t currentTimeForTimeBasedExpiration = Date_t::now(); - opCtx = client->makeOperationContext(); changeStreamPreImageRemoverCurrentTime.execute([&](const BSONObj& data) { // Populate the current time for time based expiration of pre-images. @@ -256,7 +409,7 @@ void performExpiredChangeStreamPreImagesRemovalPass(Client* client) { currentTimeForTimeBasedExpiration = currentTimeElem.Date(); } }); - deleteExpiredChangeStreamPreImages(opCtx.get(), currentTimeForTimeBasedExpiration); + deleteExpiredChangeStreamPreImages(client, currentTimeForTimeBasedExpiration); } catch (const ExceptionForCat<ErrorCategory::Interruption>&) { LOGV2_WARNING(5869105, "Periodic expired pre-images removal job was interrupted"); } catch (const DBException& exception) { diff --git a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h index 19315825043..0ddd491991f 100644 --- a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h +++ b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h @@ -35,6 +35,22 @@ namespace mongo { namespace preImageRemoverInternal { +/** + * Specifies attributes that determines if the pre-image has been expired or not. + */ +struct PreImageAttributes { + mongo::UUID collectionUUID; + Timestamp ts; + Date_t operationTime; + + /** + * Determines if the pre-image is considered expired based on the expiration parameter being + * set. + */ + bool isExpiredPreImage(const boost::optional<Date_t>& preImageExpirationTime, + const Timestamp& earliestOplogEntryTimestamp); +}; + boost::optional<Date_t> getPreImageExpirationTime(OperationContext* opCtx, Date_t currentTime); } // namespace preImageRemoverInternal diff --git a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp index b2562a1db44..ec49c8453d3 100644 --- a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp +++ b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp @@ -61,6 +61,16 @@ public: ASSERT_EQ(changeStreamOptionsManager.setOptions(opCtx, changeStreamOptions).getStatus(), ErrorCodes::OK); } + + bool isExpiredPreImage(const Timestamp& preImageTs, + const Date_t& preImageOperationTime, + const boost::optional<Date_t>& preImageExpirationTime, + const Timestamp& earliestOplogEntryTimestamp) { + preImageRemoverInternal::PreImageAttributes preImageAttributes{ + UUID::gen(), preImageTs, preImageOperationTime}; + return preImageAttributes.isExpiredPreImage(preImageExpirationTime, + earliestOplogEntryTimestamp); + } }; TEST_F(ChangeStreamPreImageExpirationPolicyTest, getPreImageExpirationTimeWithValidIntegralValue) { @@ -97,5 +107,39 @@ TEST_F(ChangeStreamPreImageExpirationPolicyTest, getPreImageExpirationTimeWithOf preImageRemoverInternal::getPreImageExpirationTime(opCtx.get(), currentTime); ASSERT_FALSE(receivedExpireAfterSeconds); } + +TEST_F(ChangeStreamPreImageExpirationPolicyTest, preImageShouldHaveExpiredWithOlderTimestamp) { + ASSERT_TRUE( + isExpiredPreImage(Timestamp(Seconds(100000), 0U) /* preImageTs */, + Date_t::now() /* preImageOperationTime */, + Date_t::now() /* preImageExpirationTime */, + Timestamp(Seconds(100000), 1U)) /* earliestOplogEntryTimestamp */); +} + +TEST_F(ChangeStreamPreImageExpirationPolicyTest, preImageShouldNotHaveExpired) { + ASSERT_FALSE( + isExpiredPreImage(Timestamp(Seconds(100000), 1U) /* preImageTs */, + Date_t::now() + Seconds(1) /* preImageOperationTime */, + Date_t::now() /* preImageExpirationTime */, + Timestamp(Seconds(100000), 0U)) /* earliestOplogEntryTimestamp */); +} + +TEST_F(ChangeStreamPreImageExpirationPolicyTest, preImageShouldHaveExpiredWithOlderOperationTime) { + ASSERT_TRUE( + isExpiredPreImage(Timestamp(Seconds(100000), 1U) /* preImageTs */, + Date_t::now() /* preImageOperationTime */, + Date_t::now() + Seconds(1) /* preImageExpirationTime */, + Timestamp(Seconds(100000), 0U)) /* earliestOplogEntryTimestamp */); +} + +TEST_F(ChangeStreamPreImageExpirationPolicyTest, + preImageShouldNotHaveExpiredWithNullExpirationTime) { + ASSERT_TRUE( + isExpiredPreImage(Timestamp(Seconds(100000), 0U) /* preImageTs */, + Date_t::now() /* preImageOperationTime */, + boost::none /* preImageExpirationTime */, + Timestamp(Seconds(100000), 1U)) /* earliestOplogEntryTimestamp */); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_filter_helpers.cpp b/src/mongo/db/pipeline/change_stream_filter_helpers.cpp index 47f0124bca2..106c9ea9eb6 100644 --- a/src/mongo/db/pipeline/change_stream_filter_helpers.cpp +++ b/src/mongo/db/pipeline/change_stream_filter_helpers.cpp @@ -319,14 +319,6 @@ std::unique_ptr<MatchExpression> buildInternalOpFilter( BSON("o2.migrateLastChunkFromShard" << BSON("$exists" << true))); } - if (feature_flags::gFeatureFlagChangeStreamsFurtherEnrichedEvents.isEnabled( - serverGlobalParams.featureCompatibility)) { - internalOpTypeOrBuilder.append( - BSON("o2.refineCollectionShardKey" << BSON("$exists" << true))); - - internalOpTypeOrBuilder.append(BSON("o2.reshardCollection" << BSON("$exists" << true))); - } - // Finalize the array of $or filter predicates. internalOpTypeOrBuilder.done(); diff --git a/src/mongo/db/pipeline/change_stream_helpers.cpp b/src/mongo/db/pipeline/change_stream_helpers.cpp deleted file mode 100644 index 720646d32ea..00000000000 --- a/src/mongo/db/pipeline/change_stream_helpers.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (C) 2018-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 "mongo/db/pipeline/change_stream_helpers.h" - -#include "mongo/db/pipeline/document_source_change_stream_gen.h" - -namespace mongo { -namespace change_stream { -ResumeTokenData resolveResumeTokenFromSpec(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const DocumentSourceChangeStreamSpec& spec) { - - if (spec.getStartAfter()) { - return spec.getStartAfter()->getData(); - } else if (spec.getResumeAfter()) { - return spec.getResumeAfter()->getData(); - } else if (spec.getStartAtOperationTime()) { - return ResumeToken::makeHighWaterMarkToken(*spec.getStartAtOperationTime(), - expCtx->changeStreamTokenVersion) - .getData(); - } - tasserted(5666901, - "Expected one of 'startAfter', 'resumeAfter' or 'startAtOperationTime' to be " - "populated in $changeStream spec"); -} -} // namespace change_stream -} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_helpers.h b/src/mongo/db/pipeline/change_stream_helpers.h deleted file mode 100644 index 7adc4f92f8f..00000000000 --- a/src/mongo/db/pipeline/change_stream_helpers.h +++ /dev/null @@ -1,47 +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. - */ - -#pragma once - -#include "mongo/db/pipeline/document_source_change_stream_gen.h" -#include "mongo/db/pipeline/expression_context.h" - -namespace mongo { - -namespace change_stream { -/** - * Extracts the resume token from the given spec. If a 'startAtOperationTime' is specified, - * returns the equivalent high-watermark token. This method should only ever be called on a spec - * where one of 'resumeAfter', 'startAfter', or 'startAtOperationTime' is populated. - */ -ResumeTokenData resolveResumeTokenFromSpec(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const DocumentSourceChangeStreamSpec& spec); - -} // namespace change_stream -} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp b/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp index ce4a9234c6a..ab0ade1e517 100644 --- a/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp +++ b/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp @@ -29,8 +29,6 @@ #include "mongo/db/pipeline/change_stream_rewrite_helpers.h" -#include <boost/algorithm/string/replace.hpp> - #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_expr.h" #include "mongo/db/pipeline/document_source_change_stream.h" @@ -227,7 +225,7 @@ boost::intrusive_ptr<Expression> exprRewriteOperationType( opCases.push_back(fromjson("{case: {$ne: ['$o.collMod', '$$REMOVE']}, then: 'modify'}")); // The default case, if nothing matches. - auto defaultCase = ExpressionConstant::create(expCtx.get(), Value())->serialize(); + auto defaultCase = ExpressionConstant::create(expCtx.get(), Value())->serialize(false); // Build the final expression object... BSONObjBuilder exprBuilder; @@ -417,7 +415,7 @@ boost::intrusive_ptr<Expression> exprRewriteDocumentKey( fromjson("{case: {$eq: ['$op', 'u']}, then: '" + updateAndReplacePath + "'}")); // The default case, if nothing matches. - auto defaultCase = ExpressionConstant::create(expCtx.get(), Value())->serialize(); + auto defaultCase = ExpressionConstant::create(expCtx.get(), Value())->serialize(false); // Build the expression BSON object. BSONObjBuilder exprBuilder; @@ -890,12 +888,9 @@ std::unique_ptr<MatchExpression> matchRewriteGenericNamespace( }(); // Convert the MatchExpression $regex into a $regexMatch on the corresponding field. - // Backslashes must be escaped to ensure they retain their special behavior. - const auto regex = - boost::replace_all_copy(std::string(nsElem.regex()), R"(\)", R"(\\)"); const std::string exprRegexMatch = str::stream() - << "{$regexMatch: {input: " << exprDbOrCollName << ", regex: '" << regex - << "', options: '" << nsElem.regexFlags() << "'}}"; + << "{$regexMatch: {input: " << exprDbOrCollName << ", regex: '" + << nsElem.regex() << "', options: '" << nsElem.regexFlags() << "'}}"; // Finally, wrap the regex in a $let which defines the '$$oplogField' variable. const std::string exprRewrittenPredicate = str::stream() @@ -1159,7 +1154,7 @@ boost::intrusive_ptr<Expression> exprRewriteNs( collCases.push_back(fromjson("{case: {$ne: ['$o.collMod', '$$REMOVE']}, then: '$o.collMod'}")); // The default case, if nothing matches. - auto defaultCase = ExpressionConstant::create(expCtx.get(), Value())->serialize(); + auto defaultCase = ExpressionConstant::create(expCtx.get(), Value())->serialize(false); // Build the collection expression object... BSONObjBuilder collExprBuilder; @@ -1500,8 +1495,7 @@ std::unique_ptr<MatchExpression> rewriteMatchExpressionTree( case MatchExpression::EXPRESSION: { // Agg expressions are rewritten in-place, so we must clone the expression tree. auto origExprVal = - static_cast<const ExprMatchExpression*>(root)->getExpression()->serialize( - SerializationOptions{}); + static_cast<const ExprMatchExpression*>(root)->getExpression()->serialize(false); auto clonedExpr = Expression::parseOperand( expCtx.get(), BSON("" << origExprVal).firstElement(), expCtx->variablesParseState); @@ -1514,11 +1508,7 @@ std::unique_ptr<MatchExpression> rewriteMatchExpressionTree( } default: { if (auto pathME = dynamic_cast<const PathMatchExpression*>(root)) { - // Only attempt to rewrite non-empty paths. - if (pathME->path().empty()) { - return nullptr; - } - + tassert(5687201, "Unexpected empty path", !pathME->path().empty()); auto firstPath = pathME->fieldRef()->getPart(0).toString(); // Only attempt to rewrite paths that begin with one of the caller-requested fields. diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp b/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp deleted file mode 100644 index 19fba5afea5..00000000000 --- a/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp +++ /dev/null @@ -1,131 +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 "mongo/db/pipeline/change_stream_split_event_helpers.h" - -#include "mongo/db/pipeline/field_path.h" -#include "mongo/db/pipeline/resume_token.h" - -namespace mongo { -namespace change_stream_split_event { - -std::pair<Document, size_t> processChangeEventBeforeSplit(const Document& event, - bool withMetadata) { - if (withMetadata) { - auto eventBson = event.toBsonWithMetaData<BSONObj::LargeSizeTrait>(); - return {Document::fromBsonWithMetaData(eventBson), eventBson.objsize()}; - } else { - // Serialize just the user data, and add the metadata fields separately. - auto eventBson = event.toBson<BSONObj::LargeSizeTrait>(); - MutableDocument mutDoc(Document{eventBson}); - mutDoc.copyMetaDataFrom(event); - return {mutDoc.freeze(), eventBson.objsize()}; - } -} - -std::queue<Document> splitChangeEvent(const Document& event, - size_t maxFragmentBsonSize, - size_t skipFirstFragments) { - // Extract the underlying BSON. We expect the event to be trivially convertible either with - // or without metadata, so we attempt to optimize the serialization here. - auto eventBson = - (event.isTriviallyConvertible() ? event.toBson<BSONObj::LargeSizeTrait>() - : event.toBsonWithMetaData<BSONObj::LargeSizeTrait>()); - - // Construct a sorted map of fields ordered by size and key for a deterministic greedy strategy - // to minimize the total number of fragments (the first fragment contains as many fields as - // possible). Don't include the original '_id' field, since each fragment will have its own. - std::map<std::pair<size_t, std::string>, Value> sortedFieldMap; - for (auto it = event.fieldIterator(); it.more();) { - auto&& [key, value] = it.next(); - if (key != kIdField) { - sortedFieldMap.emplace(std::make_pair(eventBson[key].size(), key), value); - } - } - - uassert(7182502, - "Cannot split an empty event or an event containing solely '_id' field", - !sortedFieldMap.empty()); - - auto resumeTokenData = - ResumeToken::parse(event.metadata().getSortKey().getDocument()).getData(); - - std::list<MutableDocument> fragments; - for (auto it = sortedFieldMap.cbegin(); it != sortedFieldMap.cend();) { - // Update the resume token with the index of the fragment we're about to add. - resumeTokenData.fragmentNum = fragments.size(); - - // Add a new fragment at the end of the fragments list. - auto& fragment = fragments.emplace_back(); - - // Add fields required by all fragments. - ResumeToken token(resumeTokenData); - fragment.metadata().setSortKey(Value(token.toDocument()), true); - fragment.addField(kIdField, fragment.metadata().getSortKey()); - fragment.addField(kSplitEventField, - Value(Document{{kFragmentNumberField, static_cast<int>(fragments.size())}, - {kTotalFragmentsField, 0}})); - - auto fragmentBsonSize = static_cast<size_t>(fragment.peek().toBsonWithMetaData().objsize()); - - // Fill the fragment with as many fields as we can until we run out or exceed max size. - // Always make sure we add at least one new field on each iteration. - do { - fragment.addField(it->first.second /* field name */, it->second /* field value */); - fragmentBsonSize += it->first.first /* field size */; - } while (++it != sortedFieldMap.cend() && - fragmentBsonSize + it->first.first /* field size */ <= maxFragmentBsonSize); - - uassert(7182500, - str::stream() << "Splitting change event failed: fragment size " << fragmentBsonSize - << " is greater than maximum allowed fragment size " - << maxFragmentBsonSize, - fragmentBsonSize <= maxFragmentBsonSize); - } - - // Iterate over the fragments to populate the 'kTotalFragmentsField' field and freeze the final - // events. - const auto totalFragments = Value(static_cast<int>(fragments.size())); - const auto totalFragmentsFieldPath = - FieldPath::getFullyQualifiedPath(kSplitEventField, kTotalFragmentsField); - - std::queue<Document> outputFragments; - for (auto [it, i] = std::make_pair(fragments.begin(), 0ULL); it != fragments.end(); ++it, ++i) { - // Do not insert first 'skipFirstFragments' into the output. - if (i >= skipFirstFragments) { - it->setNestedField(totalFragmentsFieldPath, totalFragments); - outputFragments.push(it->freeze()); - } - } - - return outputFragments; -} - -} // namespace change_stream_split_event -} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers.h b/src/mongo/db/pipeline/change_stream_split_event_helpers.h deleted file mode 100644 index ee39b52dead..00000000000 --- a/src/mongo/db/pipeline/change_stream_split_event_helpers.h +++ /dev/null @@ -1,68 +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. - */ - -#pragma once - -#include <queue> - -#include "mongo/db/exec/document_value/document.h" - -namespace mongo { -namespace change_stream_split_event { - -constexpr auto kIdField = "_id"_sd; -constexpr auto kSplitEventField = "splitEvent"_sd; -constexpr auto kFragmentNumberField = "fragment"_sd; -constexpr auto kTotalFragmentsField = "of"_sd; - -/** - * Calculates BSON size by serializing the event to BSON. Ensures that the serialization is - * re-usable. The parameter 'withMetadata' desides whether the metadata is counted. - * Also returns a new document optimized for later serialization by PlanExecutorPipeline. - */ -std::pair<Document, size_t> processChangeEventBeforeSplit(const Document& event, bool withMetadata); - -/** - * Splits the given change stream 'event' to several sub-events, called fragments. The size of BSON - * serialization of each fragment does not exceed the given maximum fragment size. Each fragment - * carries additionally fragment's ordinal number and the total number of fragments. Each fragment - * has its own resume token as its '_id' and the sort key. In the resume scenario, the - * 'skipFirstFragments' parameter indicates how many fragments were already received by the client - * and can be skipped. For example, the following change event - * {_id: "RESUMETOKEN1", fullDocument: ..., fullDocumentBeforeChange: ..., ...} - * can be split into the following fragments - * {_id: "RESUMETOKEN2", splitEvent{fragment: 1, of: 2}, fullDocumentBeforeChange: ...} - * {_id: "RESUMETOKEN3", splitEvent{fragment: 2, of: 2}, fullDocument: ...} - */ -std::queue<Document> splitChangeEvent(const Document& event, - size_t maxFragmentBsonSize, - size_t skipFirstFragments = 0); - -} // namespace change_stream_split_event -} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp b/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp deleted file mode 100644 index aea1ca6fce8..00000000000 --- a/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp +++ /dev/null @@ -1,158 +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 "mongo/db/pipeline/change_stream_split_event_helpers.h" -#include "mongo/db/pipeline/field_path.h" -#include "mongo/db/pipeline/resume_token.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace { - -using namespace change_stream_split_event; - -class ChangeStreamSplitEventHelpersTest : public unittest::Test { -public: - ChangeStreamSplitEventHelpersTest() { - ResumeTokenData tokenData( - Timestamp(1000, 1), 2, 0, UUID::gen(), Value(Document{{kIdField, 1}})); - doc.metadata().setSortKey(Value(ResumeToken(tokenData).toDocument()), true); - - // Approximate the size of the fragment with no data. - // This size may vary because of the variable serialization of the token - fragments with - // 'splitEvent.fragment' == 1 are 4 bytes shorter, because the token with - // 'tokenData.fragmentNum' == 0 are 2 bytes shorter. - MutableDocument fragment; - tokenData.fragmentNum = 1UL; - fragment.metadata().setSortKey(Value(ResumeToken(tokenData).toDocument()), true); - fragment.addField(kIdField, fragment.metadata().getSortKey()); - fragment.addField(kSplitEventField, - Value(Document{{kFragmentNumberField, 1}, {kTotalFragmentsField, 1}})); - minFragmentSize = static_cast<size_t>(fragment.peek().toBsonWithMetaData().objsize()); - } - - size_t getFieldBsonSize(const Document& doc, const StringData& key) { - return static_cast<size_t>(doc.toBson<BSONObj::LargeSizeTrait>().getField(key).size()); - } - - MutableDocument doc; - size_t minFragmentSize; - FieldPath fragmentNumberPath = - FieldPath::getFullyQualifiedPath(kSplitEventField, kFragmentNumberField); - FieldPath totalFragmentsPath = - FieldPath::getFullyQualifiedPath(kSplitEventField, kTotalFragmentsField); -}; - -TEST_F(ChangeStreamSplitEventHelpersTest, EmptyDocThrows) { - ASSERT_THROWS_CODE(splitChangeEvent(doc.freeze(), minFragmentSize, 0), DBException, 7182502); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, DocWithSolelyIdThrows) { - doc.addField("_id", Value(1)); - ASSERT_THROWS_CODE(splitChangeEvent(doc.freeze(), minFragmentSize, 0), DBException, 7182502); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, BasicSplitWithSingleFragment) { - doc.addField("a", Value(123)); - doc.addField("b", Value(321)); - auto fieldSize = getFieldBsonSize(doc.peek(), "a"); - auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize + fieldSize, 0); - ASSERT_EQ(1UL, fragments.size()); - auto& fragment = fragments.front(); - ASSERT_EQ(1, fragment.getNestedField(fragmentNumberPath).getInt()); - ASSERT_EQ(1, fragment.getNestedField(totalFragmentsPath).getInt()); - ASSERT_EQ(123, fragment.getField("a").getInt()); - ASSERT_EQ(321, fragment.getField("b").getInt()); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, ReplacesIdWithFragmentResumeToken) { - // Replace the test doc's _id with a numeric value. This will be overwritten by the fragment's - // resume token when we split the event. - doc.addField("_id", Value(1)); - doc.addField("a", Value(123)); - auto fieldSize = getFieldBsonSize(doc.peek(), "a"); - auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize, 0); - ASSERT_EQ(1ULL, fragments.size()); - auto& fragment = fragments.front(); - ASSERT_EQ(123, fragment.getField("a").getInt()); - auto tokenData = ResumeToken::parse(fragment.getField(kIdField).getDocument()).getData(); - ASSERT_EQ(0ULL, *tokenData.fragmentNum); - ASSERT_EQ(tokenData, - ResumeToken::parse(fragment.metadata().getSortKey().getDocument()).getData()); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, OversizedFragmentThrows) { - doc.addField("a", Value("very_long_string"_sd)); - auto fieldSize = getFieldBsonSize(doc.peek(), "a"); - ASSERT_THROWS_CODE( - splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize - 5, 0), DBException, 7182500); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, SplitEventAtMaxSizeBoundary) { - // Add two fields of equal size. The first fragment will contain the field with the name - // preceeding in the lexicographic order. - doc.addField("b", Value(321)); - doc.addField("a", Value(123)); - auto fieldSize = getFieldBsonSize(doc.peek(), "b"); - auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize, 0); - ASSERT_EQ(2ULL, fragments.size()); - auto &fragment1 = fragments.front(), fragment2 = fragments.back(); - ASSERT_EQ(123, fragment1.getField("a").getInt()); - ASSERT_EQ(321, fragment2.getField("b").getInt()); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, SplitEventFieldsOrderedInAscendingSize) { - doc.addField("a", Value("unittesting"_sd)); - doc.addField("b", Value("hello"_sd)); - auto fieldSize = getFieldBsonSize(doc.peek(), "a"); - auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize, 0); - ASSERT_EQ(2ULL, fragments.size()); - auto &fragment1 = fragments.front(), fragment2 = fragments.back(); - ASSERT_EQ(1, fragment1.getNestedField(fragmentNumberPath).getInt()); - ASSERT_EQ(2, fragment1.getNestedField(totalFragmentsPath).getInt()); - ASSERT_EQ("hello", fragment1.getField("b").getString()); - ASSERT_EQ(2, fragment2.getNestedField(fragmentNumberPath).getInt()); - ASSERT_EQ(2, fragment2.getNestedField(totalFragmentsPath).getInt()); - ASSERT_EQ("unittesting", fragment2.getField("a").getString()); -} - -TEST_F(ChangeStreamSplitEventHelpersTest, CanSkipFirstNFragments) { - doc.addField("a", Value("unittesting"_sd)); - doc.addField("b", Value("hello"_sd)); - auto fieldSize = getFieldBsonSize(doc.peek(), "a"); - auto fragmentsSkip1 = splitChangeEvent(doc.peek(), minFragmentSize + fieldSize, 1); - ASSERT_EQ(1ULL, fragmentsSkip1.size()); - ASSERT_EQ(2, fragmentsSkip1.front().getNestedField(fragmentNumberPath).getInt()); - ASSERT_EQ(2, fragmentsSkip1.front().getNestedField(totalFragmentsPath).getInt()); - auto fragmentsSkip2 = splitChangeEvent(doc.peek(), minFragmentSize + fieldSize, 2); - ASSERT_EQ(0ULL, fragmentsSkip2.size()); -} - -} // namespace -} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_test_helpers.cpp b/src/mongo/db/pipeline/change_stream_test_helpers.cpp index 8e4405fe384..b3a29103d94 100644 --- a/src/mongo/db/pipeline/change_stream_test_helpers.cpp +++ b/src/mongo/db/pipeline/change_stream_test_helpers.cpp @@ -92,14 +92,13 @@ repl::OplogEntry makeOplogEntry(repl::OpTypeEnum opType, nss, // namespace uuid, // uuid fromMigrate, // fromMigrate - boost::none, // checkExistenceForDiffInsert - repl::OplogEntry::kOplogVersion, // version - object, // o - object2, // o2 - sessionInfo, // sessionInfo - boost::none, // upsert - Date_t(), // wall clock time - {}, // statement ids + repl::OplogEntry::kOplogVersion, // version + object, // o + object2, // o2 + sessionInfo, // sessionInfo + boost::none, // upsert + Date_t(), // wall clock time + {}, // statement ids prevOpTime, // optime of previous write within same transaction preImageOpTime, // pre-image optime boost::none, // post-image optime diff --git a/src/mongo/db/pipeline/dependencies.cpp b/src/mongo/db/pipeline/dependencies.cpp index 4480ec8cf6b..d2a5563c7c7 100644 --- a/src/mongo/db/pipeline/dependencies.cpp +++ b/src/mongo/db/pipeline/dependencies.cpp @@ -37,32 +37,11 @@ namespace mongo { -OrderedPathSet DepsTracker::simplifyDependencies(OrderedPathSet dependencies, - TruncateToRootLevel truncateToRootLevel) { - // The key operation here is folding dependencies into ancestor dependencies, wherever possible. - // This is assisted by a special sort in OrderedPathSet that treats '.' - // as the first char and thus places parent paths directly before their children. - OrderedPathSet returnSet; - std::string last; - for (const auto& path : dependencies) { - if (!last.empty() && str::startsWith(path, last)) { - // We are including a parent of this field, so we can skip this field. - continue; - } - - // Check that the field requested is a valid field name in the agg language. This - // constructor will throw if it isn't. - FieldPath fp(path); - - if (truncateToRootLevel == TruncateToRootLevel::yes) { - last = fp.front().toString() + '.'; - returnSet.insert(fp.front().toString()); - } else { - last = path + '.'; - returnSet.insert(path); - } - } - return returnSet; +std::list<std::string> DepsTracker::sortedFields() const { + // Use a special comparator to put parent fieldpaths before their children. + std::list<std::string> sortedFields(fields.begin(), fields.end()); + sortedFields.sort(PathPrefixComparator()); + return sortedFields; } BSONObj DepsTracker::toProjectionWithoutMetadata( @@ -80,16 +59,35 @@ BSONObj DepsTracker::toProjectionWithoutMetadata( return bb.obj(); } - // Create a projection from the simplified dependencies (absorbing descendants into parents). - // For example, the dependencies ["a.b", "a.b.c.g", "c", "c.d", "f"] would be - // minimally covered by the projection {"a.b": 1, "c": 1, "f": 1}. + // Go through dependency fieldpaths to find the minimal set of projections that cover the + // dependencies. For example, the dependencies ["a.b", "a.b.c.g", "c", "c.d", "f"] would be + // minimally covered by the projection {"a.b": 1, "c": 1, "f": 1}. The key operation here is + // folding dependencies into ancestor dependencies, wherever possible. This is assisted by a + // special sort in DepsTracker::sortedFields that treats '.' as the first char and thus places + // parent paths directly before their children. bool idSpecified = false; - for (auto path : simplifyDependencies(fields, truncationBehavior)) { - // Remember if _id was specified. If not, we'll later explicitly add {_id: 0} - if (str::startsWith(path, "_id") && (path.size() == 3 || path[3] == '.')) { + std::string last; + for (const auto& field : sortedFields()) { + if (str::startsWith(field, "_id") && (field.size() == 3 || field[3] == '.')) { idSpecified = true; } - bb.append(path, 1); + + if (!last.empty() && str::startsWith(field, last)) { + // We are including a parent of this field, so we can skip this field. + continue; + } + + // Check that the field requested is a valid field name in the agg language. This + // constructor will throw if it isn't. + FieldPath fp(field); + + if (truncationBehavior == TruncateToRootLevel::yes) { + last = fp.front().toString() + '.'; + bb.append(fp.front(), 1); + } else { + last = field + '.'; + bb.append(field, 1); + } } if (!idSpecified) { @@ -111,7 +109,7 @@ void DepsTracker::setNeedsMetadata(DocumentMetadataFields::MetaType type, bool r } // Returns true if the lhs value should sort before the rhs, false otherwise. -bool PathComparator::operator()(const std::string& lhs, const std::string& rhs) const { +bool PathPrefixComparator::operator()(const std::string& lhs, const std::string& rhs) const { constexpr char dot = '.'; for (size_t pos = 0, len = std::min(lhs.size(), rhs.size()); pos < len; ++pos) { diff --git a/src/mongo/db/pipeline/dependencies.h b/src/mongo/db/pipeline/dependencies.h index 963584ba148..3c892de8181 100644 --- a/src/mongo/db/pipeline/dependencies.h +++ b/src/mongo/db/pipeline/dependencies.h @@ -39,21 +39,6 @@ namespace mongo { /** - * Custom comparator that orders fieldpath strings by path prefix first, then by field. - * This ensures that a parent field is ordered directly before its children. - */ -struct PathComparator { - /* Returns true if the lhs value should sort before the rhs, false otherwise. */ - bool operator()(const std::string& lhs, const std::string& rhs) const; -}; - -/** - * Set of field paths strings. When iterated over, a parent path is seen directly before its - * children (or descendants, more generally). Eg., "a", "a.a", "a.b", "a-plus", "b". - */ -typedef std::set<std::string, PathComparator> OrderedPathSet; - -/** * This struct allows components in an agg pipeline to report what they need from their input. */ struct DepsTracker { @@ -119,16 +104,6 @@ struct DepsTracker { enum class TruncateToRootLevel : bool { no, yes }; /** - * Return the set of dependencies with descendant paths removed. - * For example ["a.b", "a.b.f", "c"] --> ["a.b", "c"]. - * - * TruncateToRootLevel::yes requires all dependencies to be top-level. - * The example above would return ["a", "c"] - */ - static OrderedPathSet simplifyDependencies(OrderedPathSet dependencies, - TruncateToRootLevel truncation); - - /** * Returns a projection object covering the non-metadata dependencies tracked by this class, or * empty BSONObj if the entire document is required. By default, the resulting project will * include the full, dotted field names of the dependencies. If 'truncationBehavior' is set to @@ -210,11 +185,11 @@ struct DepsTracker { } /** - * Return names of needed fields in dotted notation. A custom comparator orders the fields - * such that a parent is immediately before its children. + * Return fieldpaths ordered such that a parent is immediately before its children. */ - OrderedPathSet fields; + std::list<std::string> sortedFields() const; + std::set<std::string> fields; // Names of needed fields in dotted notation. std::set<Variables::Id> vars; // IDs of referenced variables. bool needWholeDocument = false; // If true, ignore 'fields'; the whole document is needed. @@ -232,4 +207,12 @@ private: QueryMetadataBitSet _metadataDeps; }; + +/** Custom comparator that orders fieldpath strings by path prefix first, then by field. + * This ensures that a parent field is ordered directly before its children. + */ +struct PathPrefixComparator { + /* Returns true if the lhs value should sort before the rhs, false otherwise. */ + bool operator()(const std::string& lhs, const std::string& rhs) const; +}; } // namespace mongo diff --git a/src/mongo/db/pipeline/dependencies_test.cpp b/src/mongo/db/pipeline/dependencies_test.cpp index 75451b258b8..938130b91bd 100644 --- a/src/mongo/db/pipeline/dependencies_test.cpp +++ b/src/mongo/db/pipeline/dependencies_test.cpp @@ -45,8 +45,8 @@ using std::set; using std::string; template <size_t ArrayLen> -OrderedPathSet arrayToSet(const char* (&array)[ArrayLen]) { - OrderedPathSet out; +set<string> arrayToSet(const char* (&array)[ArrayLen]) { + set<string> out; for (size_t i = 0; i < ArrayLen; i++) out.insert(array[i]); return out; @@ -306,17 +306,19 @@ TEST(DependenciesToProjectionTest, SortFieldPaths) { "b.a" "b.aa" "b.🌲d"}; - auto fields = arrayToSet(array); + DepsTracker deps; + deps.fields = arrayToSet(array); // our custom sort will restore the ordering above - auto itr = fields.begin(); - for (unsigned long i = 0; i < fields.size(); i++) { + std::list<std::string> fieldPathSorted = deps.sortedFields(); + auto itr = fieldPathSorted.begin(); + for (unsigned long i = 0; i < fieldPathSorted.size(); i++) { ASSERT_EQ(*itr, array[i]); ++itr; } } TEST(DependenciesToProjectionTest, PathLessThan) { - auto lessThan = PathComparator(); + auto lessThan = PathPrefixComparator(); ASSERT_FALSE(lessThan("a", "a")); ASSERT_TRUE(lessThan("a", "aa")); ASSERT_TRUE(lessThan("a", "b")); diff --git a/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp b/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp index ac8924a13ed..069a7e2f0b2 100644 --- a/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp +++ b/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp @@ -30,7 +30,7 @@ #include "mongo/db/pipeline/aggregation_request_helper.h" #include "mongo/db/pipeline/sharded_agg_helpers.h" #include "mongo/s/query/sharded_agg_test_fixture.h" -#include "mongo/s/router_role.h" +#include "mongo/s/router.h" namespace mongo { namespace { @@ -53,14 +53,10 @@ TEST_F(DispatchShardPipelineTest, DoesNotSplitPipelineIfTargetingOneShard) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; - const bool startsWithDocuments = false; auto future = launchAsync([&] { - auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand, - hasChangeStream, - startsWithDocuments, - std::move(pipeline), - boost::none /*explain*/); + auto results = sharded_agg_helpers::dispatchShardPipeline( + serializedCommand, hasChangeStream, std::move(pipeline)); ASSERT_EQ(results.remoteCursors.size(), 1UL); ASSERT(!results.splitPipeline); }); @@ -88,14 +84,10 @@ TEST_F(DispatchShardPipelineTest, DoesSplitPipelineIfMatchSpansTwoShards) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; - const bool startsWithDocuments = false; auto future = launchAsync([&] { - auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand, - hasChangeStream, - startsWithDocuments, - std::move(pipeline), - boost::none /*explain*/); + auto results = sharded_agg_helpers::dispatchShardPipeline( + serializedCommand, hasChangeStream, std::move(pipeline)); ASSERT_EQ(results.remoteCursors.size(), 2UL); ASSERT(bool(results.splitPipeline)); }); @@ -126,14 +118,10 @@ TEST_F(DispatchShardPipelineTest, DispatchShardPipelineRetriesOnNetworkError) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; - const bool startsWithDocuments = false; auto future = launchAsync([&] { // Shouldn't throw. - auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand, - hasChangeStream, - startsWithDocuments, - std::move(pipeline), - boost::none /*explain*/); + auto results = sharded_agg_helpers::dispatchShardPipeline( + serializedCommand, hasChangeStream, std::move(pipeline)); ASSERT_EQ(results.remoteCursors.size(), 2UL); ASSERT(bool(results.splitPipeline)); }); @@ -175,14 +163,9 @@ TEST_F(DispatchShardPipelineTest, DispatchShardPipelineDoesNotRetryOnStaleConfig const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; - const bool startsWithDocuments = false; - auto future = launchAsync([&] { - ASSERT_THROWS_CODE(sharded_agg_helpers::dispatchShardPipeline(serializedCommand, - hasChangeStream, - startsWithDocuments, - std::move(pipeline), - boost::none /*explain*/), + ASSERT_THROWS_CODE(sharded_agg_helpers::dispatchShardPipeline( + serializedCommand, hasChangeStream, std::move(pipeline)), AssertionException, ErrorCodes::StaleConfig); }); @@ -214,7 +197,6 @@ TEST_F(DispatchShardPipelineTest, WrappedDispatchDoesRetryOnStaleConfigError) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; - const bool startsWithDocuments = false; auto future = launchAsync([&] { // Shouldn't throw. sharding::router::CollectionRouter router(getServiceContext(), kTestAggregateNss); @@ -222,11 +204,7 @@ TEST_F(DispatchShardPipelineTest, WrappedDispatchDoesRetryOnStaleConfigError) { "dispatch shard pipeline"_sd, [&](OperationContext* opCtx, const ChunkManager& cm) { return sharded_agg_helpers::dispatchShardPipeline( - serializedCommand, - hasChangeStream, - startsWithDocuments, - pipeline->clone(), - boost::none /*explain*/); + serializedCommand, hasChangeStream, pipeline->clone()); }); ASSERT_EQ(results.remoteCursors.size(), 1UL); ASSERT(!bool(results.splitPipeline)); diff --git a/src/mongo/db/pipeline/document_path_support.cpp b/src/mongo/db/pipeline/document_path_support.cpp index eeb0831b55d..a57d0496a1d 100644 --- a/src/mongo/db/pipeline/document_path_support.cpp +++ b/src/mongo/db/pipeline/document_path_support.cpp @@ -136,18 +136,19 @@ StatusWith<Value> extractElementAlongNonArrayPath(const Document& doc, const Fie return curValue; } -void documentToBsonWithPaths(const Document& input, - const OrderedPathSet& paths, - BSONObjBuilder* builder) { +BSONObj documentToBsonWithPaths(const Document& input, const std::set<std::string>& paths) { + BSONObjBuilder outputBuilder; for (auto&& path : paths) { // getNestedField does not handle dotted paths correctly, so instead of retrieving the // entire path, we just extract the first element of the path. const auto prefix = FieldPath::extractFirstFieldFromDottedPath(path); - if (!builder->hasField(prefix)) { + if (!outputBuilder.hasField(prefix)) { // Avoid adding the same prefix twice. - input.getField(prefix).addToBsonObj(builder, prefix); + input.getField(prefix).addToBsonObj(&outputBuilder, prefix); } } + + return outputBuilder.obj(); } } // namespace document_path_support diff --git a/src/mongo/db/pipeline/document_path_support.h b/src/mongo/db/pipeline/document_path_support.h index b1c127af0e4..5d9f0a1cb6b 100644 --- a/src/mongo/db/pipeline/document_path_support.h +++ b/src/mongo/db/pipeline/document_path_support.h @@ -63,14 +63,7 @@ StatusWith<Value> extractElementAlongNonArrayPath(const Document& doc, const Fie /** * Extracts 'paths' from the input document and returns a BSON object containing only those paths. */ -void documentToBsonWithPaths(const Document&, const OrderedPathSet& paths, BSONObjBuilder* builder); - -template <typename BSONTraits = BSONObj::DefaultSizeTrait> -BSONObj documentToBsonWithPaths(const Document& input, const OrderedPathSet& paths) { - BSONObjBuilder outputBuilder; - documentToBsonWithPaths(input, paths, &outputBuilder); - return outputBuilder.obj<BSONTraits>(); -} +BSONObj documentToBsonWithPaths(const Document&, const std::set<std::string>& paths); /** * Extracts 'paths' from the input document to a flat document. diff --git a/src/mongo/db/pipeline/document_path_support_test.cpp b/src/mongo/db/pipeline/document_path_support_test.cpp index 489575ba6aa..5e966e08a75 100644 --- a/src/mongo/db/pipeline/document_path_support_test.cpp +++ b/src/mongo/db/pipeline/document_path_support_test.cpp @@ -351,8 +351,8 @@ TEST(DocumentToBsonWithPathsTest, MissingFieldShouldNotAppearInResult) { TEST(DocumentToBsonWithPathsTest, ShouldSerializeNothingIfNothingIsNeeded) { Document input(fromjson("{a: 1, b: {c: 1}}")); BSONObj expected; - ASSERT_BSONOBJ_EQ(expected, - document_path_support::documentToBsonWithPaths(input, OrderedPathSet{})); + ASSERT_BSONOBJ_EQ( + expected, document_path_support::documentToBsonWithPaths(input, std::set<std::string>{})); } TEST(DocumentToBsonWithPathsTest, ShouldExtractEntireArrayFromPrefixOfDottedField) { @@ -361,26 +361,6 @@ TEST(DocumentToBsonWithPathsTest, ShouldExtractEntireArrayFromPrefixOfDottedFiel ASSERT_BSONOBJ_EQ(expected, document_path_support::documentToBsonWithPaths(input, {"a.b"})); } -TEST(DocumentToBsonWithPathsTest, SizeTraits) { - 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)); - md.addField("b", Value(longString)); - ASSERT_DOES_NOT_THROW(document_path_support::documentToBsonWithPaths(md.peek(), {"a"})); - ASSERT_THROWS_CODE(document_path_support::documentToBsonWithPaths(md.peek(), {"a", "b"}), - DBException, - ErrorCodes::BSONObjectTooLarge); - ASSERT_THROWS_CODE(document_path_support::documentToBsonWithPaths<BSONObj::DefaultSizeTrait>( - md.peek(), {"a", "b"}), - DBException, - ErrorCodes::BSONObjectTooLarge); - ASSERT_DOES_NOT_THROW(document_path_support::documentToBsonWithPaths<BSONObj::LargeSizeTrait>( - md.peek(), {"a", "b"})); -} } // namespace } // namespace document_path_support } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source.cpp b/src/mongo/db/pipeline/document_source.cpp index ea1cf479480..3a5d5d0030c 100644 --- a/src/mongo/db/pipeline/document_source.cpp +++ b/src/mongo/db/pipeline/document_source.cpp @@ -36,13 +36,11 @@ #include "mongo/db/commands/feature_compatibility_version_documentation.h" #include "mongo/db/exec/document_value/value.h" #include "mongo/db/matcher/expression_algo.h" -#include "mongo/db/pipeline/change_stream_constants.h" #include "mongo/db/pipeline/document_source_add_fields.h" #include "mongo/db/pipeline/document_source_group.h" #include "mongo/db/pipeline/document_source_internal_shard_filter.h" #include "mongo/db/pipeline/document_source_match.h" #include "mongo/db/pipeline/document_source_project.h" -#include "mongo/db/pipeline/document_source_redact.h" #include "mongo/db/pipeline/document_source_replace_root.h" #include "mongo/db/pipeline/document_source_sample.h" #include "mongo/db/pipeline/document_source_sequential_document_cache.h" @@ -172,77 +170,45 @@ bool groupMatchSwapVerified(const DocumentSourceMatch& nextMatch, return !expression::hasExistencePredicateOnPath(*(nextMatch.getMatchExpression()), "_id"_sd); } -/** - * Returns 'true' if the given stage is an internal change stream stage that can appear in a router - * (mongoS) pipeline, or 'false' otherwise. - */ -bool isChangeStreamRouterPipelineStage(StringData stageName) { - return change_stream_constants::kChangeStreamRouterPipelineStages.contains(stageName); -} } // namespace bool DocumentSource::pushMatchBefore(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - if (!constraints().canSwapWithMatch) { - return false; - } - - auto nextStageAsMatch = dynamic_cast<DocumentSourceMatch*>((*std::next(itr)).get()); - if (!nextStageAsMatch || nextStageAsMatch->isTextQuery()) { - // We do not need to attempt this optimization if the $match contains a text search - // predicate because, in that scenario, $match is already required to be the first stage in - // the pipeline. - return false; - } - - // At this point: - // 1) The next stage after 'this' is $match. - // 2) The $match stage does not contain a text search predicate. - - // TODO SERVER-55492: Remove the following workaround when there are rename checks for 'other' - // match expressions. - if (isChangeStreamRouterPipelineStage(this->getSourceName())) { - // Always move the $match stage ahead of internal change stream stages appearing in the - // router (mongoS) pipeline, because they do not access or modify any paths in the input - // document. - container->splice(itr, *container, std::next(itr)); - return true; - } - - auto thisStageAsGroup = dynamic_cast<DocumentSourceGroup*>(this); - if (thisStageAsGroup && !groupMatchSwapVerified(*nextStageAsMatch, *thisStageAsGroup)) { - return false; - } - - auto [renameableMatchPart, nonRenameableMatchPart] = - DocumentSourceMatch::splitMatchByModifiedFields(nextStageAsMatch, getModifiedPaths()); - invariant(renameableMatchPart || nonRenameableMatchPart); - if (!renameableMatchPart) { - return false; - } - - LOGV2_DEBUG(5943503, + auto nextMatch = dynamic_cast<DocumentSourceMatch*>((*std::next(itr)).get()); + auto thisGroup = dynamic_cast<DocumentSourceGroup*>(this); + if (constraints().canSwapWithMatch && nextMatch && !nextMatch->isTextQuery() && + (!thisGroup || groupMatchSwapVerified(*nextMatch, *thisGroup))) { + // We're allowed to swap with a $match and the stage after us is a $match. Furthermore, the + // $match does not contain a text search predicate, which we do not attempt to optimize + // because such a $match must already be the first stage in the pipeline. We can attempt to + // swap the $match or part of the $match before ourselves. + auto splitMatch = + DocumentSourceMatch::splitMatchByModifiedFields(nextMatch, getModifiedPaths()); + invariant(splitMatch.first || splitMatch.second); + + if (splitMatch.first) { + // At least part of the $match can be moved before this stage. Erase the original $match + // and put the independent part before this stage. If splitMatch.second is not null, + // then there is a new $match stage to insert after ourselves which is dependent on the + // modified fields. + LOGV2_DEBUG( + 5943503, 5, "Swapping all or part of a $match stage in front of another stage: ", - "matchMovingBefore"_attr = redact(renameableMatchPart->serializeToBSONForDebug()), + "matchMovingBefore"_attr = redact(splitMatch.first->serializeToBSONForDebug()), "thisStage"_attr = redact(serializeToBSONForDebug()), "matchLeftAfter"_attr = redact( - nonRenameableMatchPart ? nonRenameableMatchPart->serializeToBSONForDebug() - : BSONObj())); - - // At this point we know that at least part of the $match expression can be moved ahead of - // 'this'. So, we erase the original $match and move that renameable part ahead of 'this' stage. - container->erase(std::next(itr)); - container->insert(itr, std::move(renameableMatchPart)); - - // If 'nonRenameableMatchPart' is not null, the 'renameableMatchPart' of the $match expression - // was only one component of the original $match. So, we need to create a new $match stage for - // the remaining 'nonRenameableMatchPart' and insert it after 'this' - effectively keeping it in - // its original position in the pipeline. - if (nonRenameableMatchPart) { - container->insert(std::next(itr), std::move(nonRenameableMatchPart)); + splitMatch.second ? splitMatch.second->serializeToBSONForDebug() : BSONObj())); + container->erase(std::next(itr)); + container->insert(itr, std::move(splitMatch.first)); + if (splitMatch.second) { + container->insert(std::next(itr), std::move(splitMatch.second)); + } + + return true; + } } - return true; + return false; } bool DocumentSource::pushSampleBefore(Pipeline::SourceContainer::iterator itr, @@ -260,9 +226,7 @@ bool DocumentSource::pushSampleBefore(Pipeline::SourceContainer::iterator itr, BSONObj DocumentSource::serializeToBSONForDebug() const { std::vector<Value> serialized; - auto opts = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; - serializeToArray(serialized, opts); + serializeToArray(serialized, ExplainOptions::Verbosity::kQueryPlanner); if (serialized.empty()) { LOGV2_DEBUG(5943501, 5, @@ -276,24 +240,21 @@ BSONObj DocumentSource::serializeToBSONForDebug() const { return serialized[0].getDocument().toBson(); } -bool DocumentSource::pushSingleDocumentTransformOrRedactBefore( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - if (constraints().canSwapWithSingleDocTransformOrRedact) { - auto nextItr = std::next(itr); - if (dynamic_cast<DocumentSourceSingleDocumentTransformation*>(nextItr->get()) || - dynamic_cast<DocumentSourceRedact*>(nextItr->get())) { - LOGV2_DEBUG(5943500, - 5, - "Pushing a single document transform stage or a redact stage in ahead of " - "the current stage: ", - "singleDocTransformOrRedactStage"_attr = - redact((*nextItr)->serializeToBSONForDebug()), - "currentStage"_attr = redact(serializeToBSONForDebug())); - - // Swap 'itr' and 'nextItr' list nodes. - container->splice(itr, *container, nextItr); - return true; - } +bool DocumentSource::pushSingleDocumentTransformBefore(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container) { + auto singleDocTransform = + dynamic_cast<DocumentSourceSingleDocumentTransformation*>((*std::next(itr)).get()); + + if (constraints().canSwapWithSingleDocTransform && singleDocTransform) { + LOGV2_DEBUG(5943500, + 5, + "Swapping a single document transform stage in front of another stage: ", + "singleDocTransform"_attr = + redact(singleDocTransform->serializeToBSONForDebug()), + "thisStage"_attr = redact(serializeToBSONForDebug())); + container->insert(itr, std::move(singleDocTransform)); + container->erase(std::next(itr)); + return true; } return false; } @@ -313,43 +274,15 @@ Pipeline::SourceContainer::iterator DocumentSource::optimizeAt( } void DocumentSource::serializeToArray(vector<Value>& array, - const SerializationOptions& opts) const { - Value entry = serialize(opts); + boost::optional<ExplainOptions::Verbosity> explain) const { + Value entry = serialize(explain); if (!entry.missing()) { array.push_back(entry); } } -namespace { -std::list<boost::intrusive_ptr<DocumentSource>> throwOnParse( - BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - uasserted(6047400, spec.fieldNameStringData() + " stage is only allowed on MongoDB Atlas"); -} -std::unique_ptr<LiteParsedDocumentSource> throwOnParseLite(NamespaceString nss, - const BSONElement& spec) { - uasserted(6047401, spec.fieldNameStringData() + " stage is only allowed on MongoDB Atlas"); -} -} // namespace MONGO_INITIALIZER_GROUP(BeginDocumentSourceRegistration, ("default"), ("EndDocumentSourceRegistration")) -// Any remaining work on the parserMap should be done before finishing DocumentSource Registration. -MONGO_INITIALIZER_WITH_PREREQUISITES(EndDocumentSourceRegistration, - ("BeginDocumentSourceRegistration")) -(InitializerContext*) { - auto searchStageNames = { - "$vectorSearch"_sd, "$search"_sd, "$searchMeta"_sd, "$listSearchIndexes"_sd}; - for (auto stageName : searchStageNames) { - auto searchIt = parserMap.find(stageName); - // If the stage has not been registered at this point, register a parser that errors - // with a useful error message on parsing a search stage. - if (searchIt == parserMap.end()) { - LiteParsedDocumentSource::registerParser(stageName.toString(), - throwOnParseLite, - AllowedWithApiStrict::kAlways, - AllowedWithClientType::kAny); - DocumentSource::registerParser(stageName.toString(), throwOnParse, boost::none); - } - } -} +MONGO_INITIALIZER_GROUP(EndDocumentSourceRegistration, ("BeginDocumentSourceRegistration"), ()) } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source.h b/src/mongo/db/pipeline/document_source.h index 4e4a5bfdadd..ee3977aed18 100644 --- a/src/mongo/db/pipeline/document_source.h +++ b/src/mongo/db/pipeline/document_source.h @@ -319,8 +319,8 @@ public: * original's 'ExpressionContext'. */ virtual boost::intrusive_ptr<DocumentSource> clone( - const boost::intrusive_ptr<ExpressionContext>& expCtx) const { - tassert(7406001, "expCtx passed to clone must not be null", expCtx); + const boost::intrusive_ptr<ExpressionContext>& newExpCtx = nullptr) const { + auto expCtx = newExpCtx ? newExpCtx : pExpCtx; std::vector<Value> serializedDoc; serializeToArray(serializedDoc); tassert(5757900, @@ -381,16 +381,6 @@ public: Pipeline::SplitState = Pipeline::SplitState::kUnsplit) const = 0; /** - * If a stage's StageConstraints::PositionRequirement is kCustom, then it should also override - * this method, which will be called by the validation process. - */ - virtual void validatePipelinePosition(bool alreadyOptimized, - Pipeline::SourceContainer::const_iterator pos, - const Pipeline::SourceContainer& container) const { - MONGO_UNIMPLEMENTED_TASSERT(7183905); - }; - - /** * Informs the stage that it is no longer needed and can release its resources. After dispose() * is called the stage must still be able to handle calls to getNext(), but can return kEOF. * @@ -437,9 +427,13 @@ public: * * A subclass may choose to overwrite this, rather than serialize, if it should output multiple * stages (eg, $sort sometimes also outputs a $limit). + * + * The 'explain' parameter indicates the explain verbosity mode, or is equal boost::none if no + * explain is requested. */ - virtual void serializeToArray(std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const; + virtual void serializeToArray( + std::vector<Value>& array, + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const; /** * Shortcut method to get a BSONObj for debugging. Often useful in log messages, but is not @@ -458,14 +452,6 @@ public: virtual void reattachToOperationContext(OperationContext* opCtx) {} - /** - * Validate that all operation contexts associated with this document source, including any - * subpipelines, match the argument. - */ - virtual bool validateOperationContext(const OperationContext* opCtx) const { - return getContext()->opCtx == opCtx; - } - virtual bool usedDisk() { return false; }; @@ -537,15 +523,15 @@ private: Pipeline::SourceContainer* container); /** - * Attempts to push any kind of 'DocumentSourceSingleDocumentTransformation' stage or a $redact - * stage directly ahead of the stage present at the 'itr' position if matches the constraints. - * Returns true if optimization was performed, false otherwise. + * Attempts to push any kind of 'DocumentSourceSingleDocumentTransformation' stage directly + * ahead of the stage present at the 'itr' position if matches the constraints. Returns true if + * optimization was performed, false otherwise. * * Note that this optimization is oblivious to the transform function. The only stages that are * eligible to swap are those that can safely swap with any transform. */ - bool pushSingleDocumentTransformOrRedactBefore(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container); + bool pushSingleDocumentTransformBefore(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container); /** * Wraps various optimization methods and returns the call immediately if any one of them @@ -558,7 +544,7 @@ private: } return pushMatchBefore(itr, container) || pushSampleBefore(itr, container) || - pushSingleDocumentTransformOrRedactBefore(itr, container); + pushSingleDocumentTransformBefore(itr, container); } public: @@ -615,7 +601,9 @@ public: kAllExcept, }; - GetModPathsReturn(Type type, OrderedPathSet&& paths, StringMap<std::string>&& renames) + GetModPathsReturn(Type type, + std::set<std::string>&& paths, + StringMap<std::string>&& renames) : type(type), paths(std::move(paths)), renames(std::move(renames)) {} std::set<std::string> getNewNames() { @@ -661,11 +649,11 @@ public: return true; } // Cannot hit. - MONGO_UNREACHABLE_TASSERT(6434902); + MONGO_UNREACHABLE_TASSERT(6434901); } Type type; - OrderedPathSet paths; + std::set<std::string> paths; // Stages may fill out 'renames' to contain information about path renames. Each entry in // 'renames' maps from the new name of the path (valid in documents flowing *out* of this @@ -688,7 +676,7 @@ public: * See GetModPathsReturn above for the possible return values and what they mean. */ virtual GetModPathsReturn getModifiedPaths() const { - return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>{}, {}}; } /** @@ -726,7 +714,7 @@ public: * parallel since it will preserve the shard key. */ virtual bool canRunInParallelBeforeWriteStage( - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const { + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const { return false; } @@ -790,8 +778,12 @@ private: * This is used by the default implementation of serializeToArray() to add this object * to a pipeline being serialized. Returning a missing() Value results in no entry * being added to the array for this stage (DocumentSource). + * + * The 'explain' parameter indicates the explain verbosity mode, or is equal boost::none if no + * explain is requested. */ - virtual Value serialize(const SerializationOptions& opts = SerializationOptions{}) const = 0; + virtual Value serialize( + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const = 0; }; /** diff --git a/src/mongo/db/pipeline/document_source_bucket.cpp b/src/mongo/db/pipeline/document_source_bucket.cpp index acf56f771b9..4de8daee491 100644 --- a/src/mongo/db/pipeline/document_source_bucket.cpp +++ b/src/mongo/db/pipeline/document_source_bucket.cpp @@ -71,12 +71,6 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceBucket::createFromBson( BSONElement groupByField; Value defaultValue; - // Validating the input boundaries and default value requires a correct collation. We may not - // have this at parse time if we are not parsing for the purpose of executing the queries (see - // SERVER-87166), so we should skip the validation for that case. - const bool shouldValidateInputRanges = - pExpCtx->mongoProcessInterface->isExpectedToExecuteQueries(); - bool outputFieldSpecified = false; for (auto&& argument : bucketObj) { const auto argName = argument.fieldNameStringData(); @@ -84,10 +78,10 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceBucket::createFromBson( groupByField = argument; const bool groupByIsExpressionInObject = groupByField.type() == BSONType::Object && - groupByField.embeddedObject().firstElementFieldNameStringData().startsWith("$"); + groupByField.embeddedObject().firstElementFieldName()[0] == '$'; - const bool groupByIsPrefixedPath = groupByField.type() == BSONType::String && - groupByField.valueStringData().startsWith("$"); + const bool groupByIsPrefixedPath = + groupByField.type() == BSONType::String && groupByField.valueStringData()[0] == '$'; uassert(40202, str::stream() << "The $bucket 'groupBy' field must be defined as a $-prefixed " "path or an expression, but found: " @@ -135,8 +129,7 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceBucket::createFromBson( << "The 'boundaries' option to $bucket must be sorted, but elements " << i - 1 << " and " << i << " are not in ascending order (" << lower.toString() << " is not less than " << upper.toString() << ").", - !shouldValidateInputRanges || - pExpCtx->getValueComparator().evaluate(lower < upper)); + pExpCtx->getValueComparator().evaluate(lower < upper)); } } else if ("default" == argName) { // If there is a default, make sure that it parses to a constant expression then add @@ -173,9 +166,8 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceBucket::createFromBson( Value lowerValue = boundaryValues.front(); Value upperValue = boundaryValues.back(); - if (shouldValidateInputRanges && - canonicalizeBSONType(defaultValue.getType()) == - canonicalizeBSONType(lowerValue.getType())) { + if (canonicalizeBSONType(defaultValue.getType()) == + canonicalizeBSONType(lowerValue.getType())) { // If the default has the same canonical type as the bucket's boundaries, then make sure the // default is less than the lowest boundary or greater than or equal to the highest // boundary. diff --git a/src/mongo/db/pipeline/document_source_bucket_auto.cpp b/src/mongo/db/pipeline/document_source_bucket_auto.cpp index cec0be7b847..10300e9c334 100644 --- a/src/mongo/db/pipeline/document_source_bucket_auto.cpp +++ b/src/mongo/db/pipeline/document_source_bucket_auto.cpp @@ -57,15 +57,15 @@ boost::intrusive_ptr<Expression> parseGroupByExpression( groupByField.embeddedObject().firstElementFieldName()[0] == '$') { return Expression::parseObject(expCtx.get(), groupByField.embeddedObject(), vps); } else if (groupByField.type() == BSONType::String && - // Lager than 2 because we need a '$', at least one char for the field name and - // the final terminating 0. - groupByField.valuestrsize() > 2 && groupByField.valueStringData()[0] == '$') { + groupByField.valueStringData()[0] == '$') { return ExpressionFieldPath::parse(expCtx.get(), groupByField.str(), vps); + } else { + uasserted( + 40239, + str::stream() << "The $bucketAuto 'groupBy' field must be defined as a $-prefixed " + "path or an expression object, but found: " + << groupByField.toString(false, false)); } - uasserted(40239, - str::stream() << "The $bucketAuto 'groupBy' field must be defined as a $-prefixed " - "path or an expression object, but found: " - << groupByField.toString(false, false)); } /** @@ -221,7 +221,7 @@ void DocumentSourceBucketAuto::initializeBucketIteration() { auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx); metricsCollector.incrementKeysSorted(_sorter->numSorted()); - metricsCollector.incrementSorterSpills(_sorter->stats().spilledRanges()); + metricsCollector.incrementSorterSpills(_sorter->numSpills()); _sorter.reset(); @@ -372,25 +372,24 @@ void DocumentSourceBucketAuto::doDispose() { _sortedInput.reset(); } -Value DocumentSourceBucketAuto::serialize(const SerializationOptions& opts) const { +Value DocumentSourceBucketAuto::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument insides; - insides["groupBy"] = _groupByExpression->serialize(opts); - insides["buckets"] = opts.serializeLiteral(_nBuckets); + insides["groupBy"] = _groupByExpression->serialize(static_cast<bool>(explain)); + insides["buckets"] = Value(_nBuckets); if (_granularityRounder) { - //"granularity" only supports some strings, so a specific representative value is used if - // necessary. - insides["granularity"] = - opts.serializeLiteral(_granularityRounder->getName(), Value("R5"_sd)); + insides["granularity"] = Value(_granularityRounder->getName()); } MutableDocument outputSpec(_accumulatedFields.size()); for (auto&& accumulatedField : _accumulatedFields) { intrusive_ptr<AccumulatorState> accum = accumulatedField.makeAccumulator(); - outputSpec[opts.serializeFieldPathFromString(accumulatedField.fieldName)] = - Value(accum->serialize( - accumulatedField.expr.initializer, accumulatedField.expr.argument, opts)); + outputSpec[accumulatedField.fieldName] = + Value(accum->serialize(accumulatedField.expr.initializer, + accumulatedField.expr.argument, + static_cast<bool>(explain))); } insides["output"] = outputSpec.freezeToValue(); diff --git a/src/mongo/db/pipeline/document_source_bucket_auto.h b/src/mongo/db/pipeline/document_source_bucket_auto.h index 6d8cd09089e..6a85e21c642 100644 --- a/src/mongo/db/pipeline/document_source_bucket_auto.h +++ b/src/mongo/db/pipeline/document_source_bucket_auto.h @@ -45,8 +45,7 @@ namespace mongo { class DocumentSourceBucketAuto final : public DocumentSource { public: static constexpr StringData kStageName = "$bucketAuto"_sd; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; DepsTracker::State getDependencies(DepsTracker* deps) const final; const char* getSourceName() const final; diff --git a/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp b/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp index 4bc3cb0e11a..5bcad8dc0bc 100644 --- a/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp +++ b/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp @@ -89,9 +89,8 @@ public: assertBucketAutoType(bucketAutoStage); vector<Value> explainedStages; - bucketAutoStage->serializeToArray( - explainedStages, - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}); + bucketAutoStage->serializeToArray(explainedStages, + ExplainOptions::Verbosity::kQueryPlanner); ASSERT_EQUALS(explainedStages.size(), 1UL); Value expectedExplain = Value(expectedObj); @@ -576,27 +575,12 @@ TEST_F(BucketAutoTests, FailsWithInvalidNumberOfBuckets) { 40243); } -TEST_F(BucketAutoTests, FailsWithNonOrInvalidExpressionGroupBy) { +TEST_F(BucketAutoTests, FailsWithNonExpressionGroupBy) { auto spec = fromjson("{$bucketAuto : {groupBy : 'test', buckets : 1}}"); ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); spec = fromjson("{$bucketAuto : {groupBy : {test : 'test'}, buckets : 1}}"); ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); - - spec = fromjson("{$bucketAuto : {groupBy : '', buckets : 1}}"); - ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); - - spec = fromjson("{$bucketAuto : {groupBy : {}}, buckets : 1}}"); - ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); - - spec = fromjson("{$bucketAuto : {groupBy : '$'}, buckets : 1}}"); - ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); - - spec = fromjson("{$bucketAuto : {groupBy : []}, buckets : 1}}"); - ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); - - spec = fromjson("{$bucketAuto : {groupBy : null}, buckets : 1}}"); - ASSERT_THROWS_CODE(createBucketAuto(spec), AssertionException, 40239); } TEST_F(BucketAutoTests, FailsWithNonObjectArgument) { @@ -869,109 +853,5 @@ TEST_F(BucketAutoTests, ShouldFailOnNegativeNumbersWhenGranularitySpecified) { AssertionException, 40260); } - -TEST_F(BucketAutoTests, RedactionWithoutOutputField) { - auto spec = fromjson(R"({ - $bucketAuto: { - groupBy: '$_id', - buckets: 5, - granularity: "R5" - } - })"); - auto docSource = DocumentSourceBucketAuto::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$bucketAuto": { - "groupBy": "$HASH<_id>", - "buckets": "?number", - "granularity": "?string", - "output": { - "HASH<count>": { - "$sum": "?number" - } - } - } - })", - redact(*docSource)); -} - -TEST_F(BucketAutoTests, RedactionWithOutputField) { - auto spec = fromjson(R"({ - $bucketAuto: { - groupBy: '$year', - buckets: 3, - output: { - count: { $sum: 1 }, - years: { $push: '$year' } - } - }})"); - auto docSource = DocumentSourceBucketAuto::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$bucketAuto": { - "groupBy": "$HASH<year>", - "buckets": "?number", - "output": { - "HASH<count>": { - "$sum": "?number" - }, - "HASH<years>": { - "$push": "$HASH<year>" - } - } - } - })", - redact(*docSource)); -} - -TEST_F(BucketAutoTests, QueryShapeReParseSerializedStage) { - auto expCtx = getExpCtx(); - auto spec = fromjson(R"({ - $bucketAuto: { - groupBy: '$year', - buckets: 3, - granularity: "E192", - output: { - count: { $sum: 1 }, - years: { $push: '$year' } - } - }})"); - - auto docSource = DocumentSourceBucketAuto::createFromBson(spec.firstElement(), expCtx); - auto opts = SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}; - std::vector<Value> serialized; - docSource->serializeToArray(serialized, opts); - auto serializedDocSource = serialized[0].getDocument().toBson(); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$bucketAuto": { - "groupBy": "$year", - "buckets": 1, - "granularity": "R5", - "output": { - "count": { - "$sum": { - "$const":1 - } - }, - "years": { - "$push": "$year" - } - } - } - })", - serializedDocSource); - auto docSourceFromQueryShape = - DocumentSourceBucketAuto::createFromBson(serializedDocSource.firstElement(), expCtx); - - vector<Value> newSerialization; - docSourceFromQueryShape->serializeToArray(newSerialization, opts); - auto newSerializedDocSource = newSerialization[0].getDocument().toBson(); - ASSERT_BSONOBJ_EQ(serializedDocSource, newSerializedDocSource); -} - - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_bucket_test.cpp b/src/mongo/db/pipeline/document_source_bucket_test.cpp index e7702d58d28..1036c456ef3 100644 --- a/src/mongo/db/pipeline/document_source_bucket_test.cpp +++ b/src/mongo/db/pipeline/document_source_bucket_test.cpp @@ -79,8 +79,7 @@ public: // Serialize the DocumentSourceGroup and DocumentSourceSort from $bucket so that we can // check the explain output to make sure $group and $sort have the correct fields. - auto explain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; + auto explain = ExplainOptions::Verbosity::kQueryPlanner; vector<Value> explainedStages; groupStage->serializeToArray(explainedStages, explain); sortStage->serializeToArray(explainedStages, explain); @@ -228,35 +227,9 @@ TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWithMultipleBoundaryValues) { testCreateFromBsonResult(spec, expectedGroupExplain); } -TEST_F(BucketReturnsGroupAndSort, BucketWithEmptyGroupByStrDoesNotAccessPastEndOfString) { - // Verify that {groupBy: ''} is rejected _without_ attempting to read past the end of the empty - // string. - const auto spec = - fromjson("{$bucket : {groupBy : '', boundaries : [ 1, 5, 8 ], default : 'other'}}"); - - // Under a debug build, this would previously fail if an empty str for groupBy led to access - // past the end of the string, with pos() > size() in StringData::operator[]. - // Verify that this reaches the intended uassert, rejecting the empty string, _without_ first - // trying to read past the end of the string. - ASSERT_THROWS_CODE(DocumentSourceBucket::createFromBson(spec.firstElement(), getExpCtx()), - AssertionException, - 40202); -} - -/* - * Override the stub interface to allow full execution in these tests. - */ -class ExecutableStubMongoProcessInterface : public StubMongoProcessInterface { - bool isExpectedToExecuteQueries() override { - return true; - } -}; - class InvalidBucketSpec : public AggregationContextFixture { public: list<intrusive_ptr<DocumentSource>> createBucket(BSONObj bucketSpec) { - getExpCtx()->mongoProcessInterface = - std::make_unique<ExecutableStubMongoProcessInterface>(); auto sources = DocumentSourceBucket::createFromBson(bucketSpec.firstElement(), getExpCtx()); return sources; } diff --git a/src/mongo/db/pipeline/document_source_change_stream.cpp b/src/mongo/db/pipeline/document_source_change_stream.cpp index 3f19d4ac4ee..4fc0041cc0e 100644 --- a/src/mongo/db/pipeline/document_source_change_stream.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream.cpp @@ -37,7 +37,6 @@ #include "mongo/db/pipeline/aggregate_command_gen.h" #include "mongo/db/pipeline/change_stream_constants.h" #include "mongo/db/pipeline/change_stream_filter_helpers.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/change_stream_helpers_legacy.h" #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/document_source_change_stream_add_post_image.h" @@ -48,7 +47,6 @@ #include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" #include "mongo/db/pipeline/document_source_change_stream_handle_topology_change.h" #include "mongo/db/pipeline/document_source_change_stream_oplog_match.h" -#include "mongo/db/pipeline/document_source_change_stream_split_large_event.h" #include "mongo/db/pipeline/document_source_change_stream_transform.h" #include "mongo/db/pipeline/document_source_change_stream_unwind_transaction.h" #include "mongo/db/pipeline/document_source_limit.h" @@ -228,6 +226,23 @@ std::string DocumentSourceChangeStream::regexEscapeNsForChangeStream(StringData return result; } +ResumeTokenData DocumentSourceChangeStream::resolveResumeTokenFromSpec( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const DocumentSourceChangeStreamSpec& spec) { + if (spec.getStartAfter()) { + return spec.getStartAfter()->getData(); + } else if (spec.getResumeAfter()) { + return spec.getResumeAfter()->getData(); + } else if (spec.getStartAtOperationTime()) { + return ResumeToken::makeHighWaterMarkToken(*spec.getStartAtOperationTime(), + expCtx->changeStreamTokenVersion) + .getData(); + } + tasserted(5666901, + "Expected one of 'startAfter', 'resumeAfter' or 'startAtOperationTime' to be " + "populated in $changeStream spec"); +} + Timestamp DocumentSourceChangeStream::getStartTimeForNewStream( const boost::intrusive_ptr<ExpressionContext>& expCtx) { // If we do not have an explicit starting point, we should start from the latest majority @@ -257,30 +272,6 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::createFromBson( // Make sure that it is legal to run this $changeStream before proceeding. DocumentSourceChangeStream::assertIsLegalSpecification(expCtx, spec); - // If the user did not specify an explicit starting point, set it to the current time. - if (!spec.getResumeAfter() && !spec.getStartAfter() && !spec.getStartAtOperationTime()) { - // Make sure we update the 'startAtOperationTime' in the 'spec' so that we serialize the - // correct start point when sending it to the shards. - spec.setStartAtOperationTime(DocumentSourceChangeStream::getStartTimeForNewStream(expCtx)); - } - - // If the stream's default version differs from the client's token version, adopt the higher. - // This is the token version that will be used once the stream has passed the resume token. - const auto clientToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); - expCtx->changeStreamTokenVersion = - std::max(expCtx->changeStreamTokenVersion, clientToken.version); - - // If the user explicitly requested to resume from a high water mark token, but its version - // differs from the version chosen above, regenerate it with the new version. There is no need - // for a resumed HWM stream to adopt the old token version for events at the same clusterTime. - const bool tokenVersionsDiffer = (clientToken.version != expCtx->changeStreamTokenVersion); - const bool isHighWaterMark = ResumeToken::isHighWaterMarkToken(clientToken); - if (isHighWaterMark && tokenVersionsDiffer && (spec.getResumeAfter() || spec.getStartAfter())) { - spec.setResumeAfter(ResumeToken(ResumeToken::makeHighWaterMarkToken( - clientToken.clusterTime, expCtx->changeStreamTokenVersion))); - spec.setStartAfter(boost::none); - } - // Save a copy of the spec on the expression context. Used when building the oplog filter. expCtx->changeStreamSpec = spec; @@ -296,8 +287,15 @@ std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::_bui const boost::intrusive_ptr<ExpressionContext>& expCtx, DocumentSourceChangeStreamSpec spec) { std::list<boost::intrusive_ptr<DocumentSource>> stages; + // If the user did not specify an explicit starting point, set it to the current time. + if (!spec.getResumeAfter() && !spec.getStartAfter() && !spec.getStartAtOperationTime()) { + // Make sure we update the 'startAtOperationTime' in the 'spec' so that we serialize the + // correct start point when sending it to the shards. + spec.setStartAtOperationTime(DocumentSourceChangeStream::getStartTimeForNewStream(expCtx)); + } + // Obtain the resume token from the spec. This will be used when building the pipeline. - auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); // Unfold the $changeStream into its constituent stages and add them to the pipeline. stages.push_back(DocumentSourceChangeStreamOplogMatch::create(expCtx, spec)); @@ -312,9 +310,11 @@ std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::_bui // whether the event that matches the resume token should be followed by an "invalidate" event. stages.push_back(DocumentSourceChangeStreamCheckInvalidate::create(expCtx, spec)); - // Always include a DSCSCheckResumability stage, both to verify that there is enough history to - // cover the change stream's starting point, and to swallow all events up to the resume point. - stages.push_back(DocumentSourceChangeStreamCheckResumability::create(expCtx, spec)); + // If the starting point is a high water mark, or if we will be splitting the pipeline for + // dispatch to the shards in a cluster, we must include a DSCSCheckResumability stage. + if (expCtx->inMongos || ResumeToken::isHighWaterMarkToken(resumeToken)) { + stages.push_back(DocumentSourceChangeStreamCheckResumability::create(expCtx, spec)); + } // If the pipeline is built on MongoS, we check for topology change events here. If a topology // change event is detected, this stage forwards the event directly to the executor via an @@ -324,6 +324,7 @@ std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::_bui stages.push_back(DocumentSourceChangeStreamCheckTopologyChange::create(expCtx)); } + // If 'fullDocumentBeforeChange' is not set to 'off', add the DSCSAddPreImage stage into the // pipeline. We place this stage here so that any $match stages which follow the $changeStream // pipeline may be able to skip ahead of the DSCSAddPreImage stage. This allows a whole-db or @@ -443,7 +444,7 @@ void DocumentSourceChangeStream::assertIsLegalSpecification( !spec.getResumeAfter() || !spec.getStartAfter()); auto resumeToken = (spec.getResumeAfter() || spec.getStartAfter()) - ? change_stream::resolveResumeTokenFromSpec(expCtx, spec) + ? resolveResumeTokenFromSpec(expCtx, spec) : boost::optional<ResumeTokenData>(); uassert(40674, diff --git a/src/mongo/db/pipeline/document_source_change_stream.h b/src/mongo/db/pipeline/document_source_change_stream.h index 5a76a5205e2..60013e64444 100644 --- a/src/mongo/db/pipeline/document_source_change_stream.h +++ b/src/mongo/db/pipeline/document_source_change_stream.h @@ -239,19 +239,13 @@ public: static constexpr StringData kDropIndexesOpType = "dropIndexes"_sd; static constexpr StringData kShardCollectionOpType = "shardCollection"_sd; static constexpr StringData kMigrateLastChunkFromShardOpType = "migrateLastChunkFromShard"_sd; - static constexpr StringData kRefineCollectionShardKeyOpType = "refineCollectionShardKey"_sd; - static constexpr StringData kReshardCollectionOpType = "reshardCollection"_sd; static constexpr StringData kModifyOpType = "modify"_sd; // Default regex for collections match which prohibits system collections. static constexpr StringData kRegexAllCollections = R"((?!(\$|system\.)))"_sd; - - // Regex matching all user collections plus collections exposed when 'showSystemEvents' is set. - // Does not match a collection named $ or a collection with 'system.' in the name. - // However, it will still match collection names starting with system.buckets or a collection - // exactly named system.js. + // Regex matching all regular collections plus certain system collections. static constexpr StringData kRegexAllCollectionsShowSystemEvents = - R"((?!(\$|system\.(?!(js$|buckets\.)))))"_sd; + R"((?!(\$|system\.(?!(js$)))))"_sd; static constexpr StringData kRegexAllDBs = R"(^(?!(admin|config|local)\.)[^.]+)"_sd; static constexpr StringData kRegexCmdColl = R"(\$cmd$)"_sd; @@ -289,6 +283,15 @@ public: static void checkValueType(Value v, StringData fieldName, BSONType expectedType); /** + * Extracts the resume token from the given spec. If a 'startAtOperationTime' is specified, + * returns the equivalent high-watermark token. This method should only ever be called on a spec + * where one of 'resumeAfter', 'startAfter', or 'startAtOperationTime' is populated. + */ + static ResumeTokenData resolveResumeTokenFromSpec( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const DocumentSourceChangeStreamSpec& spec); + + /** * For a change stream with no resume information supplied by the user, returns the clusterTime * at which the new stream should begin scanning the oplog. */ diff --git a/src/mongo/db/pipeline/document_source_change_stream.idl b/src/mongo/db/pipeline/document_source_change_stream.idl index ee5a11eef39..e1b8227a0fc 100644 --- a/src/mongo/db/pipeline/document_source_change_stream.idl +++ b/src/mongo/db/pipeline/document_source_change_stream.idl @@ -30,10 +30,22 @@ global: cpp_namespace: "mongo" + cpp_includes: + - "mongo/db/pipeline/resume_token.h" imports: - "mongo/idl/basic_types.idl" - - "mongo/db/pipeline/resume_token.idl" + +types: + # A resume token is an opaque document we return to the user that contains all the information + # needed to resume a stream where they left off. It also provides the ordering of streams + # from multiple shards. + resumeToken: + bson_serialization_type: object + description: An object representing a resume token for a change stream + cpp_type: ResumeToken + serializer: ResumeToken::toBSON + deserializer: ResumeToken::parse enums: FullDocumentBeforeChangeMode: @@ -74,7 +86,6 @@ structs: description: A document used to specify the $changeStream stage of an aggregation pipeline. The same spec is also used by the $_internalChangeStreamTransform stage. - query_shape_component: true fields: resumeAfter: cpp_name: resumeAfter @@ -83,7 +94,6 @@ structs: description: An object representing the point at which we should resume reporting changes from. Only one of resumeAfter, startAfter, and startAtOperationTime should be specified. - query_shape: custom startAfter: cpp_name: startAfter @@ -93,7 +103,6 @@ structs: changes from. This is allowed to be a token from an invalidating command. Only one of resumeAfter, startAfter, and startAtOperationTime should be specified. - query_shape: custom startAtOperationTime: cpp_name: startAtOperationTime @@ -102,7 +111,6 @@ structs: description: The operation time after which we should start reporting changes. Only one of resumeAfter, startAfter, and startAtOperationTime should be specified. - query_shape: literal fullDocument: cpp_name: fullDocument @@ -110,7 +118,6 @@ structs: default: kDefault description: A string '"updateLookup"' or '"default"', indicating whether or not we should return a full document or just changes for an update. - query_shape: parameter fullDocumentBeforeChange: cpp_name: fullDocumentBeforeChange @@ -125,7 +132,6 @@ structs: set to "required", then the "fullDocumentBeforeChange" field is always populated and an exception is thrown if the pre-image is not available. - query_shape: parameter allChangesForCluster: cpp_name: allChangesForCluster @@ -133,7 +139,6 @@ structs: description: A flag indicating whether the stream should report all changes that occur on the deployment, aside from those on internal databases or collections. - query_shape: literal showMigrationEvents: cpp_name: showMigrationEvents @@ -144,14 +149,12 @@ structs: deletes may appear that do not reflect actual deletions or insertions of data. Instead they reflect this data moving from one shard to another. - query_shape: literal showSystemEvents: cpp_name: showSystemEvents type: optionalBool description: A flag indicating whether the stream should report events on system collections. - query_shape: literal allowToRunOnConfigDB: cpp_name: allowToRunOnConfigDB @@ -160,20 +163,17 @@ structs: 'config' database, which is usually banned. This flag is used internally to allow mongoS to open a stream on 'config.shards', in order to monitor for the addition of new shards to the cluster. - query_shape: literal allowToRunOnSystemNS: cpp_name: allowToRunOnSystemNS type: optionalBool description: An internal flag indicating whether the change stream may be opened on a system collection. - query_shape: literal showExpandedEvents: type: optionalBool description: A flag indicating whether the change stream output should include the expanded events and parameters that may not be part of the stable API. - query_shape: literal showRawUpdateDescription: cpp_name: showRawUpdateDescription @@ -183,7 +183,6 @@ structs: raw update description from the oplog entry for the corresponding update command. When this flag is set to true, the standard "updateDescription" field in update events will be omitted. - query_shape: literal DocumentSourceChangeStreamOplogMatchSpec: strict: true @@ -203,51 +202,32 @@ structs: strict: true description: A document used to specify the $_internalChangeStreamCheckInvalidate stage of an aggregation pipeline. - query_shape_component: true fields: startAfterInvalidate: type: resumeToken optional: true - query_shape: custom - + DocumentSourceChangeStreamCheckResumabilitySpec: strict: true description: A document used to specify the $_internalChangeStreamCheckResumability stage of an aggregation pipeline. - query_shape_component: true fields: resumeToken: type: resumeToken optional: false - query_shape: custom DocumentSourceChangeStreamAddPreImageSpec: strict: true description: A document used to specify the $_internalChangeStreamAddPreImage stage of an aggregation pipeline. - query_shape_component: true fields: fullDocumentBeforeChange: type: FullDocumentBeforeChangeMode - query_shape: parameter DocumentSourceChangeStreamAddPostImageSpec: strict: true description: A document used to specify the $_internalChangeStreamAddPostImage stage of an aggregation pipeline. - query_shape_component: true fields: fullDocument: type: FullDocumentMode - query_shape: parameter - - DocumentSourceChangeStreamHandleTopologyChangeSpec: - strict: true - description: A document used to specify the $_internalChangeStreamHandleTopologyChange stage of - an aggregation pipeline. - query_shape_component: true - fields: - originalAggregateCommand: - type: object - optional: true - query_shape: literal diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp index 20445c71bd9..051922556a5 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp @@ -48,8 +48,6 @@ REGISTER_INTERNAL_DOCUMENT_SOURCE(_internalChangeStreamAddPostImage, DocumentSourceChangeStreamAddPostImage::createFromBson, true); -constexpr auto makePostImageNotFoundErrorMsg = - &DocumentSourceChangeStreamAddPreImage::makePreImageNotFoundErrorMsg; Value assertFieldHasType(const Document& fullDoc, StringData fieldName, BSONType expectedType) { auto val = fullDoc[fieldName]; @@ -99,11 +97,12 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamAddPostImage::doGetNext( const auto postImageDoc = (_fullDocumentMode == FullDocumentModeEnum::kUpdateLookup ? lookupLatestPostImage(output.peek()) : generatePostImage(output.peek())); - uassert(ErrorCodes::NoMatchingDocument, - str::stream() << "Change stream was configured to require a post-image for all update " - "events, but the post-image was not found for event: " - << makePostImageNotFoundErrorMsg(output.peek()), - postImageDoc || _fullDocumentMode != FullDocumentModeEnum::kRequired); + uassert( + ErrorCodes::NoMatchingDocument, + str::stream() << "Change stream was configured to require a post-image for all update, " + "delete and replace events, but the post-image was not found for event: " + << output.peek().toString(), + postImageDoc || _fullDocumentMode != FullDocumentModeEnum::kRequired); // Even if no post-image was found, we have to populate the 'fullDocument' field. output[kFullDocumentFieldName] = (postImageDoc ? Value(*postImageDoc) : Value(BSONNULL)); @@ -217,14 +216,15 @@ boost::optional<Document> DocumentSourceChangeStreamAddPostImage::lookupLatestPo pExpCtx, nss, *resumeTokenData.uuid, documentKey, std::move(readConcern)); } -Value DocumentSourceChangeStreamAddPostImage::serialize(const SerializationOptions& opts) const { - return opts.verbosity +Value DocumentSourceChangeStreamAddPostImage::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + return explain ? Value(Document{ {DocumentSourceChangeStream::kStageName, Document{{"stage"_sd, kStageName}, {kFullDocumentFieldName, FullDocumentMode_serializer(_fullDocumentMode)}}}}) - : Value(Document{ - {kStageName, - DocumentSourceChangeStreamAddPostImageSpec(_fullDocumentMode).toBSON(opts)}}); + : Value(Document{{kStageName, + DocumentSourceChangeStreamAddPostImageSpec(_fullDocumentMode).toBSON()}}); } + } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_post_image.h b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.h index 0c6f5c7c724..d92994064e2 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_post_image.h +++ b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.h @@ -114,7 +114,7 @@ public: return DepsTracker::State::SEE_NEXT; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; const char* getSourceName() const final { return kStageName.rawData(); diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_post_image_test.cpp b/src/mongo/db/pipeline/document_source_change_stream_add_post_image_test.cpp index c97a393d4d7..25ca17bbbfa 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_post_image_test.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_add_post_image_test.cpp @@ -106,9 +106,7 @@ TEST_F(DocumentSourceChangeStreamAddPostImageTest, ShouldSerializeAsExpectedForE Document{{"stage"_sd, DocumentSourceChangeStreamAddPostImage::kStageName}, {"fullDocument"_sd, "updateLookup"_sd}}}}); - ASSERT_VALUE_EQ(stage->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedOutput); + ASSERT_VALUE_EQ(stage->serialize({ExplainOptions::Verbosity::kQueryPlanner}), expectedOutput); } TEST_F(DocumentSourceChangeStreamAddPostImageTest, ShouldSerializeAsExpectedForDispatch) { diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp index be473668097..97b252c9829 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp @@ -97,7 +97,7 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamAddPreImage::doGetNext() str::stream() << "Change stream was configured to require a pre-image for all update, delete " "and replace events, but pre-image id was not available for event: " - << makePreImageNotFoundErrorMsg(input.getDocument()), + << input.getDocument().toString(), _fullDocumentBeforeChangeMode != FullDocumentBeforeChangeModeEnum::kRequired); return input; } @@ -111,7 +111,7 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamAddPreImage::doGetNext() ErrorCodes::NoMatchingDocument, str::stream() << "Change stream was configured to require a pre-image for all update, " "delete and replace events, but the pre-image was not found for event: " - << makePreImageNotFoundErrorMsg(input.getDocument()), + << input.getDocument().toString(), preImageDoc || _fullDocumentBeforeChangeMode != FullDocumentBeforeChangeModeEnum::kRequired); @@ -152,25 +152,17 @@ boost::optional<Document> DocumentSourceChangeStreamAddPreImage::lookupPreImage( return preImageField.getDocument().getOwned(); } -Value DocumentSourceChangeStreamAddPreImage::serialize(const SerializationOptions& opts) const { - return opts.verbosity +Value DocumentSourceChangeStreamAddPreImage::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + return explain ? Value(Document{ {DocumentSourceChangeStream::kStageName, Document{{"stage"_sd, "internalAddPreImage"_sd}, {"fullDocumentBeforeChange"_sd, FullDocumentBeforeChangeMode_serializer(_fullDocumentBeforeChangeMode)}}}}) - : Value(Document{{kStageName, - DocumentSourceChangeStreamAddPreImageSpec(_fullDocumentBeforeChangeMode) - .toBSON(opts)}}); -} - -std::string DocumentSourceChangeStreamAddPreImage::makePreImageNotFoundErrorMsg( - const Document& event) { - auto errMsgDoc = Document{{"operationType", event["operationType"]}, - {"ns", event["ns"]}, - {"clusterTime", event["clusterTime"]}, - {"txnNumber", event["txnNumber"]}}; - return errMsgDoc.toString(); + : Value(Document{ + {kStageName, + DocumentSourceChangeStreamAddPreImageSpec(_fullDocumentBeforeChangeMode).toBSON()}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h index 5e85dc41c7d..cc735fc135e 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h +++ b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h @@ -63,9 +63,6 @@ public: static boost::optional<Document> lookupPreImage(boost::intrusive_ptr<ExpressionContext> pExpCtx, const Document& preImageId); - // Removes the internal fields from the event and returns the string representation of it. - static std::string makePreImageNotFoundErrorMsg(const Document& event); - DocumentSourceChangeStreamAddPreImage(const boost::intrusive_ptr<ExpressionContext>& expCtx, FullDocumentBeforeChangeModeEnum mode) : DocumentSource(kStageName, expCtx), _fullDocumentBeforeChangeMode(mode) { @@ -108,7 +105,7 @@ public: return DepsTracker::State::SEE_NEXT; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; const char* getSourceName() const final { return kStageName.rawData(); diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp b/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp index 7c2f2b26045..8121fedb3e7 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp @@ -31,7 +31,6 @@ #include "mongo/platform/basic.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/change_stream_start_after_invalidate_info.h" #include "mongo/db/pipeline/document_source_change_stream.h" #include "mongo/db/pipeline/document_source_change_stream_check_invalidate.h" @@ -72,7 +71,7 @@ DocumentSourceChangeStreamCheckInvalidate::create( const DocumentSourceChangeStreamSpec& spec) { // If resuming from an "invalidate" using "startAfter", pass along the resume token data to // DSCSCheckInvalidate to signify that another invalidate should not be generated. - auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); return new DocumentSourceChangeStreamCheckInvalidate( expCtx, boost::make_optional(resumeToken.fromInvalidate, std::move(resumeToken))); } @@ -180,19 +179,18 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamCheckInvalidate::doGetNe return nextInput; } -Value DocumentSourceChangeStreamCheckInvalidate::serialize(const SerializationOptions& opts) const { - BSONObjBuilder builder; - if (opts.verbosity) { - BSONObjBuilder sub(builder.subobjStart(DocumentSourceChangeStream::kStageName)); - sub.append("stage"_sd, kStageName); - sub.done(); +Value DocumentSourceChangeStreamCheckInvalidate::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { + return Value(Document{{DocumentSourceChangeStream::kStageName, + Document{{"stage"_sd, "internalCheckInvalidate"_sd}}}}); } + DocumentSourceChangeStreamCheckInvalidateSpec spec; if (_startAfterInvalidate) { spec.setStartAfterInvalidate(ResumeToken(*_startAfterInvalidate)); } - builder.append(DocumentSourceChangeStreamCheckInvalidate::kStageName, spec.toBSON(opts)); - return Value(builder.obj()); + return Value(Document{{DocumentSourceChangeStreamCheckInvalidate::kStageName, spec.toBSON()}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.h b/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.h index c2e91111239..166604c1d10 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.h +++ b/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.h @@ -64,7 +64,7 @@ public: return boost::none; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; static boost::intrusive_ptr<DocumentSourceChangeStreamCheckInvalidate> createFromBson( BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& expCtx); diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp index 01159d3dc2b..3861b21693d 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp @@ -30,7 +30,6 @@ #include "mongo/platform/basic.h" #include "mongo/db/curop.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/document_source_change_stream_check_resumability.h" #include "mongo/db/query/query_feature_flags_gen.h" #include "mongo/db/repl/oplog_entry.h" @@ -48,15 +47,16 @@ REGISTER_INTERNAL_DOCUMENT_SOURCE(_internalChangeStreamCheckResumability, // Returns ResumeStatus::kFoundToken if the document retrieved from the resumed pipeline satisfies // the client's resume token, ResumeStatus::kCheckNextDoc if it is older than the client's token, -// and ResumeToken::kSurpassedToken if it is more recent than the client's resume token, indicating -// that we will never see the token. Return ResumeStatus::kNeedsSplit if we have found the event -// that produced the resume token, but it was split in the original stream. +// and ResumeToken::kSurpassedToken if it is more recent than the client's resume token (indicating +// that we will never see the token). DocumentSourceChangeStreamCheckResumability::ResumeStatus DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( - const Document& eventFromResumedStream, const ResumeTokenData& tokenDataFromClient) { + const intrusive_ptr<ExpressionContext>& expCtx, + const Document& documentFromResumedStream, + const ResumeTokenData& tokenDataFromClient) { // Parse the stream doc into comprehensible ResumeTokenData. auto tokenDataFromResumedStream = - ResumeToken::parse(eventFromResumedStream.metadata().getSortKey().getDocument()).getData(); + ResumeToken::parse(documentFromResumedStream["_id"].getDocument()).getData(); // We start the resume with a $gte query on the timestamp, so we never expect it to be lower // than our resume token's timestamp. @@ -97,25 +97,21 @@ DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( // clusterTime. If the stream UUID sorts after the client's, however, then the stream is not // resumable; we are past the point in the stream where the token should have appeared. if (tokenDataFromResumedStream.uuid != tokenDataFromClient.uuid) { + // If we are running on a replica set deployment, we don't ever expect to see identical time + // stamps and txnOpIndex but differing UUIDs, and we reject the resume attempt at once. + if (!expCtx->inMongos && !expCtx->needsMerge) { + return ResumeStatus::kSurpassedToken; + } + // Otherwise, return a ResumeStatus based on the sort-order of the client and stream UUIDs. return tokenDataFromResumedStream.uuid > tokenDataFromClient.uuid ? ResumeStatus::kSurpassedToken : ResumeStatus::kCheckNextDoc; } - // If the eventIdentifier matches exactly, then we have found the resume point. However, this - // event may have been split by the original stream; we must check the value of the resume - // token's fragmentNum field to determine the correct return status. + // If all the fields match exactly, then we have found the token. if (ValueComparator::kInstance.evaluate(tokenDataFromResumedStream.eventIdentifier == tokenDataFromClient.eventIdentifier)) { - if (tokenDataFromClient.fragmentNum && !tokenDataFromResumedStream.fragmentNum) { - return ResumeStatus::kNeedsSplit; - } - if (tokenDataFromResumedStream.fragmentNum == tokenDataFromClient.fragmentNum) { - return ResumeStatus::kFoundToken; - } - return tokenDataFromResumedStream.fragmentNum > tokenDataFromClient.fragmentNum - ? ResumeStatus::kSurpassedToken - : ResumeStatus::kCheckNextDoc; + return ResumeStatus::kFoundToken; } // At this point, we know that the tokens differ only by eventIdentifier. The status we return @@ -134,7 +130,7 @@ DocumentSourceChangeStreamCheckResumability::DocumentSourceChangeStreamCheckResu intrusive_ptr<DocumentSourceChangeStreamCheckResumability> DocumentSourceChangeStreamCheckResumability::create(const intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec) { - auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); return new DocumentSourceChangeStreamCheckResumability(expCtx, std::move(resumeToken)); } @@ -182,21 +178,15 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamCheckResumability::doGet // Determine whether the current event sorts before, equal to or after the resume token. _resumeStatus = DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( - nextInput.getDocument(), _tokenFromClient); + pExpCtx, nextInput.getDocument(), _tokenFromClient); switch (_resumeStatus) { case ResumeStatus::kCheckNextDoc: // If the result was kCheckNextDoc, we are resumable but must swallow this event. continue; - case ResumeStatus::kNeedsSplit: - // If the result was kNeedsSplit, we found a resume token which matches the client's - // except for the splitNum attribute. Allow this document to pass through so that - // the split stage can regenerate the original fragments and their resume tokens. - return nextInput; case ResumeStatus::kSurpassedToken: // In this case the resume token wasn't found; it may be on another shard. However, // since the oplog scan did not throw, we know that we are resumable. Fall through // into the following case and return the document. - return nextInput; case ResumeStatus::kFoundToken: // We found the actual token! Return the doc so DSEnsureResumeTokenPresent sees it. return nextInput; @@ -206,20 +196,16 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamCheckResumability::doGet } Value DocumentSourceChangeStreamCheckResumability::serialize( - const SerializationOptions& opts) const { - BSONObjBuilder builder; - if (opts.verbosity) { - BSONObjBuilder sub(builder.subobjStart(DocumentSourceChangeStream::kStageName)); - sub.append("stage"_sd, kStageName); - sub << "resumeToken"_sd << Value(ResumeToken(_tokenFromClient).toDocument(opts)); - sub.done(); - } else { - builder.append( - kStageName, - DocumentSourceChangeStreamCheckResumabilitySpec(ResumeToken(_tokenFromClient)) - .toBSON(opts)); - } - return Value(builder.obj()); + boost::optional<ExplainOptions::Verbosity> explain) const { + return explain + ? Value(DOC(DocumentSourceChangeStream::kStageName + << DOC("stage" + << "internalCheckResumability"_sd + << "resumeToken" << ResumeToken(_tokenFromClient).toDocument()))) + : Value(Document{ + {DocumentSourceChangeStreamCheckResumability::kStageName, + DocumentSourceChangeStreamCheckResumabilitySpec(ResumeToken(_tokenFromClient)) + .toBSON()}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h index bf67c67615b..a290d59cacd 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h +++ b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h @@ -68,8 +68,7 @@ public: enum class ResumeStatus { kFoundToken, // The stream produced a document satisfying the client resume token. kSurpassedToken, // The stream's latest document is more recent than the resume token. - kCheckNextDoc, // The next document produced by the stream may contain the resume token. - kNeedsSplit // We found a candidate resume token but the event must be split. + kCheckNextDoc // The next document produced by the stream may contain the resume token. }; const char* getSourceName() const override; @@ -90,7 +89,7 @@ public: return boost::none; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const override; static boost::intrusive_ptr<DocumentSourceChangeStreamCheckResumability> createFromBson( BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& expCtx); @@ -99,8 +98,10 @@ public: const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec); - static ResumeStatus compareAgainstClientResumeToken(const Document& eventFromResumedStream, - const ResumeTokenData& tokenDataFromClient); + static ResumeStatus compareAgainstClientResumeToken( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const Document& documentFromResumedStream, + const ResumeTokenData& tokenDataFromClient); protected: /** diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.cpp b/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.cpp index 9c57ea5253d..26cdcb77181 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.cpp @@ -87,8 +87,8 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamCheckTopologyChange::doG } Value DocumentSourceChangeStreamCheckTopologyChange::serialize( - const SerializationOptions& opts) const { - if (opts.verbosity) { + boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { return Value(DOC(DocumentSourceChangeStream::kStageName << DOC("stage" << "internalCheckTopologyChange"_sd))); diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.h b/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.h index dee87629560..7629e5d8063 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.h +++ b/src/mongo/db/pipeline/document_source_change_stream_check_topology_change.h @@ -67,7 +67,7 @@ public: return boost::none; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; private: DocumentSourceChangeStreamCheckTopologyChange( diff --git a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp index 0ae0ba95d7e..15fcb30d58f 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp @@ -31,7 +31,6 @@ #include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/change_stream_start_after_invalidate_info.h" #include "mongo/db/query/query_feature_flags_gen.h" @@ -46,7 +45,7 @@ boost::intrusive_ptr<DocumentSourceChangeStreamEnsureResumeTokenPresent> DocumentSourceChangeStreamEnsureResumeTokenPresent::create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec) { - auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); tassert(5666902, "Expected non-high-water-mark resume token", !ResumeToken::isHighWaterMarkToken(resumeToken)); @@ -72,14 +71,13 @@ StageConstraints DocumentSourceChangeStreamEnsureResumeTokenPresent::constraints UnionRequirement::kNotAllowed, ChangeStreamRequirement::kChangeStreamStage}; - // The '$match', '$redact', and 'DocumentSourceSingleDocumentTransformation' stages can swap - // with this stage, allowing filtering and reshaping to occur earlier in the pipeline. For - // sharded cluster pipelines, swaps can allow $match, $redact and - // 'DocumentSourceSingleDocumentTransformation' stages to execute on the shards, providing - // inter-node parallelism and potentially reducing the amount of data sent form each shard to - // the mongoS. + // The '$match' and 'DocumentSourceSingleDocumentTransformation' stages can swap with this + // stage, allowing filtering and reshaping to occur earlier in the pipeline. For sharded cluster + // pipelines, swaps can allow $match and 'DocumentSourceSingleDocumentTransformation' stages to + // execute on the shards, providing inter-node parallelism and potentially reducing the amount + // of data sent form each shard to the mongoS. constraints.canSwapWithMatch = true; - constraints.canSwapWithSingleDocTransformOrRedact = true; + constraints.canSwapWithSingleDocTransform = true; return constraints; } @@ -139,10 +137,9 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamEnsureResumeTokenPresent const DocumentSource::GetNextResult nextInput = Document::fromBsonWithMetaData(extraInfo->getStartAfterInvalidateEvent()); - _resumeStatus = DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( - nextInput.getDocument(), _tokenFromClient); + pExpCtx, nextInput.getDocument(), _tokenFromClient); // This exception should always contain the client-provided resume token. tassert(5779201, @@ -154,19 +151,15 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamEnsureResumeTokenPresent } Value DocumentSourceChangeStreamEnsureResumeTokenPresent::serialize( - const SerializationOptions& opts) const { - BSONObjBuilder builder; - if (opts.verbosity) { - BSONObjBuilder sub(builder.subobjStart(DocumentSourceChangeStream::kStageName)); - sub.append("stage"_sd, kStageName); - sub << "resumeToken"_sd << Value(ResumeToken(_tokenFromClient).toDocument(opts)); - sub.done(); - } else { - BSONObjBuilder sub(builder.subobjStart(kStageName)); - sub << "resumeToken"_sd << Value(ResumeToken(_tokenFromClient).toDocument(opts)); - sub.done(); + boost::optional<ExplainOptions::Verbosity> explain) const { + // We only serialize this stage in the context of explain. + if (explain) { + return Value(DOC(DocumentSourceChangeStream::kStageName + << DOC("stage" + << "internalEnsureResumeTokenPresent"_sd + << "resumeToken" << ResumeToken(_tokenFromClient).toDocument()))); } - return Value(builder.obj()); + MONGO_UNREACHABLE_TASSERT(5467611); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h index 8fc70392337..3f24ed446ea 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h +++ b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h @@ -39,8 +39,7 @@ namespace mongo { class DocumentSourceChangeStreamEnsureResumeTokenPresent final : public DocumentSourceChangeStreamCheckResumability { public: - static constexpr StringData kStageName = - change_stream_constants::stage_names::kEnsureResumeTokenPresent; + static constexpr StringData kStageName = "$_internalChangeStreamEnsureResumeTokenPresent"_sd; const char* getSourceName() const final; @@ -48,14 +47,14 @@ public: GetModPathsReturn getModifiedPaths() const final { // This stage neither modifies nor renames any field. - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } static boost::intrusive_ptr<DocumentSourceChangeStreamEnsureResumeTokenPresent> create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec); - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; private: /** diff --git a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp index 7acb0db898c..0f32b20aa47 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp @@ -46,11 +46,6 @@ namespace mongo { namespace { -REGISTER_INTERNAL_DOCUMENT_SOURCE(_internalChangeStreamHandleTopologyChange, - LiteParsedDocumentSourceChangeStreamInternal::parse, - DocumentSourceChangeStreamHandleTopologyChange::createFromBson, - true); - // Failpoint to throw an exception when the 'kNewShardDetected' event is observed. MONGO_FAIL_POINT_DEFINE(throwChangeStreamTopologyChangeExceptionToClient); @@ -115,15 +110,6 @@ bool isShardConfigEvent(const Document& eventDoc) { } // namespace boost::intrusive_ptr<DocumentSourceChangeStreamHandleTopologyChange> -DocumentSourceChangeStreamHandleTopologyChange::createFromBson( - const BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - uassert(8131300, - str::stream() << "the '" << kStageName << "' spec must be an empty object", - elem.type() == Object && elem.Obj().isEmpty()); - return new DocumentSourceChangeStreamHandleTopologyChange(expCtx); -} - -boost::intrusive_ptr<DocumentSourceChangeStreamHandleTopologyChange> DocumentSourceChangeStreamHandleTopologyChange::create( const boost::intrusive_ptr<ExpressionContext>& expCtx) { return new DocumentSourceChangeStreamHandleTopologyChange(expCtx); @@ -145,11 +131,11 @@ StageConstraints DocumentSourceChangeStreamHandleTopologyChange::constraints( UnionRequirement::kNotAllowed, ChangeStreamRequirement::kChangeStreamStage}; - // Can be swapped with the '$match', '$redact', and 'DocumentSourceSingleDocumentTransformation' - // stages and ensures that they get pushed down to the shards, as this stage bisects the change - // streams pipeline. + // Can be swapped with the '$match' and 'DocumentSourceSingleDocumentTransformation' stages and + // ensures that they get pushed down to the shards, as this stage bisects the change streams + // pipeline. constraints.canSwapWithMatch = true; - constraints.canSwapWithSingleDocTransformOrRedact = true; + constraints.canSwapWithSingleDocTransform = true; return constraints; } @@ -242,8 +228,7 @@ BSONObj DocumentSourceChangeStreamHandleTopologyChange::createUpdatedCommandForN Document{shardCommand}, splitPipelines, boost::none, /* exhangeSpec */ - true /* needsMerge */, - boost::none /* explain */); + true /* needsMerge */); } BSONObj DocumentSourceChangeStreamHandleTopologyChange::replaceResumeTokenInCommand( @@ -272,8 +257,8 @@ BSONObj DocumentSourceChangeStreamHandleTopologyChange::replaceResumeTokenInComm } Value DocumentSourceChangeStreamHandleTopologyChange::serialize( - const SerializationOptions& opts) const { - if (opts.verbosity) { + boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { return Value(DOC(DocumentSourceChangeStream::kStageName << DOC("stage" << "internalHandleTopologyChange"_sd))); diff --git a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h index c22fe3846ad..c5d5a16fd93 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h +++ b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h @@ -48,11 +48,8 @@ namespace mongo { */ class DocumentSourceChangeStreamHandleTopologyChange final : public DocumentSource { public: - static constexpr StringData kStageName = - change_stream_constants::stage_names::kHandleTopologyChange; + static constexpr StringData kStageName = "$_internalChangeStreamHandleTopologyChange"_sd; - static boost::intrusive_ptr<DocumentSourceChangeStreamHandleTopologyChange> createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); /** * Creates a new stage which will establish a new cursor and add it to the cursors being merged * by 'mergeCursorsStage' whenever a new shard is detected by a change stream. @@ -64,13 +61,13 @@ public: return kStageName.rawData(); } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; StageConstraints constraints(Pipeline::SplitState) const final; GetModPathsReturn getModifiedPaths() const final { // This stage neither modifies nor renames any field. - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() final { diff --git a/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp b/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp index aab87a8433c..c11cb0a1aeb 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp @@ -31,7 +31,6 @@ #include "mongo/bson/bsonmisc.h" #include "mongo/db/pipeline/change_stream_filter_helpers.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/document_source_change_stream_unwind_transaction.h" namespace mongo { @@ -105,7 +104,7 @@ DocumentSourceChangeStreamOplogMatch::DocumentSourceChangeStreamOplogMatch( boost::intrusive_ptr<DocumentSourceChangeStreamOplogMatch> DocumentSourceChangeStreamOplogMatch::create(const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec) { - auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); return make_intrusive<DocumentSourceChangeStreamOplogMatch>(resumeToken.clusterTime, expCtx); } @@ -204,26 +203,16 @@ Pipeline::SourceContainer::iterator DocumentSourceChangeStreamOplogMatch::doOpti return nextChangeStreamStageItr; } -Value DocumentSourceChangeStreamOplogMatch::serialize(const SerializationOptions& opts) const { - BSONObjBuilder builder; - if (opts.verbosity) { - BSONObjBuilder sub(builder.subobjStart(DocumentSourceChangeStream::kStageName)); - sub.append("stage"_sd, kStageName); - sub.append(DocumentSourceChangeStreamOplogMatchSpec::kFilterFieldName, - getMatchExpression()->serialize(opts)); - sub.done(); - } else { - BSONObjBuilder sub(builder.subobjStart(kStageName)); - if (opts.literalPolicy != LiteralSerializationPolicy::kUnchanged || - opts.transformIdentifiers) { - sub.append(DocumentSourceChangeStreamOplogMatchSpec::kFilterFieldName, - getMatchExpression()->serialize(opts)); - } else { - DocumentSourceChangeStreamOplogMatchSpec(_predicate).serialize(&sub); - } - sub.done(); +Value DocumentSourceChangeStreamOplogMatch::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { + return Value( + Document{{DocumentSourceChangeStream::kStageName, + Document{{"stage"_sd, "internalOplogMatch"_sd}, {"filter"_sd, _predicate}}}}); } - return Value(builder.obj()); + + DocumentSourceChangeStreamOplogMatchSpec spec(_predicate); + return Value(Document{{DocumentSourceChangeStreamOplogMatch::kStageName, spec.toBSON()}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_oplog_match.h b/src/mongo/db/pipeline/document_source_change_stream_oplog_match.h index f6e8772ba8c..d6278a400c7 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_oplog_match.h +++ b/src/mongo/db/pipeline/document_source_change_stream_oplog_match.h @@ -51,7 +51,7 @@ public: } boost::intrusive_ptr<DocumentSource> clone( - const boost::intrusive_ptr<ExpressionContext>& newExpCtx) const final { + const boost::intrusive_ptr<ExpressionContext>& newExpCtx = nullptr) const final { return new DocumentSourceChangeStreamOplogMatch(*this, newExpCtx); } @@ -74,7 +74,7 @@ public: StageConstraints constraints(Pipeline::SplitState pipeState) const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; protected: Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, diff --git a/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp deleted file mode 100644 index 1cf2ef0ed58..00000000000 --- a/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp +++ /dev/null @@ -1,231 +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 "mongo/db/pipeline/document_source_change_stream_split_large_event.h" - -#include "mongo/db/pipeline/change_stream_helpers.h" -#include "mongo/db/pipeline/change_stream_split_event_helpers.h" -#include "mongo/db/pipeline/document_source_change_stream_check_resumability.h" - -namespace mongo { -namespace { -Counter64 changeStreamsLargeEventsSplitCounter; -ServerStatusMetricField<Counter64> dchangeStreamsLargeEventsSplitCounter( - "changeStreams.largeEventsSplit", &changeStreamsLargeEventsSplitCounter); -} // namespace -REGISTER_DOCUMENT_SOURCE(changeStreamSplitLargeEvent, - DocumentSourceChangeStreamSplitLargeEvent::LiteParsed::parse, - DocumentSourceChangeStreamSplitLargeEvent::createFromBson, - AllowedWithApiStrict::kNeverInVersion1); - -boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> -DocumentSourceChangeStreamSplitLargeEvent::create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const DocumentSourceChangeStreamSpec& spec) { - // If resuming from a split event, pass along the resume token data to DSCSSplitEvent so that it - // can swallow fragments that precede the actual resume point. - auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); - auto resumeAfterSplit = - resumeToken.fragmentNum ? std::move(resumeToken) : boost::optional<ResumeTokenData>{}; - return new DocumentSourceChangeStreamSplitLargeEvent(expCtx, std::move(resumeAfterSplit)); -} - -boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> -DocumentSourceChangeStreamSplitLargeEvent::createFromBson( - BSONElement rawSpec, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - // We expect an empty object spec for this stage. - uassert(7182800, - "$changeStreamSplitLargeEvent spec should be an empty object", - rawSpec.type() == BSONType::Object && rawSpec.Obj().isEmpty()); - - // If there is no change stream spec set on the expression context, then this cannot be a change - // stream pipeline. Pipeline validation will catch this issue later during parsing. - if (!expCtx->changeStreamSpec) { - return new DocumentSourceChangeStreamSplitLargeEvent(expCtx, boost::none); - } - return create(expCtx, *expCtx->changeStreamSpec); -} - -DocumentSourceChangeStreamSplitLargeEvent::DocumentSourceChangeStreamSplitLargeEvent( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<ResumeTokenData> resumeAfterSplit) - : DocumentSource(getSourceName(), expCtx), _resumeAfterSplit(std::move(resumeAfterSplit)) { - tassert(7182801, - "Expected a split event resume token, but found a non-split token", - !_resumeAfterSplit || _resumeAfterSplit->fragmentNum); -} - -Value DocumentSourceChangeStreamSplitLargeEvent::serialize(const SerializationOptions& opts) const { - return Value(Document{{DocumentSourceChangeStreamSplitLargeEvent::kStageName, Document{}}}); -} - -StageConstraints DocumentSourceChangeStreamSplitLargeEvent::constraints( - Pipeline::SplitState pipeState) const { - StageConstraints constraints{StreamType::kStreaming, - PositionRequirement::kCustom, - HostTypeRequirement::kAnyShard, - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kNotAllowed, - TransactionRequirement::kNotAllowed, - LookupRequirement::kNotAllowed, - UnionRequirement::kNotAllowed, - ChangeStreamRequirement::kRequiresChangeStream}; - - // The user cannot specify multiple split stages in the pipeline. - constraints.canAppearOnlyOnceInPipeline = true; - return constraints; -} - -DocumentSource::GetModPathsReturn DocumentSourceChangeStreamSplitLargeEvent::getModifiedPaths() - const { - // This stage may modify the entire document. - return {GetModPathsReturn::Type::kAllPaths, {}, {}}; -} - -DocumentSource::GetNextResult DocumentSourceChangeStreamSplitLargeEvent::doGetNext() { - // If we've already queued up some fragments, return them. - if (!_splitEventQueue.empty()) { - return _popFromQueue(); - } - - auto input = pSource->getNext(); - - // If the next result is EOF return, it as-is. - if (!input.isAdvanced()) { - return input; - } - - // Process the event to see if it is within the size limit. We have to serialize the document to - // perform this check, but the helper will also produce a new 'Document' which - if it is small - // enough to be returned - will not need to be re-serialized by the plan executor. - auto [eventDoc, eventBsonSize] = change_stream_split_event::processChangeEventBeforeSplit( - input.getDocument(), this->pExpCtx->needsMerge || this->pExpCtx->forPerShardCursor); - - // Make sure to leave some space for the postBatchResumeToken in the cursor response object. - size_t tokenSize = eventDoc.metadata().getSortKey().getDocument().toBson().objsize(); - - // If we are resuming from a split event, check whether this is it. If so, extract the fragment - // number from which we are resuming. Otherwise, we have already scanned past the resume point, - // which implies that it may be on another shard. Continue to split this event without skipping. - size_t skipFragments = _handleResumeAfterSplit(eventDoc, eventBsonSize + tokenSize); - - // Before proceeding, check whether the event is small enough to be returned as-is. - if (eventBsonSize + tokenSize <= kBSONObjMaxChangeEventSize) { - return std::move(eventDoc); - } - - // Split the event into N appropriately-sized fragments. - _splitEventQueue = change_stream_split_event::splitChangeEvent( - eventDoc, kBSONObjMaxChangeEventSize - tokenSize, skipFragments); - - // If the user is resuming from a split event but supplied a pipeline which produced a different - // split, we cannot reproduce the split point. Check if we're about to swallow all fragments. - uassert(ErrorCodes::ChangeStreamFatalError, - "Attempted to resume from a split event, but the resumed stream produced a different " - "split. Ensure that the pipeline used to resume is the same as the original", - !(skipFragments > 0 && _splitEventQueue.empty())); - tassert(7182804, - "Unexpected empty fragment queue after splitting a change stream event", - !_splitEventQueue.empty()); - - // Increment the ServerStatus counter to indicate that we have split a change event. - changeStreamsLargeEventsSplitCounter.increment(); - - // Return the first element from the queue of fragments. - return _popFromQueue(); -} - -Document DocumentSourceChangeStreamSplitLargeEvent::_popFromQueue() { - auto nextFragment = std::move(_splitEventQueue.front()); - _splitEventQueue.pop(); - return nextFragment; -} - -size_t DocumentSourceChangeStreamSplitLargeEvent::_handleResumeAfterSplit(const Document& eventDoc, - size_t eventBsonSize) { - if (!_resumeAfterSplit) { - return 0; - } - using DSCSCR = DocumentSourceChangeStreamCheckResumability; - auto resumeStatus = DSCSCR::compareAgainstClientResumeToken(eventDoc, *_resumeAfterSplit); - tassert(7182805, - "Observed unexpected event before resume point", - resumeStatus != DSCSCR::ResumeStatus::kCheckNextDoc); - uassert(ErrorCodes::ChangeStreamFatalError, - "Attempted to resume from a split event fragment, but the event in the resumed " - "stream was not large enough to be split", - resumeStatus != DSCSCR::ResumeStatus::kNeedsSplit || - eventBsonSize > kBSONObjMaxChangeEventSize); - auto fragmentNum = - (resumeStatus == DSCSCR::ResumeStatus::kNeedsSplit ? *_resumeAfterSplit->fragmentNum : 0); - _resumeAfterSplit.reset(); - return fragmentNum; -} - -Pipeline::SourceContainer::iterator DocumentSourceChangeStreamSplitLargeEvent::doOptimizeAt( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - // Helper to determine whether the iterator has reached its final position in the pipeline. - // Checks whether $changeStreamSplitLargeEvent should move ahead of the given stage. - auto shouldMoveAheadOf = [](const auto& stagePtr) { - return change_stream_constants::kChangeStreamRouterPipelineStages.contains( - stagePtr->getSourceName()); - }; - - // Find the point in the pipeline that the stage should move to. - for (auto it = itr; it != container->begin() && shouldMoveAheadOf(*std::prev(it));) { - // Swap 'it' with the previous stage. - container->splice(std::prev(it), *container, it); - } - - // Return an iterator pointing to the next stage to be optimized. - return std::next(itr); -} - -void DocumentSourceChangeStreamSplitLargeEvent::validatePipelinePosition( - bool alreadyOptimized, - Pipeline::SourceContainer::const_iterator pos, - const Pipeline::SourceContainer& container) const { - - // The $changeStreamSplitLargeEvent stage must be the final stage in the pipeline before - // optimization. - uassert(7182802, - str::stream() << getSourceName() << " must be the last stage in the pipeline", - alreadyOptimized || pos == std::prev(container.cend())); - - // The $changeStreamSplitLargeEvent stage must not be after 'kStagesToMoveAheadOf' stages after - // optimization. - uassert(7182803, - str::stream() << getSourceName() - << " is at the wrong position in the pipeline after optimization", - !alreadyOptimized || std::none_of(container.begin(), pos, [](const auto& stage) { - return change_stream_constants::kChangeStreamRouterPipelineStages.contains( - stage->getSourceName()); - })); -}; -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h deleted file mode 100644 index 44ed8b41955..00000000000 --- a/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h +++ /dev/null @@ -1,120 +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. - */ - -#pragma once - -#include <queue> - -#include "mongo/db/pipeline/document_source.h" - -namespace mongo { - -class DocumentSourceChangeStreamSplitLargeEvent : public DocumentSource { -public: - class LiteParsed : public LiteParsedDocumentSource { - public: - static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, - const BSONElement& spec) { - uassert(7182899, - str::stream() - << "$changeStreamSplitLargeEvent must take a nested object but found: " - << spec, - spec.type() == BSONType::Object); - return std::make_unique<LiteParsed>(spec.fieldName()); - } - - explicit LiteParsed(std::string parseTimeName) - : LiteParsedDocumentSource(std::move(parseTimeName)) {} - - bool isChangeStreamSplitLargeEvent() const final { - return true; - } - - stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { - return {}; - } - - PrivilegeVector requiredPrivileges(bool isMongos, - bool bypassDocumentValidation) const final { - return {}; - } - }; - - static constexpr StringData kStageName = "$changeStreamSplitLargeEvent"_sd; - static constexpr size_t kBSONObjMaxChangeEventSize = BSONObjMaxInternalSize - (8 * 1024); - - static boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const DocumentSourceChangeStreamSpec& spec); - - static boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); - - DocumentSource::GetModPathsReturn getModifiedPaths() const final; - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - - StageConstraints constraints(Pipeline::SplitState pipeState) const final; - - void validatePipelinePosition(bool alreadyOptimized, - Pipeline::SourceContainer::const_iterator pos, - const Pipeline::SourceContainer& container) const final; - - boost::optional<DistributedPlanLogic> distributedPlanLogic() final { - return boost::none; - } - - const char* getSourceName() const final { - return kStageName.rawData(); - } - -protected: - Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container) final; - - DocumentSource::GetNextResult doGetNext() final; - -private: - // This constructor is private, callers should use the 'create()' method above. - DocumentSourceChangeStreamSplitLargeEvent(const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<ResumeTokenData> resumeAfterSplit); - - Document _popFromQueue(); - - /** - * In case of resume after split, check whether 'eventDoc' is the split event. If so, extract - * and return the resume token's fragment number. Otherwise, return zero. - */ - size_t _handleResumeAfterSplit(const Document& eventDoc, size_t eventBsonSize); - - boost::optional<ResumeTokenData> _resumeAfterSplit; - std::queue<Document> _splitEventQueue; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_test.cpp b/src/mongo/db/pipeline/document_source_change_stream_test.cpp index ad37c4a8b2e..5d11196a08b 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_test.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_test.cpp @@ -27,7 +27,6 @@ * it in the license file. */ -#include "mongo/bson/bsontypes.h" #include "mongo/platform/basic.h" #include <boost/intrusive_ptr.hpp> @@ -53,11 +52,8 @@ #include "mongo/db/pipeline/document_source_change_stream_add_pre_image.h" #include "mongo/db/pipeline/document_source_change_stream_check_invalidate.h" #include "mongo/db/pipeline/document_source_change_stream_check_resumability.h" -#include "mongo/db/pipeline/document_source_change_stream_check_topology_change.h" #include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" -#include "mongo/db/pipeline/document_source_change_stream_handle_topology_change.h" #include "mongo/db/pipeline/document_source_change_stream_oplog_match.h" -#include "mongo/db/pipeline/document_source_change_stream_split_large_event.h" #include "mongo/db/pipeline/document_source_change_stream_transform.h" #include "mongo/db/pipeline/document_source_change_stream_unwind_transaction.h" #include "mongo/db/pipeline/document_source_limit.h" @@ -92,9 +88,6 @@ using V = Value; using DSChangeStream = DocumentSourceChangeStream; -// Deterministic values used for testing -const UUID testConstUuid = UUID::parse("6948DF80-14BD-4E04-8842-7668D9C001F5").getValue(); - class ChangeStreamStageTestNoSetup : public AggregationContextFixture { public: ChangeStreamStageTestNoSetup() : ChangeStreamStageTestNoSetup(nss) {} @@ -513,12 +506,9 @@ TEST_F(ChangeStreamStageTest, ShouldRejectBothStartAtOperationTimeAndResumeAfter // Need to put the collection in the collection catalog so the resume token is valid. std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(expCtx->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(expCtx->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(expCtx->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(expCtx->opCtx, testUuid(), std::move(collection)); + }); ASSERT_THROWS_CODE( DSChangeStream::createFromBson( @@ -538,12 +528,9 @@ TEST_F(ChangeStreamStageTest, ShouldRejectBothStartAfterAndResumeAfterOptions) { // Need to put the collection in the collection catalog so the resume token is validcollection std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(opCtx, testUuid(), std::move(collection)); + }); ASSERT_THROWS_CODE( DSChangeStream::createFromBson( @@ -564,12 +551,9 @@ TEST_F(ChangeStreamStageTest, ShouldRejectBothStartAtOperationTimeAndStartAfterO // Need to put the collection in the collection catalog so the resume token is valid. std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(opCtx, testUuid(), std::move(collection)); + }); ASSERT_THROWS_CODE( DSChangeStream::createFromBson( @@ -589,12 +573,9 @@ TEST_F(ChangeStreamStageTest, ShouldRejectResumeAfterWithResumeTokenMissingUUID) // Need to put the collection in the collection catalog so the resume token is valid. std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(opCtx, testUuid(), std::move(collection)); + }); ASSERT_THROWS_CODE( DSChangeStream::createFromBson( @@ -1546,7 +1527,6 @@ TEST_F(ChangeStreamStageTest, CommitCommandReturnsOperationsFromPreparedTransact nss.getCommandNS(), // namespace boost::none, // uuid boost::none, // fromMigrate - boost::none, // checkExistenceForDiffInsert repl::OplogEntry::kOplogVersion, // version BSON("commitTransaction" << 1), // o boost::none, // o2 @@ -1976,7 +1956,6 @@ TEST_F(ChangeStreamStageTest, PreparedTransactionWithMultipleOplogEntries) { nss.getCommandNS(), // namespace boost::none, // uuid boost::none, // fromMigrate - boost::none, // checkExistenceForDiffInsert repl::OplogEntry::kOplogVersion, // version BSON("commitTransaction" << 1), // o boost::none, // o2 @@ -2121,7 +2100,6 @@ TEST_F(ChangeStreamStageTest, PreparedTransactionEndingWithEmptyApplyOps) { nss.getCommandNS(), // namespace boost::none, // uuid boost::none, // fromMigrate - boost::none, // checkExistenceForDiffInsert repl::OplogEntry::kOplogVersion, // version BSON("commitTransaction" << 1), // o boost::none, // o2 @@ -2604,13 +2582,11 @@ TEST_F(ChangeStreamStageTest, DocumentKeyShouldIncludeShardKeyFromResumeTokenWhe const auto opTime = repl::OpTime(ts, term); const auto uuid = testUuid(); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(uuid, nss); - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); + BSONObj docKey = BSON("_id" << 1 << "shardKey" << 2); auto resumeToken = makeResumeToken(ts, uuid, docKey); @@ -2649,13 +2625,11 @@ TEST_F(ChangeStreamStageTest, DocumentKeyShouldPrioritizeO2FieldOverDocumentKeyC const auto opTime = repl::OpTime(ts, term); const auto uuid = testUuid(); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(uuid, nss); - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); + BSONObj docKey = BSON("_id" << 1); auto resumeToken = makeResumeToken(ts, uuid, docKey); @@ -2696,12 +2670,9 @@ TEST_F(ChangeStreamStageTest, DocumentKeyShouldNotIncludeShardKeyFieldsIfNotPres const auto uuid = testUuid(); std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); BSONObj docKey = BSON("_id" << 1 << "shardKey" << 2); auto resumeToken = makeResumeToken(ts, uuid, docKey); @@ -2735,14 +2706,12 @@ TEST_F(ChangeStreamStageTest, DocumentKeyShouldNotIncludeShardKeyFieldsIfNotPres TEST_F(ChangeStreamStageTest, ResumeAfterFailsIfResumeTokenDoesNotContainUUID) { const Timestamp ts(3, 45); + const auto uuid = testUuid(); std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); // Create a resume token from only the timestamp. auto resumeToken = makeResumeToken(ts); @@ -2797,12 +2766,9 @@ TEST_F(ChangeStreamStageTest, ResumeAfterWithTokenFromInvalidateShouldFail) { // Need to put the collection in the collection catalog so the resume token is valid. std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(expCtx->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(expCtx->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, testUuid(), std::move(collection)); + }); const auto resumeTokenInvalidate = makeResumeToken(kDefaultTs, @@ -3544,13 +3510,10 @@ TEST_F(ChangeStreamStageDBTest, const auto opTime = repl::OpTime(ts, term); const auto uuid = testUuid(); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(uuid, nss); - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); BSONObj docKey = BSON("_id" << 1 << "shardKey" << 2); auto resumeToken = makeResumeToken(ts, uuid, docKey); @@ -3585,13 +3548,10 @@ TEST_F(ChangeStreamStageDBTest, DocumentKeyShouldPrioritizeO2FieldOverDocumentKe const auto opTime = repl::OpTime(ts, term); const auto uuid = testUuid(); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(uuid, nss); - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); BSONObj docKey = BSON("_id" << 1); auto resumeToken = makeResumeToken(ts, uuid, docKey); @@ -3628,12 +3588,9 @@ TEST_F(ChangeStreamStageDBTest, DocumentKeyShouldNotIncludeShardKeyFieldsIfNotPr const auto uuid = testUuid(); std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); BSONObj docKey = BSON("_id" << 1 << "shardKey" << 2); auto resumeToken = makeResumeToken(ts, uuid, docKey); @@ -3668,12 +3625,9 @@ TEST_F(ChangeStreamStageDBTest, DocumentKeyShouldNotIncludeShardKeyIfResumeToken const auto uuid = testUuid(); std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); // Create a resume token from only the timestamp. auto resumeToken = makeResumeToken(ts); @@ -3707,12 +3661,9 @@ TEST_F(ChangeStreamStageDBTest, ResumeAfterWithTokenFromInvalidateShouldFail) { // Need to put the collection in the collection catalog so the resume token is valid. std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - CollectionCatalog::write(expCtx->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + CollectionCatalog::write(expCtx->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, testUuid(), std::move(collection)); + }); const auto resumeTokenInvalidate = makeResumeToken(kDefaultTs, @@ -3732,13 +3683,10 @@ TEST_F(ChangeStreamStageDBTest, ResumeAfterWithTokenFromInvalidateShouldFail) { TEST_F(ChangeStreamStageDBTest, ResumeAfterWithTokenFromDropDatabase) { const auto uuid = testUuid(); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(uuid, nss); - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); // Create a resume token from only the timestamp, similar to a 'dropDatabase' entry. auto resumeToken = makeResumeToken( @@ -3765,13 +3713,10 @@ TEST_F(ChangeStreamStageDBTest, ResumeAfterWithTokenFromDropDatabase) { TEST_F(ChangeStreamStageDBTest, StartAfterSucceedsEvenIfResumeTokenDoesNotContainUUID) { const auto uuid = testUuid(); - { - Lock::GlobalLock lk{getExpCtx()->opCtx, MODE_IX}; - std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(uuid, nss); - CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { - catalog.registerCollection(getExpCtx()->opCtx, std::move(collection)); - }); - } + std::shared_ptr<Collection> collection = std::make_shared<CollectionMock>(nss); + CollectionCatalog::write(getExpCtx()->opCtx, [&](CollectionCatalog& catalog) { + catalog.registerCollection(getExpCtx()->opCtx, uuid, std::move(collection)); + }); // Create a resume token from only the timestamp, similar to a 'dropDatabase' entry. auto resumeToken = makeResumeToken(kDefaultTs); @@ -4511,17 +4456,15 @@ TEST_F(MultiTokenFormatVersionTest, CanResumeFromV2Token) { sameTsResumeToken.eventIdentifier, Value(Document{{"operationType", "update"_sd}, {"documentKey", higherDocumentKey}})); - // The next event has a clusterTime later than the resume point, but it should not use the - // default resume token version if it is below the user's token version. + // The next event has a clusterTime later than the resume point, and should therefore start + // using the default token version. next = lastStage->getNext(); ASSERT(next.isAdvanced()); const auto afterResumeTsResumeToken = ResumeToken::parse(next.releaseDocument()["_id"].getDocument()).getData(); ASSERT_EQ(afterResumeTsResumeToken.clusterTime, afterResumeTs); - ASSERT_EQ(afterResumeTsResumeToken.version, resumeToken.version); - ASSERT_VALUE_EQ( - afterResumeTsResumeToken.eventIdentifier, - Value(Document{{"operationType", "update"_sd}, {"documentKey", midDocumentKey}})); + ASSERT_EQ(afterResumeTsResumeToken.version, ResumeTokenData::kDefaultTokenVersion); + ASSERT_VALUE_EQ(afterResumeTsResumeToken.eventIdentifier, Value(midDocumentKey)); // Verify that no other events are returned. next = lastStage->getNext(); @@ -4640,310 +4583,19 @@ TEST_F(MultiTokenFormatVersionTest, CanResumeFromV2HighWaterMark) { sameTsResumeToken2.eventIdentifier, Value(Document{{"operationType", "update"_sd}, {"documentKey", higherDocumentKey}})); - // The resumeToken after the current clusterTime should keep using the higher version, and - // the corresponding 'eventIdentifier' format. + // The resumeToken after the current clusterTime should start using the default version, and + // corresponding 'eventIdentifier' format. next = lastStage->getNext(); ASSERT(next.isAdvanced()); const auto afterResumeTsResumeToken = ResumeToken::parse(next.releaseDocument()["_id"].getDocument()).getData(); ASSERT_EQ(afterResumeTsResumeToken.clusterTime, afterResumeTs); - ASSERT_EQ(afterResumeTsResumeToken.version, resumeToken.version); - ASSERT_VALUE_EQ(afterResumeTsResumeToken.eventIdentifier, - Value(Document{{"operationType", "update"_sd}, {"documentKey", documentKey}})); + ASSERT_EQ(afterResumeTsResumeToken.version, ResumeTokenData::kDefaultTokenVersion); + ASSERT_VALUE_EQ(afterResumeTsResumeToken.eventIdentifier, Value(documentKey)); // Verify that no other events are returned. next = lastStage->getNext(); ASSERT_FALSE(next.isAdvanced()); } - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamAddPostImage) { - auto spec = DocumentSourceChangeStreamSpec(); - spec.setFullDocument(FullDocumentModeEnum::kUpdateLookup); - - auto docSource = DocumentSourceChangeStreamAddPostImage::create(getExpCtx(), spec); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamAddPostImage":{"fullDocument":"updateLookup"}})", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamAddPostImage":{"fullDocument":"updateLookup"}})", - redact(*docSource)); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamAddPreImage) { - auto docSource = DocumentSourceChangeStreamAddPreImage{ - getExpCtx(), FullDocumentBeforeChangeModeEnum::kWhenAvailable}; - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamAddPreImage": { - "fullDocumentBeforeChange": "whenAvailable" - } - })", - docSource.serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamAddPreImage":{"fullDocumentBeforeChange":"whenAvailable"}})", - redact(docSource)); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamCheckInvalidate) { - DocumentSourceChangeStreamSpec spec; - spec.setResumeAfter(ResumeToken::parse(makeResumeToken(Timestamp(), - testConstUuid, - BSON("_id" << 1 << "x" << 2), - ResumeTokenData::kFromInvalidate))); - - auto docSource = DocumentSourceChangeStreamCheckInvalidate::create(getExpCtx(), spec); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamCheckInvalidate": { - "startAfterInvalidate": { - "_data": "8200000000000000002B022C0100296F5A10046948DF8014BD4E0488427668D9C001F5461E5F6964002B021E78002B040004" - } - } - })", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamCheckInvalidate":{"startAfterInvalidate":{ - "_data": "?string" - }}})", - redact(*docSource)); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamCheckInvalidate":{"startAfterInvalidate":{ - "_data": "820000000000000000292904" - }}})", - docSource - ->serialize( - SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}) - .getDocument() - .toBson()); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamCheckResumability) { - DocumentSourceChangeStreamSpec spec; - spec.setResumeAfter(ResumeToken::parse( - makeResumeToken(Timestamp(), testConstUuid, BSON("_id" << 1 << "x" << 2)))); - - auto docSource = DocumentSourceChangeStreamCheckResumability::create(getExpCtx(), spec); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamCheckResumability": { - "resumeToken": { - "_data": "8200000000000000002B022C0100296E5A10046948DF8014BD4E0488427668D9C001F5461E5F6964002B021E78002B040004" - } - } - })", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamCheckResumability":{ - "resumeToken": { - "_data": "?string" - } - }})", - redact(*docSource)); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamCheckResumability":{ - "resumeToken": { - "_data": "820000000000000000292904" - } - }})", - docSource - ->serialize( - SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}) - .getDocument() - .toBson()); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamCheckTopologyChange) { - auto docSource = DocumentSourceChangeStreamCheckTopologyChange::create(getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamCheckTopologyChange":{}})", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamCheckTopologyChange":{}})", - redact(*docSource)); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamEnsureResumeTokenPresent) { - DocumentSourceChangeStreamSpec spec; - spec.setResumeAfter(ResumeToken::parse( - makeResumeToken(Timestamp(), testConstUuid, BSON("_id" << 1 << "x" << 2)))); - - auto docSource = DocumentSourceChangeStreamEnsureResumeTokenPresent::create(getExpCtx(), spec); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamEnsureResumeTokenPresent": { - "resumeToken": { - "_data": "8200000000000000002B022C0100296E5A10046948DF8014BD4E0488427668D9C001F5461E5F6964002B021E78002B040004" - } - } - })", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamEnsureResumeTokenPresent": { - "resumeToken": { - "_data": "?string" - } - } - })", - redact(*docSource)); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamHandleTopologyChange) { - auto docSource = DocumentSourceChangeStreamHandleTopologyChange::create(getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamHandleTopologyChange":{}})", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalChangeStreamHandleTopologyChange":{}})", - redact(*docSource)); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamSplitLargeEvent) { - DocumentSourceChangeStreamSpec spec; - spec.setResumeAfter(ResumeToken::parse( - makeResumeToken(Timestamp(), testConstUuid, BSON("_id" << 1 << "x" << 2)))); - - auto docSource = DocumentSourceChangeStreamSplitLargeEvent::create(getExpCtx(), spec); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$changeStreamSplitLargeEvent":{}})", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$changeStreamSplitLargeEvent":{}})", - redact(*docSource)); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamTransform) { - DocumentSourceChangeStreamSpec spec; - spec.setResumeAfter(ResumeToken::parse( - makeResumeToken(Timestamp(), testConstUuid, BSON("_id" << 1 << "x" << 2)))); - - auto docSource = DocumentSourceChangeStreamTransform::create(getExpCtx(), spec); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamTransform": { - "resumeAfter": { - "_data": "8200000000000000002B022C0100296E5A10046948DF8014BD4E0488427668D9C001F5461E5F6964002B021E78002B040004" - }, - "fullDocument": "default", - "fullDocumentBeforeChange": "off" - } - })", - docSource->serialize().getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamTransform": { - "resumeAfter": { - "_data": "?string" - }, - "fullDocument": "default", - "fullDocumentBeforeChange": "off" - } - })", - redact(*docSource)); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalChangeStreamTransform": { - "resumeAfter": { - "_data": "820000000000000000292904" - }, - "fullDocument": "default", - "fullDocumentBeforeChange": "off" - } - })", - docSource - ->serialize( - SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}) - .getDocument() - .toBson()); -} - -// For DocumentSource types which contain an arbitrarily internal -// MatchExpression, we don't want match the entire structure. This -// assertion allows us to check some basic structure. -void assertRedactedMatchExpressionContainsOperatorsAndRedactedFieldPaths(BSONElement el) { - // Walk the redacted BSON and assert that we have some ops and - // redacted field paths. - auto opCount = 0; - auto redactedFieldPaths = 0; - while (true) { - if (el.type() == mongo::Array) { - auto array = el.Array(); - if (array.empty()) { - break; - } - el = array[0]; - } else if (el.type() == mongo::Object) { - auto obj = el.Obj(); - if (obj.begin() == obj.end()) { - break; - } - el = obj.firstElement(); - - // Field name should be an operator or a redacted field path. - if (el.fieldName()[0] == '$') { - opCount++; - } else if (!strcmp(el.fieldName(), "$regularExpression")) { - opCount++; - // Skip $regularExpression. - continue; - } else { - if (strstr(el.fieldName(), "HASH<") != el.fieldName()) { - FAIL(std::string("Expected redacted field path: ") + el.fieldName()); - } - redactedFieldPaths++; - } - } else { - break; - } - } - - ASSERT(opCount > 0); - ASSERT(redactedFieldPaths > 0); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamUnwindTransaction) { - auto docSource = DocumentSourceChangeStreamUnwindTransaction::create(getExpCtx()); - - auto redacted = redact(*docSource); - // First, check the outermost structure. - BSONElement el = redacted.getField("$_internalChangeStreamUnwindTransaction"_sd); - ASSERT(el); - el = el.Obj().getField("filter"); - ASSERT(el); - el = el.Obj().firstElement(); - - assertRedactedMatchExpressionContainsOperatorsAndRedactedFieldPaths(el); -} - -TEST_F(ChangeStreamStageTestNoSetup, RedactDocumentSourceChangeStreamOplogMatch) { - DocumentSourceChangeStreamSpec spec; - spec.setResumeAfter(ResumeToken::parse( - makeResumeToken(Timestamp(), testConstUuid, BSON("_id" << 1 << "x" << 2)))); - - auto docSource = DocumentSourceChangeStreamOplogMatch::create(getExpCtx(), spec); - - auto redacted = redact(*docSource); - // First, check the outermost structure. - BSONElement el = redacted.getField("$_internalChangeStreamOplogMatch"_sd); - ASSERT(el); - el = el.Obj().getField("filter"); - ASSERT(el); - el = el.Obj().firstElement(); - - assertRedactedMatchExpressionContainsOperatorsAndRedactedFieldPaths(el); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_transform.cpp b/src/mongo/db/pipeline/document_source_change_stream_transform.cpp index 9e5bd9ecaa6..766150a6e54 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_transform.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_transform.cpp @@ -33,7 +33,6 @@ #include "mongo/db/pipeline/document_source_change_stream_transform.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/pipeline/resume_token.h" @@ -64,10 +63,6 @@ DocumentSourceChangeStreamTransform::createFromBson( rawSpec.type() == BSONType::Object); auto spec = DocumentSourceChangeStreamSpec::parse(IDLParserErrorContext("$changeStream"), rawSpec.Obj()); - - // Set the change stream spec on the expression context. - expCtx->changeStreamSpec = spec; - return new DocumentSourceChangeStreamTransform(expCtx, std::move(spec)); } @@ -79,7 +74,8 @@ DocumentSourceChangeStreamTransform::DocumentSourceChangeStreamTransform( _isIndependentOfAnyCollection(expCtx->ns.isCollectionlessAggregateNS()) { // Extract the resume token or high-water-mark from the spec. - auto tokenData = change_stream::resolveResumeTokenFromSpec(expCtx, _changeStreamSpec); + auto tokenData = + DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, _changeStreamSpec); // Set the initialPostBatchResumeToken on the expression context. expCtx->initialPostBatchResumeToken = ResumeToken(tokenData).toBSON(); @@ -103,109 +99,16 @@ StageConstraints DocumentSourceChangeStreamTransform::constraints( return constraints; } -namespace { - -template <typename T> -void serializeSpecField(BSONObjBuilder* builder, - const SerializationOptions& opts, - const StringData& fieldName, - const boost::optional<T>& value) { - if (value) { - opts.serializeLiteral((*value).toBSON()).addToBsonObj(builder, fieldName); - } -} - -template <> -void serializeSpecField(BSONObjBuilder* builder, - const SerializationOptions& opts, - const StringData& fieldName, - const boost::optional<Timestamp>& value) { - if (value) { - opts.serializeLiteral(*value).addToBsonObj(builder, fieldName); - } -} - -template <typename T> -void serializeSpecField(BSONObjBuilder* builder, - const SerializationOptions& opts, - const StringData& fieldName, - const T& value) { - opts.appendLiteral(builder, fieldName, value); -} - -template <> -void serializeSpecField(BSONObjBuilder* builder, - const SerializationOptions& opts, - const StringData& fieldName, - const mongo::OptionalBool& value) { - if (value.has_value()) { - opts.appendLiteral(builder, fieldName, value.value_or(true)); - } -} - -void serializeSpec(const DocumentSourceChangeStreamSpec& spec, - const SerializationOptions& opts, - BSONObjBuilder* builder) { - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kResumeAfterFieldName, - spec.getResumeAfter()); - serializeSpecField( - builder, opts, DocumentSourceChangeStreamSpec::kStartAfterFieldName, spec.getStartAfter()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kStartAtOperationTimeFieldName, - spec.getStartAtOperationTime()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kFullDocumentFieldName, - ::mongo::FullDocumentMode_serializer(spec.getFullDocument())); - serializeSpecField( - builder, - opts, - DocumentSourceChangeStreamSpec::kFullDocumentBeforeChangeFieldName, - ::mongo::FullDocumentBeforeChangeMode_serializer(spec.getFullDocumentBeforeChange())); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kAllChangesForClusterFieldName, - spec.getAllChangesForCluster()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kShowMigrationEventsFieldName, - spec.getShowMigrationEvents()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kShowSystemEventsFieldName, - spec.getShowSystemEvents()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kAllowToRunOnConfigDBFieldName, - spec.getAllowToRunOnConfigDB()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kAllowToRunOnSystemNSFieldName, - spec.getAllowToRunOnSystemNS()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kShowExpandedEventsFieldName, - spec.getShowExpandedEvents()); - serializeSpecField(builder, - opts, - DocumentSourceChangeStreamSpec::kShowRawUpdateDescriptionFieldName, - spec.getShowRawUpdateDescription()); -} - -} // namespace - -Value DocumentSourceChangeStreamTransform::serialize(const SerializationOptions& opts) const { - if (opts.verbosity) { +Value DocumentSourceChangeStreamTransform::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { return Value(Document{{DocumentSourceChangeStream::kStageName, Document{{"stage"_sd, "internalTransform"_sd}, - {"options"_sd, _changeStreamSpec.toBSON(opts)}}}}); + {"options"_sd, _changeStreamSpec.toBSON()}}}}); } - return Value(Document{ - {DocumentSourceChangeStreamTransform::kStageName, _changeStreamSpec.toBSON(opts)}}); + return Value( + Document{{DocumentSourceChangeStreamTransform::kStageName, _changeStreamSpec.toBSON()}}); } DepsTracker::State DocumentSourceChangeStreamTransform::getDependencies(DepsTracker* deps) const { @@ -215,7 +118,7 @@ DepsTracker::State DocumentSourceChangeStreamTransform::getDependencies(DepsTrac DocumentSource::GetModPathsReturn DocumentSourceChangeStreamTransform::getModifiedPaths() const { // All paths are modified. - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; } DocumentSource::GetNextResult DocumentSourceChangeStreamTransform::doGetNext() { diff --git a/src/mongo/db/pipeline/document_source_change_stream_transform.h b/src/mongo/db/pipeline/document_source_change_stream_transform.h index cedf10bf561..71941800da9 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_transform.h +++ b/src/mongo/db/pipeline/document_source_change_stream_transform.h @@ -54,7 +54,7 @@ public: DocumentSource::GetModPathsReturn getModifiedPaths() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final; diff --git a/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp b/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp index cc5711422b4..05bfcff4df7 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp @@ -127,26 +127,18 @@ StageConstraints DocumentSourceChangeStreamUnwindTransaction::constraints( } Value DocumentSourceChangeStreamUnwindTransaction::serialize( - const SerializationOptions& opts) const { - tassert(7481400, "expression has not been initialized", _expression); - - if (opts.verbosity) { - BSONObjBuilder builder; - builder.append("stage"_sd, "internalUnwindTransaction"_sd); - builder.append(DocumentSourceChangeStreamUnwindTransactionSpec::kFilterFieldName, - _expression->serialize(opts)); - - return Value(DOC(DocumentSourceChangeStream::kStageName << builder.obj())); + boost::optional<ExplainOptions::Verbosity> explain) const { + tassert(5467604, "expression has not been initialized", _expression); + + if (explain) { + return Value( + DOC(DocumentSourceChangeStream::kStageName << DOC("stage" + << "internalUnwindTransaction"_sd + << "filter" << _filter))); } - Value spec; - if (opts.literalPolicy != LiteralSerializationPolicy::kUnchanged || opts.transformIdentifiers) { - spec = Value(DOC(DocumentSourceChangeStreamUnwindTransactionSpec::kFilterFieldName - << _expression->serialize(opts))); - } else { - spec = Value(DocumentSourceChangeStreamUnwindTransactionSpec(_filter).toBSON()); - } - return Value(Document{{kStageName, spec}}); + DocumentSourceChangeStreamUnwindTransactionSpec spec(_filter); + return Value(Document{{kStageName, Value(spec.toBSON())}}); } DepsTracker::State DocumentSourceChangeStreamUnwindTransaction::getDependencies( @@ -165,7 +157,7 @@ DepsTracker::State DocumentSourceChangeStreamUnwindTransaction::getDependencies( DocumentSource::GetModPathsReturn DocumentSourceChangeStreamUnwindTransaction::getModifiedPaths() const { - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; } DocumentSource::GetNextResult DocumentSourceChangeStreamUnwindTransaction::doGetNext() { diff --git a/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.h b/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.h index c31e5c2fc68..ce7278b3b3e 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.h +++ b/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.h @@ -55,7 +55,7 @@ public: DocumentSource::GetModPathsReturn getModifiedPaths() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final; diff --git a/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp b/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp index 2d5eaf32521..28603f10a30 100644 --- a/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp +++ b/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp @@ -206,6 +206,10 @@ protected: if (!_collScan) { _collScan = std::make_unique<CollectionScan>( pExpCtx.get(), _collectionPtr, _params, &_ws, _filter.get()); + // The first call to doWork will create the cursor and return NEED_TIME. But it won't + // actually scan any of the documents that are present in the mock cursor queue. + ASSERT_EQ(_collScan->doWork(nullptr), PlanStage::NEED_TIME); + ASSERT_EQ(_getNumDocsTested(), 0); } while (true) { // If the next result is a pause, return it and don't collscan. @@ -224,7 +228,6 @@ protected: // entry into the oplog. This is like a stripped-down DSCSTransform stage. MutableDocument mutableDoc{_ws.get(id)->doc.value()}; mutableDoc["_id"] = nextResult.getDocument()["_id"]; - mutableDoc.metadata().setSortKey(nextResult.getDocument()["_id"], true); return mutableDoc.freeze(); } case PlanStage::NEED_TIME: @@ -480,12 +483,8 @@ TEST_F(CheckResumeTokenTest, ShouldFailIfTokenHasWrongNamespace) { Timestamp resumeTimestamp(100, 1); auto resumeTokenUUID = UUID::gen(); - auto otherUUID = UUID::gen(); - ASSERT_NE(resumeTokenUUID, otherUUID); - if (resumeTokenUUID > otherUUID) { - std::swap(resumeTokenUUID, otherUUID); - } auto checkResumeToken = createDSEnsureResumeTokenPresent(resumeTimestamp, "1", resumeTokenUUID); + auto otherUUID = UUID::gen(); addOplogEntryOnTestNS(resumeTimestamp, "1", otherUUID); ASSERT_THROWS_CODE( checkResumeToken->getNext(), AssertionException, ErrorCodes::ChangeStreamFatalError); diff --git a/src/mongo/db/pipeline/document_source_coll_stats.cpp b/src/mongo/db/pipeline/document_source_coll_stats.cpp index a947f1589e6..c00f5fcd44d 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.cpp +++ b/src/mongo/db/pipeline/document_source_coll_stats.cpp @@ -71,15 +71,18 @@ intrusive_ptr<DocumentSource> DocumentSourceCollStats::createFromBson( return make_intrusive<DocumentSourceCollStats>(pExpCtx, std::move(spec)); } -BSONObj DocumentSourceCollStats::makeStatsForNs( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& nss, - const DocumentSourceCollStatsSpec& spec) { +DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() { + if (_finished) { + return GetNextResult::makeEOF(); + } + + _finished = true; + BSONObjBuilder builder; - builder.append("ns", nss.ns()); + builder.append("ns", pExpCtx->ns.ns()); - auto shardName = expCtx->mongoProcessInterface->getShardName(expCtx->opCtx); + auto shardName = pExpCtx->mongoProcessInterface->getShardName(pExpCtx->opCtx); if (!shardName.empty()) { builder.append("shard", shardName); @@ -88,49 +91,37 @@ BSONObj DocumentSourceCollStats::makeStatsForNs( builder.append("host", getHostNameCachedAndPort()); builder.appendDate("localTime", jsTime()); - if (auto latencyStatsSpec = spec.getLatencyStats()) { - // getRequestOnTimeseriesView is set to true if collstats is called on the view. - auto resolvedNss = - spec.getRequestOnTimeseriesView() ? nss.getTimeseriesViewNamespace() : nss; - expCtx->mongoProcessInterface->appendLatencyStats( - expCtx->opCtx, resolvedNss, latencyStatsSpec->getHistograms(), &builder); + if (auto latencyStatsSpec = _collStatsSpec.getLatencyStats()) { + pExpCtx->mongoProcessInterface->appendLatencyStats( + pExpCtx->opCtx, pExpCtx->ns, latencyStatsSpec->getHistograms(), &builder); } - if (auto storageStats = spec.getStorageStats()) { + if (auto storageStats = _collStatsSpec.getStorageStats()) { // If the storageStats field exists, it must have been validated as an object when parsing. BSONObjBuilder storageBuilder(builder.subobjStart("storageStats")); - uassertStatusOKWithContext(expCtx->mongoProcessInterface->appendStorageStats( - expCtx->opCtx, nss, *storageStats, &storageBuilder), + uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendStorageStats( + pExpCtx->opCtx, pExpCtx->ns, *storageStats, &storageBuilder), "Unable to retrieve storageStats in $collStats stage"); storageBuilder.doneFast(); } - if (spec.getCount()) { - uassertStatusOKWithContext( - expCtx->mongoProcessInterface->appendRecordCount(expCtx->opCtx, nss, &builder), - "Unable to retrieve count in $collStats stage"); + if (_collStatsSpec.getCount()) { + uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendRecordCount( + pExpCtx->opCtx, pExpCtx->ns, &builder), + "Unable to retrieve count in $collStats stage"); } - if (spec.getQueryExecStats()) { - uassertStatusOKWithContext( - expCtx->mongoProcessInterface->appendQueryExecStats(expCtx->opCtx, nss, &builder), - "Unable to retrieve queryExecStats in $collStats stage"); + if (_collStatsSpec.getQueryExecStats()) { + uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendQueryExecStats( + pExpCtx->opCtx, pExpCtx->ns, &builder), + "Unable to retrieve queryExecStats in $collStats stage"); } - return builder.obj(); -} - -DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() { - if (_finished) { - return GetNextResult::makeEOF(); - } - - _finished = true; - return {Document(makeStatsForNs(pExpCtx, pExpCtx->ns, _collStatsSpec))}; + return {Document(builder.obj())}; } -Value DocumentSourceCollStats::serialize(const SerializationOptions& opts) const { - return Value(Document{{getSourceName(), _collStatsSpec.toBSON(opts)}}); +Value DocumentSourceCollStats::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(Document{{getSourceName(), _collStatsSpec.toBSON()}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_coll_stats.h b/src/mongo/db/pipeline/document_source_coll_stats.h index 5c5f3d5482b..72c6923ccdc 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.h +++ b/src/mongo/db/pipeline/document_source_coll_stats.h @@ -85,10 +85,6 @@ public: const DocumentSourceCollStatsSpec _spec; }; - static BSONObj makeStatsForNs(const boost::intrusive_ptr<ExpressionContext>&, - const NamespaceString&, - const DocumentSourceCollStatsSpec&); - DocumentSourceCollStats(const boost::intrusive_ptr<ExpressionContext>& pExpCtx, DocumentSourceCollStatsSpec spec) : DocumentSource(kStageName, pExpCtx), _collStatsSpec(std::move(spec)) {} @@ -113,7 +109,7 @@ public: return boost::none; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); diff --git a/src/mongo/db/pipeline/document_source_coll_stats.idl b/src/mongo/db/pipeline/document_source_coll_stats.idl index b432912392a..060c20d835a 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.idl +++ b/src/mongo/db/pipeline/document_source_coll_stats.idl @@ -39,50 +39,31 @@ structs: LatencyStatsSpec: description: Represents the 'latencyStats' argument to the $collStats stage. strict: true - query_shape_component: true fields: histograms: description: Adds latency histogram information to the embedded documents in latencyStats if true. type: optionalBool - # Do not abstract this literal, since it is parameterizing the stage like an enum rather than representing - # real user input. - query_shape: parameter DocumentSourceCollStatsSpec: description: Specification for a $collStats stage. strict: true - query_shape_component: true fields: latencyStats: description: A request to include latency stats in the $collStats output. type: LatencyStatsSpec optional: true - query_shape: literal storageStats: description: Adds storage statistics to the return document. type: StorageStatsSpec optional: true - query_shape: literal count: description: Adds the total number of documents in the collection to the return document. type: object validator: callback: validateObjectIsEmpty optional: true - # Treat this value as a parameter (meaning it won't be abstracted or transformed) because - # the _only_ accepted value is the empty object. There's no transformation needed. - query_shape: parameter queryExecStats: description: Adds query execution statistics to the return document. type: object validator: callback: validateObjectIsEmpty optional: true - # Treat this value as a parameter (meaning it won't be abstracted or transformed) because - # the _only_ accepted value is the empty object. There's no transformation needed. - query_shape: parameter - $_requestOnTimeseriesView: - description: When set to true, $collStats stage requests statistics from the view namespace. - When set to false, $collStats stage requests statistics from the underlying collection. - cpp_name: requestOnTimeseriesView - type: optionalBool - query_shape: parameter diff --git a/src/mongo/db/pipeline/document_source_coll_stats_test.cpp b/src/mongo/db/pipeline/document_source_coll_stats_test.cpp deleted file mode 100644 index f958307c8be..00000000000 --- a/src/mongo/db/pipeline/document_source_coll_stats_test.cpp +++ /dev/null @@ -1,129 +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 "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/pipeline/document_source_coll_stats.h" -#include "mongo/db/pipeline/document_source_coll_stats_gen.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/unittest/bson_test_util.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace { -using DocumentSourceCollStatsTest = AggregationContextFixture; - -auto representativeShape(const DocumentSourceCollStats& collStatsStage) { - SerializationOptions opts{LiteralSerializationPolicy::kToRepresentativeParseableValue}; - return collStatsStage.serialize(opts).getDocument().toBson(); -} - -TEST_F(DocumentSourceCollStatsTest, QueryShape) { - auto spec = DocumentSourceCollStatsSpec(); - - auto stage = make_intrusive<DocumentSourceCollStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$collStats":{}})", - redact(*stage)); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$collStats":{}})", - representativeShape(*stage)); - - spec.setCount(BSONObj()); - spec.setQueryExecStats(BSONObj()); - stage = make_intrusive<DocumentSourceCollStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$collStats":{"count":{},"queryExecStats":{}}})", - redact(*stage)); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$collStats":{"count":{},"queryExecStats":{}}})", - representativeShape(*stage)); - - auto latencyStats = LatencyStatsSpec(); - latencyStats.setHistograms(true); - spec.setLatencyStats(latencyStats); - stage = make_intrusive<DocumentSourceCollStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$collStats": { - "latencyStats": { - "histograms": true - }, - "count": {}, - "queryExecStats": {} - } - })", - redact(*stage)); - - auto storageStats = StorageStatsSpec(); - storageStats.setScale(2); - storageStats.setVerbose(true); - spec.setStorageStats(storageStats); - stage = make_intrusive<DocumentSourceCollStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$collStats": { - "latencyStats": { - "histograms": true - }, - "storageStats": { - "scale": "?number", - "verbose": true, - "waitForLock": true, - "numericOnly": false - }, - "count": {}, - "queryExecStats": {} - } - })", - redact(*stage)); - - storageStats.setWaitForLock(false); - storageStats.setNumericOnly(false); - spec.setStorageStats(storageStats); - stage = make_intrusive<DocumentSourceCollStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$collStats": { - "latencyStats": { - "histograms": true - }, - "storageStats": { - "scale": "?number", - "verbose": true, - "waitForLock": false, - "numericOnly": false - }, - "count": {}, - "queryExecStats": {} - } - })", - redact(*stage)); -} -} // namespace -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_count_test.cpp b/src/mongo/db/pipeline/document_source_count_test.cpp index 399086b0b52..9ffd8a6d120 100644 --- a/src/mongo/db/pipeline/document_source_count_test.cpp +++ b/src/mongo/db/pipeline/document_source_count_test.cpp @@ -65,13 +65,10 @@ public: dynamic_cast<DocumentSourceSingleDocumentTransformation*>(result.back().get()); ASSERT(projectStage); + auto explain = ExplainOptions::Verbosity::kQueryPlanner; vector<Value> explainedStages; - groupStage->serializeToArray( - explainedStages, - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}); - projectStage->serializeToArray( - explainedStages, - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}); + groupStage->serializeToArray(explainedStages, explain); + projectStage->serializeToArray(explainedStages, explain); ASSERT_EQUALS(explainedStages.size(), 2UL); StringData countName = countSpec.firstElement().valueStringData(); diff --git a/src/mongo/db/pipeline/document_source_current_op.cpp b/src/mongo/db/pipeline/document_source_current_op.cpp index fc5c8b32a1f..7b453935dee 100644 --- a/src/mongo/db/pipeline/document_source_current_op.cpp +++ b/src/mongo/db/pipeline/document_source_current_op.cpp @@ -27,13 +27,11 @@ * it in the license file. */ -#include "mongo/db/commands/fsync_locked.h" #include "mongo/platform/basic.h" #include "mongo/db/pipeline/document_source_current_op.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/s/sharding_feature_flags_gen.h" namespace mongo { @@ -70,8 +68,8 @@ std::unique_ptr<DocumentSourceCurrentOp::LiteParsed> DocumentSourceCurrentOp::Li << typeName(spec.type())); } - auto allUsers = kDefaultUserMode; - auto localOps = kDefaultLocalOpsMode; + auto allUsers = UserMode::kExcludeOthers; + auto localOps = LocalOpsMode::kRemoteShardOps; // Check the spec for all fields named 'allUsers'. If any of them are 'true', we require // the 'inprog' privilege. This avoids the possibility that a spec with multiple @@ -113,14 +111,13 @@ const char* DocumentSourceCurrentOp::getSourceName() const { DocumentSource::GetNextResult DocumentSourceCurrentOp::doGetNext() { if (_ops.empty()) { - _ops = pExpCtx->mongoProcessInterface->getCurrentOps( - pExpCtx, - _includeIdleConnections.value_or(kDefaultConnMode), - _includeIdleSessions.value_or(kDefaultSessionMode), - _includeOpsFromAllUsers.value_or(kDefaultUserMode), - _truncateOps.value_or(kDefaultTruncationMode), - _idleCursors.value_or(kDefaultCursorMode), - _backtrace.value_or(kDefaultBacktraceMode)); + _ops = pExpCtx->mongoProcessInterface->getCurrentOps(pExpCtx, + _includeIdleConnections, + _includeIdleSessions, + _includeOpsFromAllUsers, + _truncateOps, + _idleCursors, + _backtrace); _opsIter = _ops.begin(); @@ -148,10 +145,6 @@ DocumentSource::GetNextResult DocumentSourceCurrentOp::doGetNext() { // Add the shard name to the output document. doc.addField(kShardFieldName, Value(_shardName)); - if (mongo::lockedForWriting()) { - doc.addField(StringData("fsyncLock"), Value(true)); - } - // For operations on a shard, we change the opid from the raw numeric form to // 'shardname:opid'. We also change the fieldname 'client' to 'client_s' to indicate // that the IP is that of the mongos which initiated this request. @@ -192,13 +185,13 @@ intrusive_ptr<DocumentSource> DocumentSourceCurrentOp::createFromBson( "$currentOp must be run against the 'admin' database with {aggregate: 1}", nss.db() == NamespaceString::kAdminDb && nss.isCollectionlessAggregateNS()); - boost::optional<ConnMode> includeIdleConnections; - boost::optional<SessionMode> includeIdleSessions; - boost::optional<UserMode> includeOpsFromAllUsers; - boost::optional<LocalOpsMode> showLocalOpsOnMongoS; - boost::optional<TruncationMode> truncateOps; - boost::optional<CursorMode> idleCursors; - boost::optional<BacktraceMode> backtrace; + ConnMode includeIdleConnections = ConnMode::kExcludeIdle; + SessionMode includeIdleSessions = SessionMode::kIncludeIdle; + UserMode includeOpsFromAllUsers = UserMode::kExcludeOthers; + LocalOpsMode showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps; + TruncationMode truncateOps = TruncationMode::kNoTruncation; + CursorMode idleCursors = CursorMode::kExcludeCursors; + BacktraceMode backtrace = BacktraceMode::kExcludeBacktrace; for (auto&& elem : spec.embeddedObject()) { const auto fieldName = elem.fieldNameStringData(); @@ -279,13 +272,13 @@ intrusive_ptr<DocumentSource> DocumentSourceCurrentOp::createFromBson( intrusive_ptr<DocumentSourceCurrentOp> DocumentSourceCurrentOp::create( const boost::intrusive_ptr<ExpressionContext>& pExpCtx, - boost::optional<ConnMode> includeIdleConnections, - boost::optional<SessionMode> includeIdleSessions, - boost::optional<UserMode> includeOpsFromAllUsers, - boost::optional<LocalOpsMode> showLocalOpsOnMongoS, - boost::optional<TruncationMode> truncateOps, - boost::optional<CursorMode> idleCursors, - boost::optional<BacktraceMode> backtrace) { + ConnMode includeIdleConnections, + SessionMode includeIdleSessions, + UserMode includeOpsFromAllUsers, + LocalOpsMode showLocalOpsOnMongoS, + TruncationMode truncateOps, + CursorMode idleCursors, + BacktraceMode backtrace) { return new DocumentSourceCurrentOp(pExpCtx, includeIdleConnections, includeIdleSessions, @@ -296,38 +289,22 @@ intrusive_ptr<DocumentSourceCurrentOp> DocumentSourceCurrentOp::create( backtrace); } -Value DocumentSourceCurrentOp::serialize(const SerializationOptions& opts) const { +Value DocumentSourceCurrentOp::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { return Value(Document{ {getSourceName(), - Document{ - {kIdleConnectionsFieldName, - _includeIdleConnections.has_value() - ? opts.serializeLiteral(_includeIdleConnections.value() == ConnMode::kIncludeIdle) - : Value()}, - {kIdleSessionsFieldName, - _includeIdleSessions.has_value() - ? opts.serializeLiteral(_includeIdleSessions.value() == SessionMode::kIncludeIdle) - : Value()}, - {kAllUsersFieldName, - _includeOpsFromAllUsers.has_value() - ? opts.serializeLiteral(_includeOpsFromAllUsers.value() == UserMode::kIncludeAll) - : Value()}, - {kLocalOpsFieldName, - _showLocalOpsOnMongoS.has_value() - ? opts.serializeLiteral(_showLocalOpsOnMongoS.value() == - LocalOpsMode::kLocalMongosOps) - : Value()}, - {kTruncateOpsFieldName, - _truncateOps.has_value() - ? opts.serializeLiteral(_truncateOps.value() == TruncationMode::kTruncateOps) - : Value()}, - {kIdleCursorsFieldName, - _idleCursors.has_value() - ? opts.serializeLiteral(_idleCursors.value() == CursorMode::kIncludeCursors) - : Value()}, - {kBacktraceFieldName, - _backtrace.has_value() - ? opts.serializeLiteral(_backtrace.value() == BacktraceMode::kIncludeBacktrace) - : Value()}}}}); + Document{{kIdleConnectionsFieldName, + _includeIdleConnections == ConnMode::kIncludeIdle ? Value(true) : Value()}, + {kIdleSessionsFieldName, + _includeIdleSessions == SessionMode::kExcludeIdle ? Value(false) : Value()}, + {kAllUsersFieldName, + _includeOpsFromAllUsers == UserMode::kIncludeAll ? Value(true) : Value()}, + {kLocalOpsFieldName, + _showLocalOpsOnMongoS == LocalOpsMode::kLocalMongosOps ? Value(true) : Value()}, + {kTruncateOpsFieldName, + _truncateOps == TruncationMode::kTruncateOps ? Value(true) : Value()}, + {kIdleCursorsFieldName, + _idleCursors == CursorMode::kIncludeCursors ? Value(true) : Value()}, + {kBacktraceFieldName, + _backtrace == BacktraceMode::kIncludeBacktrace ? Value(true) : Value()}}}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_current_op.h b/src/mongo/db/pipeline/document_source_current_op.h index 4cc53475e4b..49e0be6c35d 100644 --- a/src/mongo/db/pipeline/document_source_current_op.h +++ b/src/mongo/db/pipeline/document_source_current_op.h @@ -45,14 +45,6 @@ public: static constexpr StringData kStageName = "$currentOp"_sd; - static constexpr ConnMode kDefaultConnMode = ConnMode::kExcludeIdle; - static constexpr SessionMode kDefaultSessionMode = SessionMode::kIncludeIdle; - static constexpr UserMode kDefaultUserMode = UserMode::kExcludeOthers; - static constexpr LocalOpsMode kDefaultLocalOpsMode = LocalOpsMode::kRemoteShardOps; - static constexpr TruncationMode kDefaultTruncationMode = TruncationMode::kNoTruncation; - static constexpr CursorMode kDefaultCursorMode = CursorMode::kExcludeCursors; - static constexpr BacktraceMode kDefaultBacktraceMode = BacktraceMode::kExcludeBacktrace; - class LiteParsed final : public LiteParsedDocumentSource { public: static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, @@ -106,28 +98,29 @@ public: static boost::intrusive_ptr<DocumentSourceCurrentOp> create( const boost::intrusive_ptr<ExpressionContext>& pExpCtx, - boost::optional<ConnMode> includeIdleConnections = boost::none, - boost::optional<SessionMode> includeIdleSessions = boost::none, - boost::optional<UserMode> includeOpsFromAllUsers = boost::none, - boost::optional<LocalOpsMode> showLocalOpsOnMongoS = boost::none, - boost::optional<TruncationMode> truncateOps = boost::none, - boost::optional<CursorMode> idleCursors = boost::none, - boost::optional<BacktraceMode> backtrace = boost::none); + ConnMode includeIdleConnections = ConnMode::kExcludeIdle, + SessionMode includeIdleSessions = SessionMode::kIncludeIdle, + UserMode includeOpsFromAllUsers = UserMode::kExcludeOthers, + LocalOpsMode showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps, + TruncationMode truncateOps = TruncationMode::kNoTruncation, + CursorMode idleCursors = CursorMode::kExcludeCursors, + BacktraceMode backtrace = BacktraceMode::kExcludeBacktrace); const char* getSourceName() const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { - bool showLocalOps = - _showLocalOpsOnMongoS.value_or(kDefaultLocalOpsMode) == LocalOpsMode::kLocalMongosOps; - StageConstraints constraints( - StreamType::kStreaming, - PositionRequirement::kFirst, - (showLocalOps ? HostTypeRequirement::kLocalOnly : HostTypeRequirement::kAnyShard), - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kNotAllowed, - TransactionRequirement::kNotAllowed, - LookupRequirement::kAllowed, - (showLocalOps ? UnionRequirement::kNotAllowed : UnionRequirement::kAllowed)); + StageConstraints constraints(StreamType::kStreaming, + PositionRequirement::kFirst, + (_showLocalOpsOnMongoS == LocalOpsMode::kLocalMongosOps + ? HostTypeRequirement::kLocalOnly + : HostTypeRequirement::kAnyShard), + DiskUseRequirement::kNoDiskUse, + FacetRequirement::kNotAllowed, + TransactionRequirement::kNotAllowed, + LookupRequirement::kAllowed, + (_showLocalOpsOnMongoS == LocalOpsMode::kLocalMongosOps + ? UnionRequirement::kNotAllowed + : UnionRequirement::kAllowed)); constraints.isIndependentOfAnyCollection = true; constraints.requiresInputDocSource = false; @@ -141,17 +134,17 @@ public: static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; private: DocumentSourceCurrentOp(const boost::intrusive_ptr<ExpressionContext>& pExpCtx, - boost::optional<ConnMode> includeIdleConnections, - boost::optional<SessionMode> includeIdleSessions, - boost::optional<UserMode> includeOpsFromAllUsers, - boost::optional<LocalOpsMode> showLocalOpsOnMongoS, - boost::optional<TruncationMode> truncateOps, - boost::optional<CursorMode> idleCursors, - boost::optional<BacktraceMode> backtrace) + ConnMode includeIdleConnections, + SessionMode includeIdleSessions, + UserMode includeOpsFromAllUsers, + LocalOpsMode showLocalOpsOnMongoS, + TruncationMode truncateOps, + CursorMode idleCursors, + BacktraceMode backtrace) : DocumentSource(kStageName, pExpCtx), _includeIdleConnections(includeIdleConnections), _includeIdleSessions(includeIdleSessions), @@ -163,13 +156,13 @@ private: GetNextResult doGetNext() final; - boost::optional<ConnMode> _includeIdleConnections; - boost::optional<SessionMode> _includeIdleSessions; - boost::optional<UserMode> _includeOpsFromAllUsers; - boost::optional<LocalOpsMode> _showLocalOpsOnMongoS; - boost::optional<TruncationMode> _truncateOps; - boost::optional<CursorMode> _idleCursors; - boost::optional<BacktraceMode> _backtrace; + ConnMode _includeIdleConnections = ConnMode::kExcludeIdle; + SessionMode _includeIdleSessions = SessionMode::kIncludeIdle; + UserMode _includeOpsFromAllUsers = UserMode::kExcludeOthers; + LocalOpsMode _showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps; + TruncationMode _truncateOps = TruncationMode::kNoTruncation; + CursorMode _idleCursors = CursorMode::kExcludeCursors; + BacktraceMode _backtrace = BacktraceMode::kExcludeBacktrace; std::string _shardName; diff --git a/src/mongo/db/pipeline/document_source_current_op_test.cpp b/src/mongo/db/pipeline/document_source_current_op_test.cpp index 440205960f2..260bfbf0829 100644 --- a/src/mongo/db/pipeline/document_source_current_op_test.cpp +++ b/src/mongo/db/pipeline/document_source_current_op_test.cpp @@ -154,10 +154,10 @@ TEST_F(DocumentSourceCurrentOpTest, ShouldFailToParseIfUnrecognisedParameterSpec ErrorCodes::FailedToParse); } -TEST_F(DocumentSourceCurrentOpTest, ShouldParseAndSerializeAllExplicitlySpecifiedArguments) { +TEST_F(DocumentSourceCurrentOpTest, ShouldParseAndSerializeNonDefaultOptionalArguments) { const auto specObj = fromjson( - "{$currentOp:{idleConnections:false, idleSessions:false, allUsers:true, localOps:true, " - "truncateOps:false}}"); + "{$currentOp:{idleConnections:true, idleSessions:false, allUsers:true, localOps:true, " + "truncateOps:true}}"); const auto parsed = DocumentSourceCurrentOp::createFromBson(specObj.firstElement(), getExpCtx()); @@ -165,42 +165,19 @@ TEST_F(DocumentSourceCurrentOpTest, ShouldParseAndSerializeAllExplicitlySpecifie const auto currentOp = static_cast<DocumentSourceCurrentOp*>(parsed.get()); const auto expectedOutput = Document{{"$currentOp", - Document{{"idleConnections", false}, + Document{{"idleConnections", true}, {"idleSessions", false}, {"allUsers", true}, {"localOps", true}, - {"truncateOps", false}}}}; + {"truncateOps", true}}}}; ASSERT_DOCUMENT_EQ(currentOp->serialize().getDocument(), expectedOutput); } -TEST_F(DocumentSourceCurrentOpTest, - ShouldParseAndSerializeAllExplicitlySpecifiedArgumentsWithRedaction) { - auto spec = fromjson( - R"({ - $currentOp: { - idleConnections: true, - allUsers: false, - idleSessions: false, - localOps: true - } - })"); - auto docSource = DocumentSourceCurrentOp::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$currentOp": { - "idleConnections": "?bool", - "idleSessions": "?bool", - "allUsers": "?bool", - "localOps": "?bool" - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceCurrentOpTest, ShouldNotSerializeOmittedOptionalArguments) { - const auto specObj = fromjson("{$currentOp:{}}"); +TEST_F(DocumentSourceCurrentOpTest, ShouldParseButNotSerializeDefaultOptionalArguments) { + const auto specObj = fromjson( + "{$currentOp:{idleConnections:false, idleSessions:true, allUsers:false, localOps:false, " + "truncateOps:false}}"); const auto parsed = DocumentSourceCurrentOp::createFromBson(specObj.firstElement(), getExpCtx()); @@ -212,17 +189,17 @@ TEST_F(DocumentSourceCurrentOpTest, ShouldNotSerializeOmittedOptionalArguments) ASSERT_DOCUMENT_EQ(currentOp->serialize().getDocument(), expectedOutput); } -TEST_F(DocumentSourceCurrentOpTest, ShouldNotSerializeOmittedOptionalArgumentsWithRedaction) { +TEST_F(DocumentSourceCurrentOpTest, ShouldNotSerializeOmittedOptionalArguments) { const auto specObj = fromjson("{$currentOp:{}}"); - const auto docSource = + const auto parsed = DocumentSourceCurrentOp::createFromBson(specObj.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$currentOp": {} - })", - redact(*docSource)); + const auto currentOp = static_cast<DocumentSourceCurrentOp*>(parsed.get()); + + const auto expectedOutput = Document{{"$currentOp", Document{}}}; + + ASSERT_DOCUMENT_EQ(currentOp->serialize().getDocument(), expectedOutput); } TEST_F(DocumentSourceCurrentOpTest, ShouldReturnEOFImmediatelyIfNoCurrentOps) { diff --git a/src/mongo/db/pipeline/document_source_cursor.cpp b/src/mongo/db/pipeline/document_source_cursor.cpp index 520c32f74cc..55bacba9d7f 100644 --- a/src/mongo/db/pipeline/document_source_cursor.cpp +++ b/src/mongo/db/pipeline/document_source_cursor.cpp @@ -206,12 +206,9 @@ void DocumentSourceCursor::recordPlanSummaryStats() { _exec->getPlanExplainer().getSummaryStats(&_stats.planSummaryStats); } -Value DocumentSourceCursor::serialize(const SerializationOptions& opts) const { - auto verbosity = opts.verbosity; - // We never parse a DocumentSourceCursor, so we only serialize for explain. Since it's never - // part of user input, there's no need to compute its query shape. - if (!verbosity || opts.transformIdentifiers || - opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) +Value DocumentSourceCursor::serialize(boost::optional<ExplainOptions::Verbosity> verbosity) const { + // We never parse a DocumentSourceCursor, so we only serialize for explain. + if (!verbosity) return Value(); invariant(_exec); diff --git a/src/mongo/db/pipeline/document_source_cursor.h b/src/mongo/db/pipeline/document_source_cursor.h index 29f724da0a1..38a2ab183ae 100644 --- a/src/mongo/db/pipeline/document_source_cursor.h +++ b/src/mongo/db/pipeline/document_source_cursor.h @@ -72,7 +72,7 @@ public: const char* getSourceName() const override; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { StageConstraints constraints(StreamType::kStreaming, diff --git a/src/mongo/db/pipeline/document_source_densify.cpp b/src/mongo/db/pipeline/document_source_densify.cpp index b7899f078a2..740ef193431 100644 --- a/src/mongo/db/pipeline/document_source_densify.cpp +++ b/src/mongo/db/pipeline/document_source_densify.cpp @@ -335,19 +335,10 @@ DocumentSource::GetNextResult DocumentSourceInternalDensify::densifyExplicitRang RangeStatement(_range.getStep(), ExplicitBounds(bounds.first, bounds.second), _range.getUnit())); - } else if (_current < bounds.first) { - // All the documents we saw were below the explicit range, so _current is below the range. - // Densification starts at the first bounds, so _current is no longer relevant. - createDocGenerator(bounds.first, - RangeStatement(_range.getStep(), - ExplicitBounds(bounds.first, bounds.second), - _range.getUnit())); - } else if (_current->increment(_range) >= bounds.second) { _densifyState = DensifyState::kDensifyDone; return DocumentSource::GetNextResult::makeEOF(); } else { - // _current is somewhere in the middle of the range. auto lowerBound = _current->increment(_range); createDocGenerator(lowerBound, RangeStatement(_range.getStep(), @@ -417,7 +408,6 @@ DocumentSource::GetNextResult DocumentSourceInternalDensify::processFirstDocForE case ValComparedToRange::kRangeMin: { _densifyState = DensifyState::kNeedGen; _current = val; - setPartitionValue(doc); return doc; } case ValComparedToRange::kBelow: { @@ -645,16 +635,17 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalDensify::createFromBs return results.front(); } -Value DocumentSourceInternalDensify::serialize(const SerializationOptions& opts) const { +Value DocumentSourceInternalDensify::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument spec; - spec[kFieldFieldName] = Value(opts.serializeFieldPath(_field)); + spec[kFieldFieldName] = Value(_field.fullPath()); std::vector<Value> serializedPartitionByFields(_partitions.size()); std::transform(_partitions.begin(), _partitions.end(), serializedPartitionByFields.begin(), - [&](FieldPath field) -> Value { return Value(opts.serializeFieldPath(field)); }); + [&](FieldPath field) -> Value { return Value(field.fullPath()); }); spec[kPartitionByFieldsFieldName] = Value(serializedPartitionByFields); - spec[kRangeFieldName] = _range.serialize(opts); + spec[kRangeFieldName] = _range.serialize(); MutableDocument out; out[getSourceName()] = Value(spec.freeze()); diff --git a/src/mongo/db/pipeline/document_source_densify.h b/src/mongo/db/pipeline/document_source_densify.h index 1214c0bac53..6415c5425d8 100644 --- a/src/mongo/db/pipeline/document_source_densify.h +++ b/src/mongo/db/pipeline/document_source_densify.h @@ -241,16 +241,15 @@ public: static RangeStatement parse(RangeSpec spec); - Value serialize(const SerializationOptions& opts) const { + Value serialize() const { MutableDocument spec; - spec[kArgStep] = opts.serializeLiteral(_step); + spec[kArgStep] = _step; spec[kArgBounds] = stdx::visit( visit_helper::Overloaded{[&](Full) { return Value(kValFull); }, [&](Partition) { return Value(kValPartition); }, [&](ExplicitBounds bounds) { return Value(std::vector<Value>( - {opts.serializeLiteral(bounds.first.toValue()), - opts.serializeLiteral(bounds.second.toValue())})); + {bounds.first.toValue(), bounds.second.toValue()})); }}, _bounds); if (_unit) @@ -381,7 +380,7 @@ public: return kStageName.rawData(); } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; DepsTracker::State getDependencies(DepsTracker* deps) const final { deps->fields.insert(_field.fullPath()); diff --git a/src/mongo/db/pipeline/document_source_densify_test.cpp b/src/mongo/db/pipeline/document_source_densify_test.cpp index f83101b7736..c1a0218e9eb 100644 --- a/src/mongo/db/pipeline/document_source_densify_test.cpp +++ b/src/mongo/db/pipeline/document_source_densify_test.cpp @@ -49,7 +49,6 @@ using DensifyExplicitNumericTest = AggregationContextFixture; using DensifyPartitionNumericTest = AggregationContextFixture; using DensifyCloneTest = AggregationContextFixture; using DensifyStepTest = AggregationContextFixture; -using DensifyRedactionTest = AggregationContextFixture; Date_t makeDate(std::string dateStr) { auto statusDate = dateFromISOString(dateStr); @@ -849,161 +848,6 @@ TEST_F(DensifyExplicitNumericTest, CorrectlyDensifiesForNumericExplicitRangeStar ASSERT_FALSE(next.isAdvanced()); } -TEST_F(DensifyExplicitNumericTest, - CorrectlyDensifiesForPartionedRangeStartingBeforeBoundsRangeWithDocMatchingBoundsStart) { - auto densify = DocumentSourceInternalDensify( - getExpCtx(), - "b", - std::list<FieldPath>({"a"}), - RangeStatement(Value(1), ExplicitBounds(Value(4), Value(8)), boost::none)); - auto source = DocumentSourceMock::createForTest( - {"{a: 1, b: 2}", "{a: 1, b: 4}", "{a: 1, b: 6}"}, getExpCtx()); - densify.setSource(source.get()); - - auto next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(4, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(5, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(6, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(7, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT_FALSE(next.isAdvanced()); - - next = densify.getNext(); - ASSERT_FALSE(next.isAdvanced()); -} - -TEST_F(DensifyExplicitNumericTest, CorrectlyDensifiesForPartionedRangeAcrossTwoPartitions) { - auto densify = DocumentSourceInternalDensify( - getExpCtx(), - "b", - std::list<FieldPath>({"a"}), - RangeStatement(Value(1), ExplicitBounds(Value(0), Value(10)), boost::none)); - auto source = DocumentSourceMock::createForTest( - {"{a: 1, b: 2}", "{a: 2, b: 5}", "{a: 1, b: 7}", "{a: 2, b: 9}"}, getExpCtx()); - densify.setSource(source.get()); - - auto next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(0, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(1, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(2, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(0, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(1, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(2, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(3, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(4, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(5, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(3, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(4, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(5, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(6, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(7, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(6, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(7, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(8, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(2, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(9, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(8, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT(next.isAdvanced()); - ASSERT_EQUALS(1, next.getDocument().getField("a").getDouble()); - ASSERT_EQUALS(9, next.getDocument().getField("b").getDouble()); - - next = densify.getNext(); - ASSERT_FALSE(next.isAdvanced()); - - next = densify.getNext(); - ASSERT_FALSE(next.isAdvanced()); -} - TEST_F(DensifyExplicitNumericTest, CorrectlyDensifiesForNumericExplicitRangeOnlyInsideRange) { auto densify = DocumentSourceInternalDensify( getExpCtx(), @@ -1378,7 +1222,7 @@ TEST_F(DensifyPartitionNumericTest, DensifiesOnImmediateEOFExplicitRange) { ASSERT_FALSE(next.isAdvanced()); } -TEST_F(DensifyCloneTest, InternalDensifyCanBeCloned) { +TEST_F(DensifyCloneTest, InternalDesnifyCanBeCloned) { std::list<boost::intrusive_ptr<DocumentSource>> sources; sources.push_back(make_intrusive<DocumentSourceInternalDensify>( @@ -1439,174 +1283,5 @@ TEST(DensifyStepTest, InternalDensifyIsOffStepForDaysWithLargeDateStep) { ASSERT_FALSE(val.isOnStepRelativeTo(base, range)); } - -TEST_F(DensifyRedactionTest, RedactionDateBounds) { - auto spec = fromjson(R"({ - $densify: { - field: "a", - range: { - step: 1, - unit: "hour", - bounds: [ - {$date: "2023-04-23T00:00:00.000Z"}, - {$date: "2023-04-23T08:00:00.000Z"} - ] - } - } - })"); - - auto docSource = - DocumentSourceInternalDensify::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalDensify": { - "field": "HASH<a>", - "partitionByFields": [], - "range": { - "step": "?number", - "bounds": [ - "?date", - "?date" - ], - "unit": "hour" - } - } - })", - redact(*docSource)); -} - -TEST_F(DensifyRedactionTest, RedactionFullBoundsWithPartitionFields) { - auto spec = fromjson(R"({ - $densify: { - field: "foo", - partitionByFields: ["a", "b", "c.d"], - range: { - bounds: "full", - step: 100 - } - } - })"); - auto docSource = - DocumentSourceInternalDensify::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalDensify": { - "field": "HASH<foo>", - "partitionByFields": [ - "HASH<a>", - "HASH<b>", - "HASH<c>.HASH<d>" - ], - "range": { - "step": "?number", - "bounds": "full" - } - } - })", - redact(*docSource)); -} - -TEST_F(DensifyRedactionTest, RedactionPartitionBounds) { - auto spec = fromjson(R"({ - $densify: { - field: "x", - partitionByFields: ["foo"], - range: { - bounds: "partition", - step: 50, - unit: "second" - } - } - })"); - auto docSource = - DocumentSourceInternalDensify::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalDensify": { - "field": "HASH<x>", - "partitionByFields": [ - "HASH<foo>" - ], - "range": { - "step": "?number", - "bounds": "partition", - "unit": "second" - } - } - })", - redact(*docSource)); -} - -void assertRangeTimeUnitSerialization(const boost::intrusive_ptr<ExpressionContext>& expCtx, - BSONObj inputStage, - BSONObj expectedStage, - const SerializationOptions& opts) { - auto parsedStage = - DocumentSourceInternalDensify::createFromBson(inputStage.firstElement(), expCtx); - std::vector<Value> serialization; - parsedStage->serializeToArray(serialization, opts); - - auto serializedStage = serialization[0].getDocument().toBson(); - ASSERT_BSONOBJ_EQ(expectedStage, serializedStage); -} - -TEST_F(DensifyRedactionTest, RangeTimeUnitSerializationRepresentative) { - assertRangeTimeUnitSerialization( - getExpCtx(), - fromjson(R"({ - $densify: { - field: "x", - partitionByFields: ["foo"], - range: { - bounds: "partition", - step: 50, - unit: "second" - } - } - })"), - fromjson(R"({ - $_internalDensify: { - field: "x", - partitionByFields: [ - "foo" - ], - range: { - step: 1, - bounds: "partition", - unit: "second" - } - } - })"), - SerializationOptions::kRepresentativeQueryShapeSerializeOptions); -} - -TEST_F(DensifyRedactionTest, RangeTimeUnitSerializationDebug) { - assertRangeTimeUnitSerialization(getExpCtx(), - fromjson( - R"({ - $densify: { - field: "x", - partitionByFields: ["foo"], - range: { - bounds: "partition", - step: 50, - unit: "second" - } - } - })"), - fromjson( - R"({ - $_internalDensify: { - field: "x", - partitionByFields: ["foo"], - range: { - step: "?number", - bounds: "partition", - unit: "second" - } - } - })"), - SerializationOptions::kDebugQueryShapeSerializeOptions); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_documents.cpp b/src/mongo/db/pipeline/document_source_documents.cpp index 23f5b8c96f8..5d4e02c71ea 100644 --- a/src/mongo/db/pipeline/document_source_documents.cpp +++ b/src/mongo/db/pipeline/document_source_documents.cpp @@ -44,32 +44,32 @@ namespace mongo { using boost::intrusive_ptr; REGISTER_DOCUMENT_SOURCE(documents, - DocumentSourceDocuments::LiteParsed::parse, + LiteParsedDocumentSourceDefault::parse, DocumentSourceDocuments::createFromBson, AllowedWithApiStrict::kAlways); std::list<intrusive_ptr<DocumentSource>> DocumentSourceDocuments::createFromBson( BSONElement elem, const intrusive_ptr<ExpressionContext>& expCtx) { - // kGenFieldName is a temporary field to hold docs to wire $project, $unwind, and $replaceRoot - // together. This may show up in explain, but it will not possibly overlap with user data, since - // the user data is nested a level below, within an array or object. - auto projectContent = BSON(kGenFieldName << elem); + // genField is a temporary field to hold docs to wire $project, + // $unwind, and $replaceRoot together. + auto genField = UUID::gen().toString(); + auto projectContent = BSON(genField << elem); auto queue = DocumentSourceQueue::create(expCtx, DocumentSourceDocuments::kStageName); queue->emplace_back(Document{}); /* Create the following pipeline from $documents: [...] - * => [ {$queue: [{}] }, - * {$project: {[kGenFieldName]: [...]}}, - * {$unwind: "$" + kGenFieldName}, - * {$replaceWith: "$" + kGenFieldName} ] + * => [ queue([{}]), + * project: {tempDocumentsField: [...]}, + * unwind: "$tempDocumentsField", + * replaceWith: "$tempDocumentsField" ] */ - return {queue, - DocumentSourceProject::create(projectContent, expCtx, elem.fieldNameStringData()), - DocumentSourceUnwind::create(expCtx, kGenFieldName, false, {}, true), - DocumentSourceReplaceRoot::create( - expCtx, - ExpressionFieldPath::createPathFromString( - expCtx.get(), kGenFieldName, expCtx->variablesParseState), - "elements within the array passed to $documents")}; + return { + queue, + DocumentSourceProject::create(projectContent, expCtx, elem.fieldNameStringData()), + DocumentSourceUnwind::create(expCtx, genField, false, {}, true), + DocumentSourceReplaceRoot::create(expCtx, + ExpressionFieldPath::createPathFromString( + expCtx.get(), genField, expCtx->variablesParseState), + "elements within the array passed to $documents")}; } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_documents.h b/src/mongo/db/pipeline/document_source_documents.h index 992b6c6cc4e..a6ab70e0c59 100644 --- a/src/mongo/db/pipeline/document_source_documents.h +++ b/src/mongo/db/pipeline/document_source_documents.h @@ -35,39 +35,10 @@ namespace mongo { namespace DocumentSourceDocuments { -class LiteParsed : public LiteParsedDocumentSource { -public: - static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, const BSONElement& spec) { - return std::make_unique<LiteParsed>(spec.fieldName()); - } - LiteParsed(std::string parseTimeName) : LiteParsedDocumentSource(std::move(parseTimeName)) {} - - stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { - return stdx::unordered_set<NamespaceString>(); - } - - PrivilegeVector requiredPrivileges(bool isMongos, bool bypassDocumentValidation) const final { - return {}; - } - - bool isInitialSource() const final { - return true; - } - - bool isDocuments() const final { - return true; - } - - bool allowedToPassthroughFromMongos() const final { - return false; - } -}; - -static const std::string kGenFieldName = "_tempDocumentsField"s; static constexpr StringData kStageName = "$documents"_sd; -std::list<boost::intrusive_ptr<DocumentSource>> createFromBson( +static std::list<boost::intrusive_ptr<DocumentSource>> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); }; // namespace DocumentSourceDocuments diff --git a/src/mongo/db/pipeline/document_source_documents_test.cpp b/src/mongo/db/pipeline/document_source_documents_test.cpp deleted file mode 100644 index 3067df16abc..00000000000 --- a/src/mongo/db/pipeline/document_source_documents_test.cpp +++ /dev/null @@ -1,80 +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 "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/pipeline/document_source_documents.h" -#include "mongo/db/pipeline/document_source_unwind.h" - -namespace mongo { - -using DocumentSourceDocumentsTest = AggregationContextFixture; - -TEST_F(DocumentSourceDocumentsTest, DocumentsStageRedactsCorrectly) { - auto spec = fromjson(R"({ - $documents: [ - { x: 10 }, { x: 2 }, { x: 5 } - ] - })"); - auto docSourcesList = DocumentSourceDocuments::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_EQ(4, docSourcesList.size()); - - // We must retrieve the internally-generated field name shared across these stages in order to - // make sure they're serialized properly. - std::vector<boost::intrusive_ptr<DocumentSource>> docSourcesVec(docSourcesList.begin(), - docSourcesList.end()); - auto unwindStage = static_cast<DocumentSourceUnwind*>(docSourcesVec[2].get()); - ASSERT(unwindStage); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$queue":"?array<?object>"})", - redact(*docSourcesVec[0])); - - - ASSERT_BSONOBJ_EQ( // NOLINT - fromjson(R"({ - "$project": { - "HASH<_id>": true, - "HASH<)" + - // SERVER-87666 ensure that the generated field is consistent. - DocumentSourceDocuments::kGenFieldName + - R"(>": "?array<?object>" - } - })"), - redact(*docSourcesVec[1])); - - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - "{'$unwind': {'path' : '$HASH<" + DocumentSourceDocuments::kGenFieldName + ">' } }", - redact(*docSourcesVec[2])); - - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - "{$replaceRoot: {newRoot: '$HASH<" + DocumentSourceDocuments::kGenFieldName + ">'}}", - redact(*docSourcesVec[3])); -} - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_exchange.cpp b/src/mongo/db/pipeline/document_source_exchange.cpp index 441779021e9..931b78f316e 100644 --- a/src/mongo/db/pipeline/document_source_exchange.cpp +++ b/src/mongo/db/pipeline/document_source_exchange.cpp @@ -91,8 +91,8 @@ const char* DocumentSourceExchange::getSourceName() const { return kStageName.rawData(); } -Value DocumentSourceExchange::serialize(const SerializationOptions& opts) const { - return Value(DOC(getSourceName() << _exchange->getSpec().toBSON(opts))); +Value DocumentSourceExchange::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(getSourceName() << _exchange->getSpec().toBSON())); } DocumentSourceExchange::DocumentSourceExchange( diff --git a/src/mongo/db/pipeline/document_source_exchange.h b/src/mongo/db/pipeline/document_source_exchange.h index 544f6fb924f..dd2c00030de 100644 --- a/src/mongo/db/pipeline/document_source_exchange.h +++ b/src/mongo/db/pipeline/document_source_exchange.h @@ -221,7 +221,7 @@ public: const char* getSourceName() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; /** * DocumentSourceExchange does not have a direct source (it is reading through the shared diff --git a/src/mongo/db/pipeline/document_source_exchange_test.cpp b/src/mongo/db/pipeline/document_source_exchange_test.cpp index 4ed642c49c7..ac234357b17 100644 --- a/src/mongo/db/pipeline/document_source_exchange_test.cpp +++ b/src/mongo/db/pipeline/document_source_exchange_test.cpp @@ -747,30 +747,4 @@ TEST_F(DocumentSourceExchangeTest, RejectInvalidMissingKeys) { Exchange(parseSpec(spec), Pipeline::create({}, getExpCtx())), AssertionException, 50967); } -TEST_F(DocumentSourceExchangeTest, QueryShape) { - const size_t nDocs = 500; - - auto source = getMockSource(nDocs); - - ExchangeSpec spec; - spec.setPolicy(ExchangePolicyEnum::kRoundRobin); - spec.setConsumers(1); - spec.setBufferSize(1024); - boost::intrusive_ptr<Exchange> ex = new Exchange(spec, Pipeline::create({source}, getExpCtx())); - boost::intrusive_ptr<DocumentSourceExchange> stage = - new DocumentSourceExchange(getExpCtx(), ex, 0, nullptr); - - ASSERT_BSONOBJ_EQ_AUTO( // - R"({ - "$_internalExchange": { - "policy": "roundrobin", - "consumers": "?number", - "orderPreserving": false, - "bufferSize": "?number", - "key": "?object" - } - })", - redact(*stage)); -} - } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_facet.cpp b/src/mongo/db/pipeline/document_source_facet.cpp index 6dfe8fd0a68..a20d0a1eb0a 100644 --- a/src/mongo/db/pipeline/document_source_facet.cpp +++ b/src/mongo/db/pipeline/document_source_facet.cpp @@ -185,12 +185,11 @@ DocumentSource::GetNextResult DocumentSourceFacet::doGetNext() { return resultDoc.freeze(); } -Value DocumentSourceFacet::serialize(const SerializationOptions& opts) const { +Value DocumentSourceFacet::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument serialized; for (auto&& facet : _facets) { - serialized[opts.serializeFieldPathFromString(facet.name)] = - Value(opts.verbosity ? facet.pipeline->writeExplainOps(opts) - : facet.pipeline->serialize(opts)); + serialized[facet.name] = Value(explain ? facet.pipeline->writeExplainOps(*explain) + : facet.pipeline->serialize()); } return Value(Document{{"$facet", serialized.freezeToValue()}}); } @@ -223,13 +222,6 @@ void DocumentSourceFacet::reattachToOperationContext(OperationContext* opCtx) { } } -bool DocumentSourceFacet::validateOperationContext(const OperationContext* opCtx) const { - return getContext()->opCtx == opCtx && - std::all_of(_facets.begin(), _facets.end(), [opCtx](const auto& f) { - return f.pipeline->validateOperationContext(opCtx); - }); -} - StageConstraints DocumentSourceFacet::constraints(Pipeline::SplitState) const { // Currently we don't split $facet to have a merger part and a shards part (see SERVER-24154). // This means that if any stage in any of the $facet pipelines needs to run on the primary shard @@ -290,8 +282,6 @@ DepsTracker::State DocumentSourceFacet::getDependencies(DepsTracker* deps) const deps->vars.insert(subDepsTracker.vars.begin(), subDepsTracker.vars.end()); deps->needWholeDocument = deps->needWholeDocument || subDepsTracker.needWholeDocument; - // If the subpipeline needs any metadata, the top level pipeline must know to generate it. - deps->metadataDeps() |= subDepsTracker.metadataDeps(); // The text score is the only type of metadata that could be needed by $facet. deps->setNeedsMetadata( @@ -337,25 +327,20 @@ intrusive_ptr<DocumentSource> DocumentSourceFacet::createFromBson( }); }); - // These checks potentially require that we check the catalog to determine where our data - // lives. In circumstances where we aren't actually running the query, we don't need to do - // this (and it can erroneously error - SERVER-83912). - if (expCtx->mongoProcessInterface->isExpectedToExecuteQueries()) { - // Validate that none of the facet pipelines have any conflicting HostTypeRequirements. - // This verifies both that all stages within each pipeline are consistent, and that the - // pipelines are consistent with one another. - if (!needsShard && pipeline->needsShard()) { - needsShard.emplace(facetName); - } - if (!needsMongoS && pipeline->needsMongosMerger()) { - needsMongoS.emplace(facetName); - } - uassert(ErrorCodes::IllegalOperation, - str::stream() << "$facet pipeline '" << *needsMongoS - << "' must run on mongoS, but '" << *needsShard - << "' requires a shard", - !(needsShard && needsMongoS)); + // Validate that none of the facet pipelines have any conflicting HostTypeRequirements. This + // verifies both that all stages within each pipeline are consistent, and that the pipelines + // are consistent with one another. + if (!needsShard && pipeline->needsShard()) { + needsShard.emplace(facetName); + } + if (!needsMongoS && pipeline->needsMongosMerger()) { + needsMongoS.emplace(facetName); } + uassert(ErrorCodes::IllegalOperation, + str::stream() << "$facet pipeline '" << *needsMongoS + << "' must run on mongoS, but '" << *needsShard + << "' requires a shard", + !(needsShard && needsMongoS)); facetPipelines.emplace_back(facetName, std::move(pipeline)); } diff --git a/src/mongo/db/pipeline/document_source_facet.h b/src/mongo/db/pipeline/document_source_facet.h index e3efb9c37cd..f6368d90dc0 100644 --- a/src/mongo/db/pipeline/document_source_facet.h +++ b/src/mongo/db/pipeline/document_source_facet.h @@ -140,7 +140,6 @@ public: void addInvolvedCollections(stdx::unordered_set<NamespaceString>* involvedNssSet) const final; void detachFromOperationContext() final; void reattachToOperationContext(OperationContext* opCtx) final; - bool validateOperationContext(const OperationContext* opCtx) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final; bool usedDisk() final; const SpecificStats* getSpecificStats() const final { @@ -160,7 +159,7 @@ private: size_t bufferSizeBytes, size_t maxOutputDocBytes); - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; boost::intrusive_ptr<TeeBuffer> _teeBuffer; std::vector<FacetPipeline> _facets; diff --git a/src/mongo/db/pipeline/document_source_facet_test.cpp b/src/mongo/db/pipeline/document_source_facet_test.cpp index 78367b24b02..44e3d9668e9 100644 --- a/src/mongo/db/pipeline/document_source_facet_test.cpp +++ b/src/mongo/db/pipeline/document_source_facet_test.cpp @@ -186,19 +186,9 @@ TEST_F(DocumentSourceFacetTest, ShouldAcceptLegalSpecification) { ASSERT_TRUE(facetStage.get()); } -/* - * Override the stub interface to allow full execution in these tests. - */ -class ExecutableStubMongoProcessInterface : public StubMongoProcessInterface { - bool isExpectedToExecuteQueries() override { - return true; - } -}; - TEST_F(DocumentSourceFacetTest, ShouldRejectConflictingHostTypeRequirementsWithinSinglePipeline) { auto ctx = getExpCtx(); ctx->inMongos = true; - ctx->mongoProcessInterface = std::make_unique<ExecutableStubMongoProcessInterface>(); auto spec = fromjson( "{$facet: {badPipe: [{$_internalSplitPipeline: {mergeType: 'anyShard'}}, " @@ -212,7 +202,6 @@ TEST_F(DocumentSourceFacetTest, ShouldRejectConflictingHostTypeRequirementsWithi TEST_F(DocumentSourceFacetTest, ShouldRejectConflictingHostTypeRequirementsAcrossPipelines) { auto ctx = getExpCtx(); ctx->inMongos = true; - ctx->mongoProcessInterface = std::make_unique<ExecutableStubMongoProcessInterface>(); auto spec = fromjson( "{$facet: {shardPipe: [{$_internalSplitPipeline: {mergeType: 'anyShard'}}], mongosPipe: " @@ -915,165 +904,5 @@ TEST_F(DocumentSourceFacetTest, ShouldSurfaceStrictestRequirementsOfEachConstrai ASSERT_FALSE( facetStage->constraints(Pipeline::SplitState::kUnsplit).isAllowedInLookupPipeline()); } - -TEST_F(DocumentSourceFacetTest, RedactsCorrectly) { - auto spec = fromjson(R"({ - $facet: { - a: [ - { $unwind: "$foo" }, - { $sortByCount: "$foo" } - ], - b: [ - { - $match: { - bar: { $exists: 1 } - } - }, - { - $bucket: { - groupBy: "$bar.foo", - boundaries: [0, 50, 100, 200], - output: { - z: { $sum : 1 } - } - } - } - ], - c: [ - { - $bucketAuto: { - groupBy: "$bar.baz", - buckets: 4 - } - } - ] - } - })"); - auto docSource = DocumentSourceFacet::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$facet": { - "HASH<a>": [ - { - "$unwind": { - "path": "$HASH<foo>" - } - }, - { - "$group": { - "_id": "$HASH<foo>", - "HASH<count>": { - "$sum": "?number" - } - } - }, - { - "$sort": { - "HASH<count>": -1 - } - } - ], - "HASH<b>": [ - { - "$match": { - "HASH<bar>": { - "$exists": "?bool" - } - } - }, - { - "$group": { - "_id": { - "$switch": { - "branches": [ - { - "case": { - "$and": [ - { - "$gte": [ - "$HASH<bar>.HASH<foo>", - "?number" - ] - }, - { - "$lt": [ - "$HASH<bar>.HASH<foo>", - "?number" - ] - } - ] - }, - "then": "?number" - }, - { - "case": { - "$and": [ - { - "$gte": [ - "$HASH<bar>.HASH<foo>", - "?number" - ] - }, - { - "$lt": [ - "$HASH<bar>.HASH<foo>", - "?number" - ] - } - ] - }, - "then": "?number" - }, - { - "case": { - "$and": [ - { - "$gte": [ - "$HASH<bar>.HASH<foo>", - "?number" - ] - }, - { - "$lt": [ - "$HASH<bar>.HASH<foo>", - "?number" - ] - } - ] - }, - "then": "?number" - } - ] - } - }, - "HASH<z>": { - "$sum": "?number" - } - } - }, - { - "$sort": { - "HASH<_id>": 1 - } - } - ], - "HASH<c>": [ - { - "$bucketAuto": { - "groupBy": "$HASH<bar>.HASH<baz>", - "buckets": "?number", - "output": { - "HASH<count>": { - "$sum": "?number" - } - } - } - } - ] - } - })", - redact(*docSource)); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp index d98aed38f9b..2fc1208e2b9 100644 --- a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp @@ -177,12 +177,12 @@ StageConstraints DocumentSourceFindAndModifyImageLookup::constraints( ChangeStreamRequirement::kDenylist); } -Value DocumentSourceFindAndModifyImageLookup::serialize(const SerializationOptions& opts) const { +Value DocumentSourceFindAndModifyImageLookup::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { return Value( Document{{kStageName, Value(Document{{kIncludeCommitTransactionTimestampFieldName, - _includeCommitTransactionTimestamp ? opts.serializeLiteral(true) - : Value()}})}}); + _includeCommitTransactionTimestamp ? Value(true) : Value()}})}}); } DepsTracker::State DocumentSourceFindAndModifyImageLookup::getDependencies( @@ -199,7 +199,7 @@ DepsTracker::State DocumentSourceFindAndModifyImageLookup::getDependencies( } DocumentSource::GetModPathsReturn DocumentSourceFindAndModifyImageLookup::getModifiedPaths() const { - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; } DocumentSource::GetNextResult DocumentSourceFindAndModifyImageLookup::doGetNext() { diff --git a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.h b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.h index 9e554afcc2e..9fac2c429c4 100644 --- a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.h +++ b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.h @@ -60,7 +60,7 @@ public: DocumentSource::GetModPathsReturn getModifiedPaths() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const; StageConstraints constraints(Pipeline::SplitState pipeState) const final; diff --git a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup_test.cpp b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup_test.cpp index 843e414b5ba..94c20f0ca8e 100644 --- a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup_test.cpp +++ b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup_test.cpp @@ -74,7 +74,6 @@ repl::OplogEntry makeOplogEntry( nss, // namespace uuid, // uuid boost::none, // fromMigrate - boost::none, // checkExistenceForDiffInsert repl::OplogEntry::kOplogVersion, // version oField, // o boost::none, // o2 diff --git a/src/mongo/db/pipeline/document_source_geo_near.cpp b/src/mongo/db/pipeline/document_source_geo_near.cpp index dc4aa5f8be6..1ee840df794 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near.cpp @@ -57,45 +57,40 @@ REGISTER_DOCUMENT_SOURCE(geoNear, DocumentSourceGeoNear::createFromBson, AllowedWithApiStrict::kAlways); -Value DocumentSourceGeoNear::serialize(const SerializationOptions& opts) const { +Value DocumentSourceGeoNear::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument result; if (keyFieldPath) { - result.setField(kKeyFieldName, Value(opts.serializeFieldPath(*keyFieldPath))); + result.setField(kKeyFieldName, Value(keyFieldPath->fullPath())); } auto nearValue = [&]() -> Value { if (auto constGeometry = dynamic_cast<ExpressionConstant*>(_nearGeometry.get()); constGeometry) { - return opts.serializeLiteral(constGeometry->getValue()); + return constGeometry->getValue(); } else { - return _nearGeometry->serialize(opts); + return _nearGeometry->serialize(static_cast<bool>(explain)); } }(); result.setField("near", nearValue); - result.setField("distanceField", Value(opts.serializeFieldPath(*distanceField))); + result.setField("distanceField", Value(distanceField->fullPath())); if (maxDistance) { - result.setField("maxDistance", opts.serializeLiteral(*maxDistance)); + result.setField("maxDistance", Value(*maxDistance)); } if (minDistance) { - result.setField("minDistance", opts.serializeLiteral(*minDistance)); + result.setField("minDistance", Value(*minDistance)); } - if (opts.transformIdentifiers || opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) { - auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(query, pExpCtx)); - result.setField("query", Value(matchExpr->serialize(opts))); - } else { - result.setField("query", Value(query)); - } - result.setField("spherical", opts.serializeLiteral(spherical)); + result.setField("query", Value(query)); + result.setField("spherical", Value(spherical)); if (distanceMultiplier) { - result.setField("distanceMultiplier", opts.serializeLiteral(*distanceMultiplier)); + result.setField("distanceMultiplier", Value(*distanceMultiplier)); } if (includeLocs) - result.setField("includeLocs", Value(opts.serializeFieldPath(*includeLocs))); + result.setField("includeLocs", Value(includeLocs->fullPath())); return Value(DOC(getSourceName() << result.freeze())); } diff --git a/src/mongo/db/pipeline/document_source_geo_near.h b/src/mongo/db/pipeline/document_source_geo_near.h index e9fd04e5f2d..83ddd02bb88 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.h +++ b/src/mongo/db/pipeline/document_source_geo_near.h @@ -51,7 +51,7 @@ public: StageConstraints constraints(Pipeline::SplitState pipeState) const final { return {StreamType::kStreaming, - PositionRequirement::kCustom, + PositionRequirement::kFirstAfterOptimization, HostTypeRequirement::kAnyShard, DiskUseRequirement::kNoDiskUse, FacetRequirement::kNotAllowed, @@ -60,17 +60,6 @@ public: UnionRequirement::kAllowed}; } - void validatePipelinePosition(bool alreadyOptimized, - Pipeline::SourceContainer::const_iterator pos, - const Pipeline::SourceContainer& container) const final { - // This stage must be in the first position in the pipeline after optimization. - uassert(40603, - str::stream() << getSourceName() - << " was not the first stage in the pipeline after optimization. Is " - "optimization disabled or inhibited?", - !alreadyOptimized || pos == container.cbegin()); - } - /** * DocumentSourceGeoNear should always be replaced by a DocumentSourceGeoNearCursor before * executing a pipeline, so this method should never be called. @@ -79,7 +68,7 @@ public: MONGO_UNREACHABLE; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; boost::intrusive_ptr<DocumentSource> optimize() final; diff --git a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp index 8de9f300ab9..27e72886225 100644 --- a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp @@ -77,6 +77,9 @@ DocumentSourceGeoNearCursor::DocumentSourceGeoNearCursor( _distanceField(std::move(distanceField)), _locationField(std::move(locationField)), _distanceMultiplier(distanceMultiplier) { + tassert(6466203, + "$geoNear cursor shouldn't have secondary collections", + collections.getSecondaryCollections().empty()); invariant(_distanceMultiplier >= 0); } diff --git a/src/mongo/db/pipeline/document_source_geo_near_test.cpp b/src/mongo/db/pipeline/document_source_geo_near_test.cpp index 3ef5920c99e..cc3a510b84e 100644 --- a/src/mongo/db/pipeline/document_source_geo_near_test.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near_test.cpp @@ -111,98 +111,5 @@ TEST_F(DocumentSourceGeoNearTest, CanParseAndSerializeKeyField) { {"spherical", false}}}}}}; ASSERT_VALUE_EQ(expectedSerialization, serialized[0]); } - -TEST_F(DocumentSourceGeoNearTest, RedactionWithGeoJSONPoint) { - auto spec = fromjson(R"({ - $geoNear: { - distanceField: "a", - maxDistance: 2, - minDistance: 1, - near: { - type: "Point", - coordinates: [ -23.484, 28.3913 ] - }, - query: { foo : "bar" }, - spherical: true - } - })"); - auto docSource = DocumentSourceGeoNear::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$geoNear": { - "near": "?object", - "distanceField": "HASH<a>", - "maxDistance": "?number", - "minDistance": "?number", - "query": { - "HASH<foo>": { - "$eq": "?string" - } - }, - "spherical": "?bool" - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceGeoNearTest, RedactionWithGeoJSONLineString) { - auto spec = fromjson(R"({ - $geoNear: { - distanceField: "a", - near: { - type: "LineString", - coordinates: [[0,0], [-1,-1]] - }, - minDistance: 0.5 - } - })"); - auto docSource = DocumentSourceGeoNear::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$geoNear": { - "near": "?object", - "distanceField": "HASH<a>", - "minDistance": "?number", - "query": {}, - "spherical": "?bool" - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceGeoNearTest, RedactionWithLegacyCoordinates) { - auto spec = fromjson(R"({ - $geoNear: { - distanceField: "foo", - distanceMultiplier: 3.14, - includeLocs: "bar.baz", - near: [10, 10], - key: "z", - query: { - a : { $gt: 10 } - }, - spherical: false - } - })"); - auto docSource = DocumentSourceGeoNear::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$geoNear": { - "key": "HASH<z>", - "near": "?array<?number>", - "distanceField": "HASH<foo>", - "query": { - "HASH<a>": { - "$gt": "?number" - } - }, - "spherical": "?bool", - "distanceMultiplier": "?number", - "includeLocs": "HASH<bar>.HASH<baz>" - } - })", - redact(*docSource)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_graph_lookup.cpp b/src/mongo/db/pipeline/document_source_graph_lookup.cpp index 62a35123173..417e92df4b0 100644 --- a/src/mongo/db/pipeline/document_source_graph_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_graph_lookup.cpp @@ -507,7 +507,7 @@ void DocumentSourceGraphLookUp::performSearch() { } DocumentSource::GetModPathsReturn DocumentSourceGraphLookUp::getModifiedPaths() const { - OrderedPathSet modifiedPaths{_as.fullPath()}; + std::set<std::string> modifiedPaths{_as.fullPath()}; if (_unwind) { auto pathsModifiedByUnwind = _unwind.get()->getModifiedPaths(); invariant(pathsModifiedByUnwind.type == GetModPathsReturn::Type::kFiniteSet); @@ -544,56 +544,46 @@ void DocumentSourceGraphLookUp::checkMemoryUsage() { _cache.evictDownTo(_maxMemoryUsageBytes - _frontierUsageBytes - _visitedUsageBytes); } -void DocumentSourceGraphLookUp::serializeToArray(std::vector<Value>& array, - const SerializationOptions& opts) const { +void DocumentSourceGraphLookUp::serializeToArray( + std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { auto fromValue = (pExpCtx->ns.db() == _from.db()) - ? Value(opts.serializeIdentifier(_from.coll())) - : Value(Document{{"db", opts.serializeIdentifier(_from.db())}, - {"coll", opts.serializeIdentifier(_from.coll())}}); + ? Value(_from.coll()) + : Value(Document{{"db", _from.db()}, {"coll", _from.coll()}}); // Serialize default options. - MutableDocument spec(DOC("from" << fromValue << "as" << opts.serializeFieldPath(_as) - << "connectToField" << opts.serializeFieldPath(_connectToField) - << "connectFromField" - << opts.serializeFieldPath(_connectFromField) << "startWith" - << _startWith->serialize(opts))); + MutableDocument spec(DOC("from" << fromValue << "as" << _as.fullPath() << "connectToField" + << _connectToField.fullPath() << "connectFromField" + << _connectFromField.fullPath() << "startWith" + << _startWith->serialize(false))); // depthField is optional; serialize it if it was specified. if (_depthField) { - spec["depthField"] = Value(opts.serializeFieldPath(*_depthField)); + spec["depthField"] = Value(_depthField->fullPath()); } if (_maxDepth) { - spec["maxDepth"] = Value(opts.serializeLiteral(*_maxDepth)); + spec["maxDepth"] = Value(*_maxDepth); } if (_additionalFilter) { - if (opts.transformIdentifiers || - opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) { - auto matchExpr = - uassertStatusOK(MatchExpressionParser::parse(*_additionalFilter, pExpCtx)); - spec["restrictSearchWithMatch"] = Value(matchExpr->serialize(opts)); - } else { - spec["restrictSearchWithMatch"] = Value(*_additionalFilter); - } + spec["restrictSearchWithMatch"] = Value(*_additionalFilter); } // If we are explaining, include an absorbed $unwind inside the $graphLookup specification. - if (_unwind && opts.verbosity) { + if (_unwind && explain) { const boost::optional<FieldPath> indexPath = (*_unwind)->indexPath(); spec["unwinding"] = Value(DOC("preserveNullAndEmptyArrays" - << opts.serializeLiteral((*_unwind)->preserveNullAndEmptyArrays()) - << "includeArrayIndex" - << (indexPath ? Value(opts.serializeFieldPath(*indexPath)) : Value()))); + << (*_unwind)->preserveNullAndEmptyArrays() << "includeArrayIndex" + << (indexPath ? Value((*indexPath).fullPath()) : Value()))); } array.push_back(Value(DOC(getSourceName() << spec.freeze()))); // If we are not explaining, the output of this method must be parseable, so serialize our // $unwind into a separate stage. - if (_unwind && !opts.verbosity) { - (*_unwind)->serializeToArray(array, opts); + if (_unwind && !explain) { + (*_unwind)->serializeToArray(array); } } @@ -605,10 +595,6 @@ void DocumentSourceGraphLookUp::reattachToOperationContext(OperationContext* opC _fromExpCtx->opCtx = opCtx; } -bool DocumentSourceGraphLookUp::validateOperationContext(const OperationContext* opCtx) const { - return getContext()->opCtx == opCtx && _fromExpCtx->opCtx == opCtx; -} - DocumentSourceGraphLookUp::DocumentSourceGraphLookUp( const boost::intrusive_ptr<ExpressionContext>& expCtx, NamespaceString from, @@ -637,7 +623,6 @@ DocumentSourceGraphLookUp::DocumentSourceGraphLookUp( _variablesParseState(expCtx->variablesParseState.copyWith(_variables.useIdGenerator())) { const auto& resolvedNamespace = pExpCtx->getResolvedNamespace(_from); _fromExpCtx = pExpCtx->copyForSubPipeline(resolvedNamespace.ns, resolvedNamespace.uuid); - _fromExpCtx->inLookup = true; // We append an additional BSONObj to '_fromPipeline' as a placeholder for the $match stage // we'll eventually construct from the input document. @@ -649,7 +634,10 @@ DocumentSourceGraphLookUp::DocumentSourceGraphLookUp( DocumentSourceGraphLookUp::DocumentSourceGraphLookUp( const DocumentSourceGraphLookUp& original, const boost::intrusive_ptr<ExpressionContext>& newExpCtx) - : DocumentSource(kStageName, newExpCtx), + : DocumentSource( + kStageName, + newExpCtx ? newExpCtx + : original.pExpCtx->copyWith(original.pExpCtx->ns, original.pExpCtx->uuid)), _from(original._from), _as(original._as), _connectFromField(original._connectFromField), @@ -668,8 +656,7 @@ DocumentSourceGraphLookUp::DocumentSourceGraphLookUp( _variables(original._variables), _variablesParseState(original._variablesParseState.copyWith(_variables.useIdGenerator())) { if (original._unwind) { - _unwind = - static_cast<DocumentSourceUnwind*>(original._unwind.value()->clone(pExpCtx).get()); + _unwind = static_cast<DocumentSourceUnwind*>(original._unwind.get()->clone().get()); } } diff --git a/src/mongo/db/pipeline/document_source_graph_lookup.h b/src/mongo/db/pipeline/document_source_graph_lookup.h index 52134b61efe..bc0146f1829 100644 --- a/src/mongo/db/pipeline/document_source_graph_lookup.h +++ b/src/mongo/db/pipeline/document_source_graph_lookup.h @@ -97,7 +97,7 @@ public: void serializeToArray( std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const final override; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; /** * Returns the 'as' path, and possibly the fields modified by an absorbed $unwind. @@ -132,10 +132,6 @@ public: DepsTracker::State getDependencies(DepsTracker* deps) const final { _startWith->addDependencies(deps); - if (_additionalFilter) { - uassertStatusOK(MatchExpressionParser::parse(*_additionalFilter, _fromExpCtx)) - ->addDependencies(deps); - } return DepsTracker::State::SEE_NEXT; }; @@ -145,8 +141,6 @@ public: void reattachToOperationContext(OperationContext* opCtx) final; - bool validateOperationContext(const OperationContext* opCtx) const final; - static boost::intrusive_ptr<DocumentSourceGraphLookUp> create( const boost::intrusive_ptr<ExpressionContext>& expCtx, NamespaceString fromNs, @@ -188,10 +182,9 @@ private: boost::optional<long long> maxDepth, boost::optional<boost::intrusive_ptr<DocumentSourceUnwind>> unwindSrc); - Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final { // Should not be called; use serializeToArray instead. - MONGO_UNREACHABLE_TASSERT(7484306); + MONGO_UNREACHABLE; } /** diff --git a/src/mongo/db/pipeline/document_source_graph_lookup_test.cpp b/src/mongo/db/pipeline/document_source_graph_lookup_test.cpp index c5dd404a04e..26345aa16ab 100644 --- a/src/mongo/db/pipeline/document_source_graph_lookup_test.cpp +++ b/src/mongo/db/pipeline/document_source_graph_lookup_test.cpp @@ -713,126 +713,5 @@ TEST_F(DocumentSourceGraphLookUpTest, ShouldNotExpandArraysWithinArraysAtEndOfCo ASSERT(graphLookupStage->getNext().isEOF()); } -TEST_F(DocumentSourceGraphLookUpTest, RedactionStartWithSingleField) { - NamespaceString graphLookupNs(getExpCtx()->ns.db(), "coll"); - getExpCtx()->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {graphLookupNs.coll().toString(), {graphLookupNs, std::vector<BSONObj>()}}}); - - auto spec = fromjson(R"({ - "$graphLookup": { - "from": "coll", - "startWith": "$a.b", - "connectFromField": "c.d", - "connectToField": "e.f", - "as": "x", - "depthField": "y", - "maxDepth": 5, - "restrictSearchWithMatch": { - "foo": "abc", - "bar.baz": { "$gt": 5 } - } - } - })"); - auto docSource = DocumentSourceGraphLookUp::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$graphLookup": { - "from": "HASH<coll>", - "as": "HASH<x>", - "connectToField": "HASH<e>.HASH<f>", - "connectFromField": "HASH<c>.HASH<d>", - "startWith": "$HASH<a>.HASH<b>", - "depthField": "HASH<y>", - "maxDepth": "?number", - "restrictSearchWithMatch": { - "$and": [ - { - "HASH<foo>": { - "$eq": "?string" - } - }, - { - "HASH<bar>.HASH<baz>": { - "$gt": "?number" - } - } - ] - } - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceGraphLookUpTest, RedactionStartWithArrayOfFields) { - NamespaceString graphLookupNs(getExpCtx()->ns.db(), "coll"); - getExpCtx()->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {graphLookupNs.coll().toString(), {graphLookupNs, std::vector<BSONObj>()}}}); - - auto spec = fromjson(R"({ - $graphLookup: { - from: "coll", - startWith: ["$a.b", "$bar.baz"], - connectFromField: "x", - connectToField: "y", - as: "z" - } - })"); - auto docSource = DocumentSourceGraphLookUp::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$graphLookup": { - "from": "HASH<coll>", - "as": "HASH<z>", - "connectToField": "HASH<y>", - "connectFromField": "HASH<x>", - "startWith": ["$HASH<a>.HASH<b>", "$HASH<bar>.HASH<baz>"] - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceGraphLookUpTest, RedactionWithAbsorbedUnwind) { - auto expCtx = getExpCtx(); - - NamespaceString graphLookupNs(getExpCtx()->ns.db(), "coll"); - expCtx->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {graphLookupNs.coll().toString(), {graphLookupNs, std::vector<BSONObj>()}}}); - - auto unwindStage = DocumentSourceUnwind::create(expCtx, "results", false, boost::none); - auto graphLookupStage = DocumentSourceGraphLookUp::create( - getExpCtx(), - graphLookupNs, - "results", - "from", - "to", - ExpressionFieldPath::deprecatedCreate(expCtx.get(), "startPoint"), - boost::none, - boost::none, - boost::none, - unwindStage); - - auto serialized = redactToArray(*graphLookupStage); - ASSERT_EQ(2, serialized.size()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$graphLookup": { - "from": "HASH<coll>", - "as": "HASH<results>", - "connectToField": "HASH<to>", - "connectFromField": "HASH<from>", - "startWith": "$HASH<startPoint>" - } - })", - serialized[0].getDocument().toBson()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$unwind": { - path: "$HASH<results>" - } - })", - serialized[1].getDocument().toBson()); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_group.cpp b/src/mongo/db/pipeline/document_source_group.cpp index f437debe890..3f718360838 100644 --- a/src/mongo/db/pipeline/document_source_group.cpp +++ b/src/mongo/db/pipeline/document_source_group.cpp @@ -45,6 +45,82 @@ namespace mongo { +namespace { + +/** + * Generates a new file name on each call using a static, atomic and monotonically increasing + * number. + * + * Each user of the Sorter must implement this function to ensure that all temporary files that the + * Sorter instances produce are uniquely identified using a unique file name extension with separate + * atomic variable. This is necessary because the sorter.cpp code is separately included in multiple + * places, rather than compiled in one place and linked, and so cannot provide a globally unique ID. + */ +std::string nextFileName() { + static AtomicWord<unsigned> documentSourceGroupFileCounter; + return "extsort-doc-group." + std::to_string(documentSourceGroupFileCounter.fetchAndAdd(1)); +} + +} // namespace + +using boost::intrusive_ptr; +using std::pair; +using std::shared_ptr; +using std::vector; + +Document GroupFromFirstDocumentTransformation::applyTransformation(const Document& input) { + MutableDocument output(_accumulatorExprs.size()); + + for (auto&& expr : _accumulatorExprs) { + auto value = expr.second->evaluate(input, &expr.second->getExpressionContext()->variables); + output.addField(expr.first, value.missing() ? Value(BSONNULL) : std::move(value)); + } + + return output.freeze(); +} + +void GroupFromFirstDocumentTransformation::optimize() { + for (auto&& expr : _accumulatorExprs) { + expr.second = expr.second->optimize(); + } +} + +Document GroupFromFirstDocumentTransformation::serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const { + + MutableDocument newRoot(_accumulatorExprs.size()); + for (auto&& expr : _accumulatorExprs) { + newRoot.addField(expr.first, expr.second->serialize(static_cast<bool>(explain))); + } + + return {{"newRoot", newRoot.freezeToValue()}}; +} + +DepsTracker::State GroupFromFirstDocumentTransformation::addDependencies(DepsTracker* deps) const { + for (auto&& expr : _accumulatorExprs) { + expr.second->addDependencies(deps); + } + + // This stage will replace the entire document with a new document, so any existing fields + // will be replaced and cannot be required as dependencies. We use EXHAUSTIVE_ALL here + // instead of EXHAUSTIVE_FIELDS, as in ReplaceRootTransformation, because the stages that + // follow a $group stage should not depend on document metadata. + return DepsTracker::State::EXHAUSTIVE_ALL; +} + +DocumentSource::GetModPathsReturn GroupFromFirstDocumentTransformation::getModifiedPaths() const { + // Replaces the entire root, so all paths are modified. + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; +} + +std::unique_ptr<GroupFromFirstDocumentTransformation> GroupFromFirstDocumentTransformation::create( + const intrusive_ptr<ExpressionContext>& expCtx, + const std::string& groupId, + vector<pair<std::string, intrusive_ptr<Expression>>> accumulatorExprs) { + return std::make_unique<GroupFromFirstDocumentTransformation>(groupId, + std::move(accumulatorExprs)); +} + constexpr StringData DocumentSourceGroup::kStageName; REGISTER_DOCUMENT_SOURCE(group, @@ -56,70 +132,449 @@ const char* DocumentSourceGroup::getSourceName() const { return kStageName.rawData(); } -boost::intrusive_ptr<DocumentSourceGroup> DocumentSourceGroup::create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, +bool DocumentSourceGroup::shouldSpillWithAttemptToSaveMemory() { + if (!_memoryTracker._allowDiskUse && + (_memoryTracker.currentMemoryBytes() > + static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes))) { + freeMemory(); + } + + if (_memoryTracker.currentMemoryBytes() > + static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes)) { + uassert(ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed, + "Exceeded memory limit for $group, but didn't allow external sort." + " Pass allowDiskUse:true to opt in.", + _memoryTracker._allowDiskUse); + _memoryTracker.resetCurrent(); + return true; + } + return false; +} + +void DocumentSourceGroup::freeMemory() { + invariant(_groups); + for (auto&& group : *_groups) { + for (size_t i = 0; i < group.second.size(); i++) { + // Subtract the current usage. + _memoryTracker.update(_accumulatedFields[i].fieldName, + -1 * group.second[i]->getMemUsage()); + + group.second[i]->reduceMemoryConsumptionIfAble(); + + // Update the memory usage for this AccumulationStatement. + _memoryTracker.update(_accumulatedFields[i].fieldName, group.second[i]->getMemUsage()); + } + } +} + +DocumentSource::GetNextResult DocumentSourceGroup::doGetNext() { + if (!_initialized) { + const auto initializationResult = initialize(); + if (initializationResult.isPaused()) { + return initializationResult; + } + invariant(initializationResult.isEOF()); + } + + for (auto&& accum : _currentAccumulators) { + accum->reset(); // Prep accumulators for a new group. + } + + if (_spilled) { + return getNextSpilled(); + } else { + return getNextStandard(); + } +} + +DocumentSource::GetNextResult DocumentSourceGroup::getNextSpilled() { + // We aren't streaming, and we have spilled to disk. + if (!_sorterIterator) + return GetNextResult::makeEOF(); + + _currentId = _firstPartOfNextGroup.first; + const size_t numAccumulators = _accumulatedFields.size(); + + // Call startNewGroup on every accumulator. + Value expandedId = expandId(_currentId); + Document idDoc = + expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); + for (size_t i = 0; i < numAccumulators; ++i) { + Value initializerValue = + _accumulatedFields[i].expr.initializer->evaluate(idDoc, &pExpCtx->variables); + _currentAccumulators[i]->startNewGroup(initializerValue); + } + + while (pExpCtx->getValueComparator().evaluate(_currentId == _firstPartOfNextGroup.first)) { + // Inside of this loop, _firstPartOfNextGroup is the current data being processed. + // At loop exit, it is the first value to be processed in the next group. + switch (numAccumulators) { // mirrors switch in spill() + case 1: // Single accumulators serialize as a single Value. + _currentAccumulators[0]->process(_firstPartOfNextGroup.second, true); + case 0: // No accumulators so no Values. + break; + default: { // Multiple accumulators serialize as an array of Values. + const vector<Value>& accumulatorStates = _firstPartOfNextGroup.second.getArray(); + for (size_t i = 0; i < numAccumulators; i++) { + _currentAccumulators[i]->process(accumulatorStates[i], true); + } + } + } + + if (!_sorterIterator->more()) { + dispose(); + break; + } + + _firstPartOfNextGroup = _sorterIterator->next(); + } + + return makeDocument(_currentId, _currentAccumulators, pExpCtx->needsMerge); +} + +DocumentSource::GetNextResult DocumentSourceGroup::getNextStandard() { + // Not spilled, and not streaming. + if (_groups->empty()) + return GetNextResult::makeEOF(); + + Document out = makeDocument(groupsIterator->first, groupsIterator->second, pExpCtx->needsMerge); + + if (++groupsIterator == _groups->end()) + dispose(); + + return out; +} + +void DocumentSourceGroup::doDispose() { + // Free our resources. + _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); + _sorterIterator.reset(); + + // Make us look done. + groupsIterator = _groups->end(); +} + +intrusive_ptr<DocumentSource> DocumentSourceGroup::optimize() { + // Optimizing a 'DocumentSourceGroup' might modify its expressions to become incompatible with + // SBE. We temporarily highjack the context's 'sbeCompatible' flag to communicate the situation + // back to the 'DocumentSourceGroup'. Notice, that while a particular 'DocumentSourceGroup' + // might become incompatible with SBE, other groups in the pipeline and the collection access + // could be still eligible for lowering to SBE, thus we must reset the context's 'sbeCompatible' + // flag back to its original value at the end of the 'optimize()' call. + // + // TODO SERVER-XXXXX: replace this hack with a proper per-stage tracking of SBE compatibility. + auto expCtx = _idExpressions[0]->getExpressionContext(); + auto orgSbeCompatible = expCtx->sbeCompatible; + expCtx->sbeCompatible = true; + + // TODO: If all _idExpressions are ExpressionConstants after optimization, then we know there + // will be only one group. We should take advantage of that to avoid going through the hash + // table. + for (size_t i = 0; i < _idExpressions.size(); i++) { + _idExpressions[i] = _idExpressions[i]->optimize(); + } + + for (auto&& accumulatedField : _accumulatedFields) { + accumulatedField.expr.initializer = accumulatedField.expr.initializer->optimize(); + accumulatedField.expr.argument = accumulatedField.expr.argument->optimize(); + } + + _sbeCompatible = _sbeCompatible && expCtx->sbeCompatible; + expCtx->sbeCompatible = orgSbeCompatible; + + return this; +} + +Value DocumentSourceGroup::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + MutableDocument insides; + + // Add the _id. + if (_idFieldNames.empty()) { + invariant(_idExpressions.size() == 1); + insides["_id"] = _idExpressions[0]->serialize(static_cast<bool>(explain)); + } else { + // Decomposed document case. + invariant(_idExpressions.size() == _idFieldNames.size()); + MutableDocument md; + for (size_t i = 0; i < _idExpressions.size(); i++) { + md[_idFieldNames[i]] = _idExpressions[i]->serialize(static_cast<bool>(explain)); + } + insides["_id"] = md.freezeToValue(); + } + + // Add the remaining fields. + for (auto&& accumulatedField : _accumulatedFields) { + intrusive_ptr<AccumulatorState> accum = accumulatedField.makeAccumulator(); + insides[accumulatedField.fieldName] = + Value(accum->serialize(accumulatedField.expr.initializer, + accumulatedField.expr.argument, + static_cast<bool>(explain))); + } + + if (_doingMerge) { + // This makes the output unparsable (with error) on pre 2.6 shards, but it will never + // be sent to old shards when this flag is true since they can't do a merge anyway. + insides["$doingMerge"] = Value(true); + } + + MutableDocument out; + out[getSourceName()] = Value(insides.freeze()); + + if (explain && *explain >= ExplainOptions::Verbosity::kExecStats) { + MutableDocument md; + + for (size_t i = 0; i < _accumulatedFields.size(); i++) { + md[_accumulatedFields[i].fieldName] = Value(static_cast<long long>( + _memoryTracker[_accumulatedFields[i].fieldName].maxMemoryBytes())); + } + + out["maxAccumulatorMemoryUsageBytes"] = Value(md.freezeToValue()); + out["totalOutputDataSizeBytes"] = + Value(static_cast<long long>(_stats.totalOutputDataSizeBytes)); + out["usedDisk"] = Value(_stats.spills > 0); + out["spills"] = Value(static_cast<long long>(_stats.spills)); + } + + return Value(out.freezeToValue()); +} + +DepsTracker::State DocumentSourceGroup::getDependencies(DepsTracker* deps) const { + // add the _id + for (size_t i = 0; i < _idExpressions.size(); i++) { + _idExpressions[i]->addDependencies(deps); + } + + // add the rest + for (auto&& accumulatedField : _accumulatedFields) { + accumulatedField.expr.argument->addDependencies(deps); + // Don't add initializer, because it doesn't refer to docs from the input stream. + } + + return DepsTracker::State::EXHAUSTIVE_ALL; +} + +DocumentSource::GetModPathsReturn DocumentSourceGroup::getModifiedPaths() const { + // We preserve none of the fields, but any fields referenced as part of the group key are + // logically just renamed. + StringMap<std::string> renames; + for (std::size_t i = 0; i < _idExpressions.size(); ++i) { + auto idExp = _idExpressions[i]; + auto pathToPutResultOfExpression = + _idFieldNames.empty() ? "_id" : "_id." + _idFieldNames[i]; + auto computedPaths = idExp->getComputedPaths(pathToPutResultOfExpression); + for (auto&& rename : computedPaths.renames) { + renames[rename.first] = rename.second; + } + } + + return {DocumentSource::GetModPathsReturn::Type::kAllExcept, + std::set<std::string>{}, // No fields are preserved. + std::move(renames)}; +} + +StringMap<boost::intrusive_ptr<Expression>> DocumentSourceGroup::getIdFields() const { + if (_idFieldNames.empty()) { + invariant(_idExpressions.size() == 1); + return {{"_id", _idExpressions[0]}}; + } else { + invariant(_idFieldNames.size() == _idExpressions.size()); + StringMap<boost::intrusive_ptr<Expression>> result; + for (std::size_t i = 0; i < _idFieldNames.size(); ++i) { + result["_id." + _idFieldNames[i]] = _idExpressions[i]; + } + return result; + } +} + +const std::vector<AccumulationStatement>& DocumentSourceGroup::getAccumulatedFields() const { + return _accumulatedFields; +} + +intrusive_ptr<DocumentSourceGroup> DocumentSourceGroup::create( + const intrusive_ptr<ExpressionContext>& expCtx, const boost::intrusive_ptr<Expression>& groupByExpression, std::vector<AccumulationStatement> accumulationStatements, boost::optional<size_t> maxMemoryUsageBytes) { - boost::intrusive_ptr<DocumentSourceGroup> groupStage = - new DocumentSourceGroup(expCtx, maxMemoryUsageBytes); + intrusive_ptr<DocumentSourceGroup> groupStage( + new DocumentSourceGroup(expCtx, maxMemoryUsageBytes)); groupStage->setIdExpression(groupByExpression); for (auto&& statement : accumulationStatements) { groupStage->addAccumulator(statement); + groupStage->_memoryTracker.set(statement.fieldName, 0); } return groupStage; } -DocumentSourceGroup::DocumentSourceGroup(const boost::intrusive_ptr<ExpressionContext>& expCtx, +DocumentSourceGroup::DocumentSourceGroup(const intrusive_ptr<ExpressionContext>& expCtx, boost::optional<size_t> maxMemoryUsageBytes) - : DocumentSourceGroupBase(kStageName, expCtx, maxMemoryUsageBytes), _groupsReady(false) {} + : DocumentSource(kStageName, expCtx), + _doingMerge(false), + _memoryTracker{expCtx->allowDiskUse && !expCtx->inMongos, + maxMemoryUsageBytes + ? *maxMemoryUsageBytes + : static_cast<size_t>(internalDocumentSourceGroupMaxMemoryBytes.load())}, + _initialized(false), + _groups(expCtx->getValueComparator().makeUnorderedValueMap<Accumulators>()), + _spilled(false), + _sbeCompatible(false) {} -boost::intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - return createFromBsonWithMaxMemoryUsage(std::move(elem), expCtx, boost::none); +void DocumentSourceGroup::addAccumulator(AccumulationStatement accumulationStatement) { + _accumulatedFields.push_back(accumulationStatement); } -boost::intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBsonWithMaxMemoryUsage( - BSONElement elem, - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes) { - boost::intrusive_ptr<DocumentSourceGroup> groupStage( - new DocumentSourceGroup(expCtx, maxMemoryUsageBytes)); - groupStage->initializeFromBson(elem); - return groupStage; +namespace { + +intrusive_ptr<Expression> parseIdExpression(const intrusive_ptr<ExpressionContext>& expCtx, + BSONElement groupField, + const VariablesParseState& vps) { + if (groupField.type() == Object) { + // {_id: {}} is treated as grouping on a constant, not an expression + if (groupField.Obj().isEmpty()) { + return ExpressionConstant::create(expCtx.get(), Value(groupField)); + } + + const BSONObj idKeyObj = groupField.Obj(); + if (idKeyObj.firstElementFieldName()[0] == '$') { + // grouping on a $op expression + return Expression::parseObject(expCtx.get(), idKeyObj, vps); + } else { + for (auto&& field : idKeyObj) { + uassert(17390, + "$group does not support inclusion-style expressions", + !field.isNumber() && field.type() != Bool); + } + return ExpressionObject::parse(expCtx.get(), idKeyObj, vps); + } + } else { + return Expression::parseOperand(expCtx.get(), groupField, vps); + } } -DocumentSource::GetNextResult DocumentSourceGroup::doGetNext() { - if (!_groupsReady) { - const auto initializationResult = performBlockingGroup(); - if (initializationResult.isPaused()) { - return initializationResult; +} // namespace + +void DocumentSourceGroup::setIdExpression(const boost::intrusive_ptr<Expression> idExpression) { + if (auto object = dynamic_cast<ExpressionObject*>(idExpression.get())) { + auto& childExpressions = object->getChildExpressions(); + invariant(!childExpressions.empty()); // We expect to have converted an empty object into a + // constant expression. + + // grouping on an "artificial" object. Rather than create the object for each input + // in initialize(), instead group on the output of the raw expressions. The artificial + // object will be created at the end in makeDocument() while outputting results. + for (auto&& childExpPair : childExpressions) { + _idFieldNames.push_back(childExpPair.first); + _idExpressions.push_back(childExpPair.second); } - invariant(initializationResult.isEOF()); + } else { + _idExpressions.push_back(idExpression); } +} - auto result = getNextReadyGroup(); - if (result.isEOF()) { - dispose(); +boost::intrusive_ptr<Expression> DocumentSourceGroup::getIdExpression() const { + // _idFieldNames is empty and _idExpressions has one element when the _id expression is not an + // object expression. + if (_idFieldNames.empty() && _idExpressions.size() == 1) { + return _idExpressions[0]; + } + + tassert(6586300, + "Field and its expression must be always paired in ExpressionObject", + _idFieldNames.size() > 0 && _idFieldNames.size() == _idExpressions.size()); + + // Each expression in '_idExpressions' may have been optimized and so, compose the object _id + // expression out of the optimized expressions. + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fieldsAndExprs; + for (size_t i = 0; i < _idExpressions.size(); ++i) { + fieldsAndExprs.emplace_back(_idFieldNames[i], _idExpressions[i]); } - return result; + + return ExpressionObject::create(_idExpressions[0]->getExpressionContext(), + std::move(fieldsAndExprs)); } -DocumentSource::GetNextResult DocumentSourceGroup::performBlockingGroup() { +intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBson( + BSONElement elem, const intrusive_ptr<ExpressionContext>& expCtx) { + uassert(15947, "a group's fields must be specified in an object", elem.type() == Object); + + intrusive_ptr<DocumentSourceGroup> groupStage(new DocumentSourceGroup(expCtx)); + + BSONObj groupObj(elem.Obj()); + BSONObjIterator groupIterator(groupObj); + VariablesParseState vps = expCtx->variablesParseState; + expCtx->sbeGroupCompatible = true; + while (groupIterator.more()) { + BSONElement groupField(groupIterator.next()); + StringData pFieldName = groupField.fieldNameStringData(); + if (pFieldName == "_id") { + uassert(15948, + "a group's _id may only be specified once", + groupStage->_idExpressions.empty()); + groupStage->setIdExpression(parseIdExpression(expCtx, groupField, vps)); + invariant(!groupStage->_idExpressions.empty()); + } else if (pFieldName == "$doingMerge") { + massert(17030, "$doingMerge should be true if present", groupField.Bool()); + + groupStage->setDoingMerge(true); + } else { + // Any other field will be treated as an accumulator specification. + groupStage->addAccumulator( + AccumulationStatement::parseAccumulationStatement(expCtx.get(), groupField, vps)); + groupStage->_memoryTracker.set(pFieldName, 0); + } + } + groupStage->_sbeCompatible = expCtx->sbeGroupCompatible && expCtx->sbeCompatible; + + uassert( + 15955, "a group specification must include an _id", !groupStage->_idExpressions.empty()); + return groupStage; +} + +namespace { + +using GroupsMap = DocumentSourceGroup::GroupsMap; + +class SorterComparator { +public: + typedef pair<Value, Value> Data; + + SorterComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} + + int operator()(const Data& lhs, const Data& rhs) const { + return _valueComparator.compare(lhs.first, rhs.first); + } + +private: + ValueComparator _valueComparator; +}; + +class SpillSTLComparator { +public: + SpillSTLComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} + + bool operator()(const GroupsMap::value_type* lhs, const GroupsMap::value_type* rhs) const { + return _valueComparator.evaluate(lhs->first < rhs->first); + } + +private: + ValueComparator _valueComparator; +}; +} // namespace + +DocumentSource::GetNextResult DocumentSourceGroup::initialize() { GetNextResult input = pSource->getNext(); - return performBlockingGroupSelf(input); + return initializeSelf(input); } -// This separate NOINLINE function is used here to decrease stack utilization of -// performBlockingGroup() and prevent stack overflows. -MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::performBlockingGroupSelf( +// This separate NOINLINE function is used here to decrease stack utilization of initialize() and +// prevent stack overflows. +MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initializeSelf( GetNextResult input) { - setExecutionStarted(); + const size_t numAccumulators = _accumulatedFields.size(); // Barring any pausing, this loop exhausts 'pSource' and populates '_groups'. for (; input.isAdvanced(); input = pSource->getNext()) { if (shouldSpillWithAttemptToSaveMemory()) { - spill(); + _sortedFiles.push_back(spill()); } // We release the result document here so that it does not outlive the end of this loop @@ -127,7 +582,58 @@ MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::perfo auto rootDocument = input.releaseDocument(); Value id = computeId(rootDocument); - processDocument(id, rootDocument); + // Look for the _id value in the map. If it's not there, add a new entry with a blank + // accumulator. This is done in a somewhat odd way in order to avoid hashing 'id' and + // looking it up in '_groups' multiple times. + const size_t oldSize = _groups->size(); + vector<intrusive_ptr<AccumulatorState>>& group = (*_groups)[id]; + const bool inserted = _groups->size() != oldSize; + + vector<uint64_t> oldAccumMemUsage(numAccumulators, 0); + if (inserted) { + _memoryTracker.set(_memoryTracker.currentMemoryBytes() + id.getApproximateSize()); + + // Initialize and add the accumulators + Value expandedId = expandId(id); + Document idDoc = + expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); + group.reserve(numAccumulators); + for (auto&& accumulatedField : _accumulatedFields) { + auto accum = accumulatedField.makeAccumulator(); + Value initializerValue = + accumulatedField.expr.initializer->evaluate(idDoc, &pExpCtx->variables); + accum->startNewGroup(initializerValue); + group.push_back(accum); + } + } + + /* tickle all the accumulators for the group we found */ + dassert(numAccumulators == group.size()); + + for (size_t i = 0; i < numAccumulators; i++) { + // Only process the input and update the memory footprint if the current accumulator + // needs more input. + if (group[i]->needsInput()) { + const auto prevMemUsage = inserted ? 0 : group[i]->getMemUsage(); + group[i]->process(_accumulatedFields[i].expr.argument->evaluate( + rootDocument, &pExpCtx->variables), + _doingMerge); + _memoryTracker.update(_accumulatedFields[i].fieldName, + group[i]->getMemUsage() - prevMemUsage); + } + } + + if (kDebugBuild && !storageGlobalParams.readOnly) { + // In debug mode, spill every time we have a duplicate id to stress merge logic. + if (!inserted && // is a dup + !pExpCtx->inMongos && // can't spill to disk in mongos + !_memoryTracker + ._allowDiskUse && // don't change behavior when testing external sort + _sortedFiles.size() < 20) { // don't open too many FDs + + _sortedFiles.push_back(spill()); + } + } } switch (input.getStatus()) { @@ -138,14 +644,280 @@ MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::perfo return input; // Propagate pause. } case DocumentSource::GetNextResult::ReturnStatus::kEOF: { - readyGroups(); + // Do any final steps necessary to prepare to output results. + if (!_sortedFiles.empty()) { + _spilled = true; + if (!_groups->empty()) { + _sortedFiles.push_back(spill()); + } + + // We won't be using groups again so free its memory. + _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); + + _sorterIterator.reset(Sorter<Value, Value>::Iterator::merge( + _sortedFiles, SortOptions(), SorterComparator(pExpCtx->getValueComparator()))); + + // prepare current to accumulate data + _currentAccumulators.reserve(numAccumulators); + for (auto&& accumulatedField : _accumulatedFields) { + _currentAccumulators.push_back(accumulatedField.makeAccumulator()); + } + + verify(_sorterIterator->more()); // we put data in, we should get something out. + _firstPartOfNextGroup = _sorterIterator->next(); + } else { + // start the group iterator + groupsIterator = _groups->begin(); + } + // This must happen last so that, unless control gets here, we will re-enter // initialization after getting a GetNextResult::ResultState::kPauseExecution. - _groupsReady = true; + _initialized = true; return input; } } MONGO_UNREACHABLE; } +shared_ptr<Sorter<Value, Value>::Iterator> DocumentSourceGroup::spill() { + _stats.spills++; + + vector<const GroupsMap::value_type*> ptrs; // using pointers to speed sorting + ptrs.reserve(_groups->size()); + for (GroupsMap::const_iterator it = _groups->begin(), end = _groups->end(); it != end; ++it) { + ptrs.push_back(&*it); + } + + stable_sort(ptrs.begin(), ptrs.end(), SpillSTLComparator(pExpCtx->getValueComparator())); + + // Initialize '_file' in a lazy manner only when it is needed. + if (!_file) { + _file = + std::make_shared<Sorter<Value, Value>::File>(pExpCtx->tempDir + "/" + nextFileName()); + } + SortedFileWriter<Value, Value> writer(SortOptions().TempDir(pExpCtx->tempDir), _file); + switch (_accumulatedFields.size()) { // same as ptrs[i]->second.size() for all i. + case 0: // no values, essentially a distinct + for (size_t i = 0; i < ptrs.size(); i++) { + writer.addAlreadySorted(ptrs[i]->first, Value()); + } + break; + + case 1: // just one value, use optimized serialization as single Value + for (size_t i = 0; i < ptrs.size(); i++) { + writer.addAlreadySorted(ptrs[i]->first, + ptrs[i]->second[0]->getValue(/*toBeMerged=*/true)); + } + break; + + default: // multiple values, serialize as array-typed Value + for (size_t i = 0; i < ptrs.size(); i++) { + vector<Value> accums; + for (size_t j = 0; j < ptrs[i]->second.size(); j++) { + accums.push_back(ptrs[i]->second[j]->getValue(/*toBeMerged=*/true)); + } + writer.addAlreadySorted(ptrs[i]->first, Value(std::move(accums))); + } + break; + } + + auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx); + metricsCollector.incrementKeysSorted(ptrs.size()); + metricsCollector.incrementSorterSpills(1); + + _groups->clear(); + // Zero out the current per-accumulation statement memory consumption, as the memory has been + // freed by spilling. + for (auto accum : _accumulatedFields) { + _memoryTracker.set(accum.fieldName, 0); + } + + Sorter<Value, Value>::Iterator* iteratorPtr = writer.done(); + return shared_ptr<Sorter<Value, Value>::Iterator>(iteratorPtr); +} + +Value DocumentSourceGroup::computeId(const Document& root) { + // If only one expression, return result directly + if (_idExpressions.size() == 1) { + Value retValue = _idExpressions[0]->evaluate(root, &pExpCtx->variables); + return retValue.missing() ? Value(BSONNULL) : std::move(retValue); + } + + // Multiple expressions get results wrapped in a vector + vector<Value> vals; + vals.reserve(_idExpressions.size()); + for (size_t i = 0; i < _idExpressions.size(); i++) { + vals.push_back(_idExpressions[i]->evaluate(root, &pExpCtx->variables)); + } + return Value(std::move(vals)); +} + +Value DocumentSourceGroup::expandId(const Value& val) { + // _id doesn't get wrapped in a document + if (_idFieldNames.empty()) + return val; + + // _id is a single-field document containing val + if (_idFieldNames.size() == 1) + return Value(DOC(_idFieldNames[0] << val)); + + // _id is a multi-field document containing the elements of val + const vector<Value>& vals = val.getArray(); + invariant(_idFieldNames.size() == vals.size()); + MutableDocument md(vals.size()); + for (size_t i = 0; i < vals.size(); i++) { + md[_idFieldNames[i]] = vals[i]; + } + return md.freezeToValue(); +} + +Document DocumentSourceGroup::makeDocument(const Value& id, + const Accumulators& accums, + bool mergeableOutput) { + const size_t n = _accumulatedFields.size(); + MutableDocument out(1 + n); + + /* add the _id field */ + out.addField("_id", expandId(id)); + + /* add the rest of the fields */ + for (size_t i = 0; i < n; ++i) { + Value val = accums[i]->getValue(mergeableOutput); + if (val.missing()) { + // we return null in this case so return objects are predictable + out.addField(_accumulatedFields[i].fieldName, Value(BSONNULL)); + } else { + out.addField(_accumulatedFields[i].fieldName, std::move(val)); + } + } + + _stats.totalOutputDataSizeBytes += out.getApproximateSize(); + return out.freeze(); +} + +boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceGroup::distributedPlanLogic() { + intrusive_ptr<DocumentSourceGroup> mergingGroup(new DocumentSourceGroup(pExpCtx)); + mergingGroup->setDoingMerge(true); + + VariablesParseState vps = pExpCtx->variablesParseState; + /* the merger will use the same grouping key */ + mergingGroup->setIdExpression(ExpressionFieldPath::parse(pExpCtx.get(), "$$ROOT._id", vps)); + + for (auto&& accumulatedField : _accumulatedFields) { + // The merger's output field names will be the same, as will the accumulator factories. + // However, for some accumulators, the expression to be accumulated will be different. The + // original accumulator may be collecting an expression based on a field expression or + // constant. Here, we accumulate the output of the same name from the prior group. + auto copiedAccumulatedField = accumulatedField; + copiedAccumulatedField.expr.argument = ExpressionFieldPath::parse( + pExpCtx.get(), "$$ROOT." + copiedAccumulatedField.fieldName, vps); + mergingGroup->addAccumulator(copiedAccumulatedField); + mergingGroup->_memoryTracker.set(copiedAccumulatedField.fieldName, 0); + } + + // {shardsStage, mergingStage, sortPattern} + return DistributedPlanLogic{this, mergingGroup, boost::none}; +} + +bool DocumentSourceGroup::pathIncludedInGroupKeys(const std::string& dottedPath) const { + return std::any_of( + _idExpressions.begin(), _idExpressions.end(), [&dottedPath](const auto& exp) { + if (auto fieldExp = dynamic_cast<ExpressionFieldPath*>(exp.get())) { + if (fieldExp->representsPath(dottedPath)) { + return true; + } + } + return false; + }); +} + +bool DocumentSourceGroup::canRunInParallelBeforeWriteStage( + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const { + if (_doingMerge) { + return true; // This is fine. + } + + // Certain $group stages are allowed to execute on each exchange consumer. In order to + // guarantee each consumer will only group together data from its own shard, the $group must + // group on a superset of the shard key. + for (auto&& currentPathOfShardKey : nameOfShardKeyFieldsUponEntryToStage) { + if (!pathIncludedInGroupKeys(currentPathOfShardKey)) { + // This requires an exact path match, but as a future optimization certain path + // prefixes should be okay. For example, if the shard key path is "a.b", and we're + // grouping by "a", then each group of "a" is strictly more specific than "a.b", so + // we can deduce that grouping by "a" will not need to group together documents + // across different values of the shard key field "a.b", and thus as long as any + // other shard key fields are similarly preserved will not need to consume a merged + // stream to perform the group. + return false; + } + } + return true; +} + +std::unique_ptr<GroupFromFirstDocumentTransformation> +DocumentSourceGroup::rewriteGroupAsTransformOnFirstDocument() const { + if (_idExpressions.size() != 1) { + // This transformation is only intended for $group stages that group on a single field. + return nullptr; + } + + auto fieldPathExpr = dynamic_cast<ExpressionFieldPath*>(_idExpressions.front().get()); + if (!fieldPathExpr || fieldPathExpr->isVariableReference()) { + return nullptr; + } + + const auto fieldPath = fieldPathExpr->getFieldPath(); + if (fieldPath.getPathLength() == 1) { + // The path is $$CURRENT or $$ROOT. This isn't really a sensible value to group by (since + // each document has a unique _id, it will just return the entire collection). We only + // apply the rewrite when grouping by a single field, so we cannot apply it in this case, + // where we are grouping by the entire document. + tassert(5943200, + "Optimization attempted on group by always-dissimilar system variable", + fieldPath.getFieldName(0) == "CURRENT" || fieldPath.getFieldName(0) == "ROOT"); + return nullptr; + } + + const auto groupId = fieldPath.tail().fullPath(); + + // We can't do this transformation if there are any non-$first accumulators. + for (auto&& accumulator : _accumulatedFields) { + if (AccumulatorDocumentsNeeded::kFirstDocument != + accumulator.makeAccumulator()->documentsNeeded()) { + return nullptr; + } + } + + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fields; + + boost::intrusive_ptr<Expression> idField; + // The _id field can be specified either as a fieldpath (ex. _id: "$a") or as a singleton + // object (ex. _id: {v: "$a"}). + if (_idFieldNames.empty()) { + idField = ExpressionFieldPath::deprecatedCreate(pExpCtx.get(), groupId); + } else { + invariant(_idFieldNames.size() == 1); + idField = ExpressionObject::create(pExpCtx.get(), + {{_idFieldNames.front(), _idExpressions.front()}}); + } + fields.push_back(std::make_pair("_id", idField)); + + for (auto&& accumulator : _accumulatedFields) { + fields.push_back(std::make_pair(accumulator.fieldName, accumulator.expr.argument)); + + // Since we don't attempt this transformation for non-$first accumulators, + // the initializer should always be trivial. + } + + return GroupFromFirstDocumentTransformation::create(pExpCtx, groupId, std::move(fields)); +} + +size_t DocumentSourceGroup::getMaxMemoryUsageBytes() const { + return _memoryTracker._maxAllowedMemoryUsageBytes; +} + } // namespace mongo + +#include "mongo/db/sorter/sorter.cpp" +// Explicit instantiation unneeded since we aren't exposing Sorter outside of this file. diff --git a/src/mongo/db/pipeline/document_source_group.h b/src/mongo/db/pipeline/document_source_group.h index a1f8e9b2b9a..edffed6758b 100644 --- a/src/mongo/db/pipeline/document_source_group.h +++ b/src/mongo/db/pipeline/document_source_group.h @@ -32,19 +32,75 @@ #include <memory> #include <utility> -#include "mongo/db/pipeline/document_source_group_base.h" +#include "mongo/db/pipeline/accumulation_statement.h" +#include "mongo/db/pipeline/accumulator.h" +#include "mongo/db/pipeline/document_source.h" +#include "mongo/db/pipeline/memory_usage_tracker.h" +#include "mongo/db/pipeline/transformer_interface.h" +#include "mongo/db/sorter/sorter.h" namespace mongo { /** - * This class represents hash based group implementation that stores all groups until source is - * depleted and only then starts outputing documents. + * GroupFromFirstTransformation consists of a list of (field name, expression pairs). It returns a + * document synthesized by assigning each field name in the output document to the result of + * evaluating the corresponding expression. If the expression evaluates to missing, we assign a + * value of BSONNULL. This is necessary to match the semantics of $first for missing fields. */ -class DocumentSourceGroup final : public DocumentSourceGroupBase { +class GroupFromFirstDocumentTransformation final : public TransformerInterface { public: + GroupFromFirstDocumentTransformation( + const std::string& groupId, + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs) + : _accumulatorExprs(std::move(accumulatorExprs)), _groupId(groupId) {} + + TransformerType getType() const final { + return TransformerType::kGroupFromFirstDocument; + } + + /** + * The path of the field that we are grouping on: i.e., the field in the input document that we + * will use to create the _id field of the ouptut document. + */ + const std::string& groupId() const { + return _groupId; + } + + Document applyTransformation(const Document& input) final; + + void optimize() final; + + Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const final; + + DepsTracker::State addDependencies(DepsTracker* deps) const final; + + DocumentSource::GetModPathsReturn getModifiedPaths() const final; + + static std::unique_ptr<GroupFromFirstDocumentTransformation> create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const std::string& groupId, + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs); + +private: + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> _accumulatorExprs; + std::string _groupId; +}; + +class DocumentSourceGroup final : public DocumentSource { +public: + using Accumulators = std::vector<boost::intrusive_ptr<AccumulatorState>>; + using GroupsMap = ValueUnorderedMap<Accumulators>; + static constexpr StringData kStageName = "$group"_sd; + boost::intrusive_ptr<DocumentSource> optimize() final; + DepsTracker::State getDependencies(DepsTracker* deps) const final; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; const char* getSourceName() const final; + GetModPathsReturn getModifiedPaths() const final; + StringMap<boost::intrusive_ptr<Expression>> getIdFields() const; + const std::vector<AccumulationStatement>& getAccumulatedFields() const; /** * Convenience method for creating a new $group stage. If maxMemoryUsageBytes is boost::none, @@ -62,40 +118,199 @@ public: */ static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); - static boost::intrusive_ptr<DocumentSource> createFromBsonWithMaxMemoryUsage( - BSONElement elem, - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes); -protected: - GetNextResult doGetNext() final; + StageConstraints constraints(Pipeline::SplitState pipeState) const final { + StageConstraints constraints(StreamType::kBlocking, + PositionRequirement::kNone, + HostTypeRequirement::kNone, + DiskUseRequirement::kWritesTmpData, + FacetRequirement::kAllowed, + TransactionRequirement::kAllowed, + LookupRequirement::kAllowed, + UnionRequirement::kAllowed); + constraints.canSwapWithMatch = true; + return constraints; + } - bool isSpecFieldReserved(StringData) final { - return false; + /** + * Add an accumulator, which will become a field in each Document that results from grouping. + */ + void addAccumulator(AccumulationStatement accumulationStatement); + + /** + * Sets the expression to use to determine the group id of each document. + */ + void setIdExpression(boost::intrusive_ptr<Expression> idExpression); + + /** + * Returns the expression to use to determine the group id of each document. + */ + boost::intrusive_ptr<Expression> getIdExpression() const; + + /** + * Returns true if this $group stage represents a 'global' $group which is merging together + * results from earlier partial groups. + */ + bool doingMerge() const { + return _doingMerge; } + /** + * Tell this source if it is doing a merge from shards. Defaults to false. + */ + void setDoingMerge(bool doingMerge) { + _doingMerge = doingMerge; + } + + /** + * Returns true if this $group stage used disk during execution and false otherwise. + */ + bool usedDisk() final { + return _stats.spills > 0; + } + + const SpecificStats* getSpecificStats() const final { + return &_stats; + } + + boost::optional<DistributedPlanLogic> distributedPlanLogic() final; + bool canRunInParallelBeforeWriteStage( + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const final; + + /** + * When possible, creates a document transformer that transforms the first document in a group + * into one of the output documents of the $group stage. This is possible when we are grouping + * on a single field and all accumulators are $first (or there are no accumluators). + * + * It is sometimes possible to use a DISTINCT_SCAN to scan the first document of each group, + * in which case this transformation can replace the actual $group stage in the pipeline + * (SERVER-9507). + */ + std::unique_ptr<GroupFromFirstDocumentTransformation> rewriteGroupAsTransformOnFirstDocument() + const; + + /** + * Returns maximum allowed memory footprint. + */ + size_t getMaxMemoryUsageBytes() const; + + // True if this $group can be pushed down to SBE. + bool sbeCompatible() const { + return _sbeCompatible; + } + +protected: + GetNextResult doGetNext() final; + void doDispose() final; + private: explicit DocumentSourceGroup(const boost::intrusive_ptr<ExpressionContext>& expCtx, boost::optional<size_t> maxMemoryUsageBytes = boost::none); /** - * Before returning anything, this source must prepare itself. performBlockingGroup() exhausts - * the previous source before - * returning. The '_groupsReady' boolean indicates that performBlockingGroup() has finished. + * getNext() dispatches to one of these three depending on what type of $group it is. These + * methods expect '_currentAccumulators' to have been reset before being called, and also expect + * initialize() to have been called already. + */ + GetNextResult getNextSpilled(); + GetNextResult getNextStandard(); + + /** + * Before returning anything, this source must prepare itself. In a streaming $group, + * initialize() requests the first document from the previous source, and uses it to prepare the + * accumulators. In an unsorted $group, initialize() exhausts the previous source before + * returning. The '_initialized' boolean indicates that initialize() has finished. * * This method may not be able to finish initialization in a single call if 'pSource' returns a * DocumentSource::GetNextResult::kPauseExecution, so it returns the last GetNextResult * encountered, which may be either kEOF or kPauseExecution. */ - GetNextResult performBlockingGroup(); + GetNextResult initialize(); /** - * Initializes this $group after any children are initialized. See performBlockingGroup() for + * Initializes this $group after any children are potentially initialized see initialize() for * more details. */ - GetNextResult performBlockingGroupSelf(GetNextResult input); + GetNextResult initializeSelf(GetNextResult input); + + /** + * Spill groups map to disk and returns an iterator to the file. Note: Since a sorted $group + * does not exhaust the previous stage before returning, and thus does not maintain as large a + * store of documents at any one time, only an unsorted group can spill to disk. + */ + std::shared_ptr<Sorter<Value, Value>::Iterator> spill(); + + /** + * If we ran out of memory, finish all the pending operations so that some memory + * can be freed. + */ + void freeMemory(); + + Document makeDocument(const Value& id, const Accumulators& accums, bool mergeableOutput); + + /** + * Computes the internal representation of the group key. + */ + Value computeId(const Document& root); + + /** + * Converts the internal representation of the group key to the _id shape specified by the + * user. + */ + Value expandId(const Value& val); + + /** + * Returns true if 'dottedPath' is one of the group keys present in '_idExpressions'. + */ + bool pathIncludedInGroupKeys(const std::string& dottedPath) const; + + /** + * Cleans up any pending memory usage. Throws error, if memory usage is above + * 'maxMemoryUsageBytes' and cannot spill to disk. + * + * Returns true, if the caller should spill to disk, false otherwise. + */ + bool shouldSpillWithAttemptToSaveMemory(); + + std::vector<AccumulationStatement> _accumulatedFields; + + bool _doingMerge; + + MemoryUsageTracker _memoryTracker; + + GroupStats _stats; + + std::shared_ptr<Sorter<Value, Value>::File> _file; + + // If the expression for the '_id' field represents a non-empty object, we track its fields' + // names in '_idFieldNames'. + std::vector<std::string> _idFieldNames; + // Expressions for the individual fields when '_id' produces a document in the order of + // '_idFieldNames' or the whole expression otherwise. + std::vector<boost::intrusive_ptr<Expression>> _idExpressions; + + bool _initialized; + + Value _currentId; + Accumulators _currentAccumulators; + + // We use boost::optional to defer initialization until the ExpressionContext containing the + // correct comparator is injected, since the groups must be built using the comparator's + // definition of equality. + boost::optional<GroupsMap> _groups; + + std::vector<std::shared_ptr<Sorter<Value, Value>::Iterator>> _sortedFiles; + bool _spilled; + + // Only used when '_spilled' is false. + GroupsMap::iterator groupsIterator; + + // Only used when '_spilled' is true. + std::unique_ptr<Sorter<Value, Value>::Iterator> _sorterIterator; + + std::pair<Value, Value> _firstPartOfNextGroup; - bool _groupsReady; + bool _sbeCompatible; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_group_base.cpp b/src/mongo/db/pipeline/document_source_group_base.cpp deleted file mode 100644 index 2c4e8c2dd3b..00000000000 --- a/src/mongo/db/pipeline/document_source_group_base.cpp +++ /dev/null @@ -1,812 +0,0 @@ -/** - * Copyright (C) 2018-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 "mongo/platform/basic.h" - -#include <memory> - -#include "mongo/db/exec/document_value/document.h" -#include "mongo/db/exec/document_value/value.h" -#include "mongo/db/exec/document_value/value_comparator.h" -#include "mongo/db/pipeline/accumulation_statement.h" -#include "mongo/db/pipeline/accumulator.h" -#include "mongo/db/pipeline/document_source_group.h" -#include "mongo/db/pipeline/document_source_group_base.h" -#include "mongo/db/pipeline/expression.h" -#include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/stats/resource_consumption_metrics.h" -#include "mongo/util/destructor_guard.h" - -namespace mongo { - -namespace { - -/** - * Generates a new file name on each call using a static, atomic and monotonically increasing - * number. - * - * Each user of the Sorter must implement this function to ensure that all temporary files that the - * Sorter instances produce are uniquely identified using a unique file name extension with separate - * atomic variable. This is necessary because the sorter.cpp code is separately included in multiple - * places, rather than compiled in one place and linked, and so cannot provide a globally unique ID. - */ -std::string nextFileName() { - static AtomicWord<unsigned> documentSourceGroupFileCounter; - return "extsort-doc-group." + std::to_string(documentSourceGroupFileCounter.fetchAndAdd(1)); -} - -} // namespace - -using boost::intrusive_ptr; -using std::pair; -using std::shared_ptr; -using std::vector; - -Value DocumentSourceGroupBase::serialize(const SerializationOptions& opts) const { - MutableDocument insides; - - // Add the _id. - if (_idFieldNames.empty()) { - invariant(_idExpressions.size() == 1); - insides["_id"] = _idExpressions[0]->serialize(opts); - } else { - // Decomposed document case. - invariant(_idExpressions.size() == _idFieldNames.size()); - MutableDocument md; - for (size_t i = 0; i < _idExpressions.size(); i++) { - md[opts.serializeFieldPathFromString(_idFieldNames[i])] = - _idExpressions[i]->serialize(opts); - } - insides["_id"] = md.freezeToValue(); - } - - // Add the remaining fields. - for (auto&& accumulatedField : _accumulatedFields) { - intrusive_ptr<AccumulatorState> accum = accumulatedField.makeAccumulator(); - insides[opts.serializeFieldPathFromString(accumulatedField.fieldName)] = - Value(accum->serialize( - accumulatedField.expr.initializer, accumulatedField.expr.argument, opts)); - } - - if (_doingMerge) { - insides["$doingMerge"] = opts.serializeLiteral(true); - } - - serializeAdditionalFields(insides, opts); - - MutableDocument out; - out[getSourceName()] = insides.freezeToValue(); - - if (opts.verbosity && *opts.verbosity >= ExplainOptions::Verbosity::kExecStats) { - MutableDocument md; - - for (size_t i = 0; i < _accumulatedFields.size(); i++) { - md[opts.serializeFieldPathFromString(_accumulatedFields[i].fieldName)] = - opts.serializeLiteral(static_cast<long long>( - _memoryTracker[_accumulatedFields[i].fieldName].maxMemoryBytes())); - } - - out["maxAccumulatorMemoryUsageBytes"] = Value(md.freezeToValue()); - out["totalOutputDataSizeBytes"] = - opts.serializeLiteral(static_cast<long long>(_stats.totalOutputDataSizeBytes)); - out["usedDisk"] = opts.serializeLiteral(_stats.spills > 0); - out["spills"] = opts.serializeLiteral(static_cast<long long>(_stats.spills)); - } - - return out.freezeToValue(); -} - - -bool DocumentSourceGroupBase::shouldSpillWithAttemptToSaveMemory() { - if (!_memoryTracker._allowDiskUse && - (_memoryTracker.currentMemoryBytes() > - static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes))) { - freeMemory(); - } - - if (_memoryTracker.currentMemoryBytes() > - static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes)) { - uassert(ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed, - "Exceeded memory limit for $group, but didn't allow external sort." - " Pass allowDiskUse:true to opt in.", - _memoryTracker._allowDiskUse); - _memoryTracker.resetCurrent(); - return true; - } - return false; -} - -void DocumentSourceGroupBase::freeMemory() { - invariant(_groups); - for (auto&& group : *_groups) { - for (size_t i = 0; i < group.second.size(); i++) { - // Subtract the current usage. - _memoryTracker.update(_accumulatedFields[i].fieldName, - -1 * group.second[i]->getMemUsage()); - - group.second[i]->reduceMemoryConsumptionIfAble(); - - // Update the memory usage for this AccumulationStatement. - _memoryTracker.update(_accumulatedFields[i].fieldName, group.second[i]->getMemUsage()); - } - } -} - -DocumentSource::GetNextResult DocumentSourceGroupBase::getNextReadyGroup() { - if (_spilled) { - return getNextSpilled(); - } else { - return getNextStandard(); - } -} - -DocumentSource::GetNextResult DocumentSourceGroupBase::getNextSpilled() { - // We aren't streaming, and we have spilled to disk. - if (!_sorterIterator) - return GetNextResult::makeEOF(); - - Value currentId = _firstPartOfNextGroup.first; - const size_t numAccumulators = _accumulatedFields.size(); - - // Call startNewGroup on every accumulator. - Value expandedId = expandId(currentId); - Document idDoc = - expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); - for (size_t i = 0; i < numAccumulators; ++i) { - Value initializerValue = - _accumulatedFields[i].expr.initializer->evaluate(idDoc, &pExpCtx->variables); - _currentAccumulators[i]->reset(); - _currentAccumulators[i]->startNewGroup(initializerValue); - } - - while (pExpCtx->getValueComparator().evaluate(currentId == _firstPartOfNextGroup.first)) { - // Inside of this loop, _firstPartOfNextGroup is the current data being processed. - // At loop exit, it is the first value to be processed in the next group. - switch (numAccumulators) { // mirrors switch in spill() - case 1: // Single accumulators serialize as a single Value. - _currentAccumulators[0]->process(_firstPartOfNextGroup.second, true); - [[fallthrough]]; - case 0: // No accumulators so no Values. - break; - default: { // Multiple accumulators serialize as an array of Values. - const vector<Value>& accumulatorStates = _firstPartOfNextGroup.second.getArray(); - for (size_t i = 0; i < numAccumulators; i++) { - _currentAccumulators[i]->process(accumulatorStates[i], true); - } - } - } - - if (!_sorterIterator->more()) { - _sorterIterator.reset(); - break; - } - - _firstPartOfNextGroup = _sorterIterator->next(); - } - - return makeDocument(currentId, _currentAccumulators, pExpCtx->needsMerge); -} - -DocumentSource::GetNextResult DocumentSourceGroupBase::getNextStandard() { - // Not spilled, and not streaming. - if (_groupsIterator == _groups->end()) - return GetNextResult::makeEOF(); - - Document out = - makeDocument(_groupsIterator->first, _groupsIterator->second, pExpCtx->needsMerge); - ++_groupsIterator; - return out; -} - -void DocumentSourceGroupBase::doDispose() { - resetReadyGroups(); -} - -intrusive_ptr<DocumentSource> DocumentSourceGroupBase::optimize() { - // Optimizing a 'DocumentSourceGroupBase' might modify its expressions to become incompatible - // with SBE. We temporarily highjack the context's 'sbeCompatible' flag to communicate the - // situation back to the 'DocumentSourceGroupBase'. Notice, that while a particular - // 'DocumentSourceGroupBase' might become incompatible with SBE, other groups in the pipeline - // and the collection access could be still eligible for lowering to SBE, thus we must reset the - // context's 'sbeCompatible' flag back to its original value at the end of the 'optimize()' - // call. - // - // TODO SERVER-XXXXX: replace this hack with a proper per-stage tracking of SBE compatibility. - auto expCtx = _idExpressions[0]->getExpressionContext(); - auto orgSbeCompatible = expCtx->sbeCompatible; - expCtx->sbeCompatible = true; - - // TODO: If all _idExpressions are ExpressionConstants after optimization, then we know there - // will be only one group. We should take advantage of that to avoid going through the hash - // table. - for (size_t i = 0; i < _idExpressions.size(); i++) { - _idExpressions[i] = _idExpressions[i]->optimize(); - } - - for (auto&& accumulatedField : _accumulatedFields) { - accumulatedField.expr.initializer = accumulatedField.expr.initializer->optimize(); - accumulatedField.expr.argument = accumulatedField.expr.argument->optimize(); - } - - _sbeCompatible = _sbeCompatible && expCtx->sbeCompatible; - expCtx->sbeCompatible = orgSbeCompatible; - - return this; -} - -DepsTracker::State DocumentSourceGroupBase::getDependencies(DepsTracker* deps) const { - // add the _id - for (size_t i = 0; i < _idExpressions.size(); i++) { - _idExpressions[i]->addDependencies(deps); - } - - // add the rest - for (auto&& accumulatedField : _accumulatedFields) { - accumulatedField.expr.argument->addDependencies(deps); - // Don't add initializer, because it doesn't refer to docs from the input stream. - } - - return DepsTracker::State::EXHAUSTIVE_ALL; -} - -DocumentSource::GetModPathsReturn DocumentSourceGroupBase::getModifiedPaths() const { - // We preserve none of the fields, but any fields referenced as part of the group key are - // logically just renamed. - StringMap<std::string> renames; - for (std::size_t i = 0; i < _idExpressions.size(); ++i) { - auto idExp = _idExpressions[i]; - auto pathToPutResultOfExpression = - _idFieldNames.empty() ? "_id" : "_id." + _idFieldNames[i]; - auto computedPaths = idExp->getComputedPaths(pathToPutResultOfExpression); - for (auto&& rename : computedPaths.renames) { - renames[rename.first] = rename.second; - } - } - - return {DocumentSource::GetModPathsReturn::Type::kAllExcept, - OrderedPathSet{}, // No fields are preserved. - std::move(renames)}; -} - -StringMap<boost::intrusive_ptr<Expression>> DocumentSourceGroupBase::getIdFields() const { - if (_idFieldNames.empty()) { - invariant(_idExpressions.size() == 1); - return {{"_id", _idExpressions[0]}}; - } else { - invariant(_idFieldNames.size() == _idExpressions.size()); - StringMap<boost::intrusive_ptr<Expression>> result; - for (std::size_t i = 0; i < _idFieldNames.size(); ++i) { - result["_id." + _idFieldNames[i]] = _idExpressions[i]; - } - return result; - } -} - -std::vector<boost::intrusive_ptr<Expression>>& DocumentSourceGroupBase::getMutableIdFields() { - tassert(7020503, "Can't mutate _id fields after initialization", !_executionStarted); - return _idExpressions; -} - -const std::vector<AccumulationStatement>& DocumentSourceGroupBase::getAccumulatedFields() const { - return _accumulatedFields; -} - -std::vector<AccumulationStatement>& DocumentSourceGroupBase::getMutableAccumulatedFields() { - tassert(7020504, "Can't mutate accumulated fields after initialization", !_executionStarted); - return _accumulatedFields; -} - -DocumentSourceGroupBase::DocumentSourceGroupBase(StringData stageName, - const intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes) - : DocumentSource(stageName, expCtx), - _doingMerge(false), - _memoryTracker{expCtx->allowDiskUse && !expCtx->inMongos, - maxMemoryUsageBytes - ? *maxMemoryUsageBytes - : static_cast<size_t>(internalDocumentSourceGroupMaxMemoryBytes.load())}, - _executionStarted(false), - _groups(expCtx->getValueComparator().makeUnorderedValueMap<Accumulators>()), - _spilled(false), - _sbeCompatible(false) {} - -void DocumentSourceGroupBase::addAccumulator(AccumulationStatement accumulationStatement) { - _accumulatedFields.push_back(accumulationStatement); - _memoryTracker.set(accumulationStatement.fieldName, 0); -} - -namespace { - -intrusive_ptr<Expression> parseIdExpression(const intrusive_ptr<ExpressionContext>& expCtx, - BSONElement groupField, - const VariablesParseState& vps) { - if (groupField.type() == Object) { - // {_id: {}} is treated as grouping on a constant, not an expression - if (groupField.Obj().isEmpty()) { - return ExpressionConstant::create(expCtx.get(), Value(groupField)); - } - - const BSONObj idKeyObj = groupField.Obj(); - if (idKeyObj.firstElementFieldName()[0] == '$') { - // grouping on a $op expression - return Expression::parseObject(expCtx.get(), idKeyObj, vps); - } else { - for (auto&& field : idKeyObj) { - uassert(17390, - "$group does not support inclusion-style expressions", - !field.isNumber() && field.type() != Bool); - } - return ExpressionObject::parse(expCtx.get(), idKeyObj, vps); - } - } else { - return Expression::parseOperand(expCtx.get(), groupField, vps); - } -} - -} // namespace - -void DocumentSourceGroupBase::setIdExpression(const boost::intrusive_ptr<Expression> idExpression) { - if (auto object = dynamic_cast<ExpressionObject*>(idExpression.get())) { - auto& childExpressions = object->getChildExpressions(); - invariant(!childExpressions.empty()); // We expect to have converted an empty object into a - // constant expression. - - // grouping on an "artificial" object. Rather than create the object for each input - // in initialize(), instead group on the output of the raw expressions. The artificial - // object will be created at the end in makeDocument() while outputting results. - for (auto&& childExpPair : childExpressions) { - _idFieldNames.push_back(childExpPair.first); - _idExpressions.push_back(childExpPair.second); - } - } else { - _idExpressions.push_back(idExpression); - } -} - -boost::intrusive_ptr<Expression> DocumentSourceGroupBase::getIdExpression() const { - // _idFieldNames is empty and _idExpressions has one element when the _id expression is not an - // object expression. - if (_idFieldNames.empty() && _idExpressions.size() == 1) { - return _idExpressions[0]; - } - - tassert(6586300, - "Field and its expression must be always paired in ExpressionObject", - _idFieldNames.size() > 0 && _idFieldNames.size() == _idExpressions.size()); - - // Each expression in '_idExpressions' may have been optimized and so, compose the object _id - // expression out of the optimized expressions. - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fieldsAndExprs; - for (size_t i = 0; i < _idExpressions.size(); ++i) { - fieldsAndExprs.emplace_back(_idFieldNames[i], _idExpressions[i]); - } - - return ExpressionObject::create(_idExpressions[0]->getExpressionContext(), - std::move(fieldsAndExprs)); -} - -void DocumentSourceGroupBase::initializeFromBson(BSONElement elem) { - uassert(15947, "a group's fields must be specified in an object", elem.type() == Object); - - BSONObj groupObj(elem.Obj()); - BSONObjIterator groupIterator(groupObj); - VariablesParseState vps = pExpCtx->variablesParseState; - pExpCtx->sbeGroupCompatible = true; - while (groupIterator.more()) { - BSONElement groupField(groupIterator.next()); - StringData pFieldName = groupField.fieldNameStringData(); - if (pFieldName == "_id") { - uassert(15948, "a group's _id may only be specified once", _idExpressions.empty()); - setIdExpression(parseIdExpression(pExpCtx, groupField, vps)); - invariant(!_idExpressions.empty()); - } else if (pFieldName == "$doingMerge") { - massert(17030, "$doingMerge should be true if present", groupField.Bool()); - - setDoingMerge(true); - } else if (isSpecFieldReserved(pFieldName)) { - // No-op: field is used by the derived class. - } else { - // Any other field will be treated as an accumulator specification. - addAccumulator( - AccumulationStatement::parseAccumulationStatement(pExpCtx.get(), groupField, vps)); - } - } - _sbeCompatible = pExpCtx->sbeGroupCompatible && pExpCtx->sbeCompatible; - - uassert(15955, "a group specification must include an _id", !_idExpressions.empty()); -} - -namespace { - -using GroupsMap = DocumentSourceGroupBase::GroupsMap; - -class SorterComparator { -public: - typedef pair<Value, Value> Data; - - SorterComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} - - int operator()(const Data& lhs, const Data& rhs) const { - return _valueComparator.compare(lhs.first, rhs.first); - } - -private: - ValueComparator _valueComparator; -}; - -class SpillSTLComparator { -public: - SpillSTLComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} - - bool operator()(const GroupsMap::value_type* lhs, const GroupsMap::value_type* rhs) const { - return _valueComparator.evaluate(lhs->first < rhs->first); - } - -private: - ValueComparator _valueComparator; -}; -} // namespace - -void DocumentSourceGroupBase::processDocument(const Value& id, const Document& root) { - const size_t numAccumulators = _accumulatedFields.size(); - - // Look for the _id value in the map. If it's not there, add a new entry with a blank - // accumulator. This is done in a somewhat odd way in order to avoid hashing 'id' and - // looking it up in '_groups' multiple times. - const size_t oldSize = _groups->size(); - vector<intrusive_ptr<AccumulatorState>>& group = (*_groups)[id]; - const bool inserted = _groups->size() != oldSize; - - if (inserted) { - _memoryTracker.set(_memoryTracker.currentMemoryBytes() + id.getApproximateSize()); - - // Initialize and add the accumulators - Value expandedId = expandId(id); - Document idDoc = - expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); - group.reserve(numAccumulators); - for (auto&& accumulatedField : _accumulatedFields) { - auto accum = accumulatedField.makeAccumulator(); - Value initializerValue = - accumulatedField.expr.initializer->evaluate(idDoc, &pExpCtx->variables); - accum->startNewGroup(initializerValue); - group.push_back(accum); - } - } - - /* tickle all the accumulators for the group we found */ - dassert(numAccumulators == group.size()); - - for (size_t i = 0; i < numAccumulators; i++) { - // Only process the input and update the memory footprint if the current accumulator - // needs more input. - if (group[i]->needsInput()) { - const auto prevMemUsage = inserted ? 0 : group[i]->getMemUsage(); - group[i]->process( - _accumulatedFields[i].expr.argument->evaluate(root, &pExpCtx->variables), - _doingMerge); - _memoryTracker.update(_accumulatedFields[i].fieldName, - group[i]->getMemUsage() - prevMemUsage); - } - } - - if (kDebugBuild && !storageGlobalParams.readOnly) { - // In debug mode, spill every time we have a duplicate id to stress merge logic. - if (!inserted && // is a dup - !pExpCtx->inMongos && // can't spill to disk in mongos - !_memoryTracker._allowDiskUse && // don't change behavior when testing external sort - _sortedFiles.size() < 20) { // don't open too many FDs - spill(); - } - } -} - -void DocumentSourceGroupBase::readyGroups() { - _spilled = !_sortedFiles.empty(); - if (_spilled) { - if (!_groups->empty()) { - spill(); - } - - _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); - - _sorterIterator.reset(Sorter<Value, Value>::Iterator::merge( - _sortedFiles, SortOptions(), SorterComparator(pExpCtx->getValueComparator()))); - - // prepare current to accumulate data - _currentAccumulators.reserve(_accumulatedFields.size()); - for (auto&& accumulatedField : _accumulatedFields) { - _currentAccumulators.push_back(accumulatedField.makeAccumulator()); - } - - verify(_sorterIterator->more()); // we put data in, we should get something out. - _firstPartOfNextGroup = _sorterIterator->next(); - } else { - // start the group iterator - _groupsIterator = _groups->begin(); - } -} - -void DocumentSourceGroupBase::resetReadyGroups() { - // Free our resources. - _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); - _memoryTracker.resetCurrent(); - _sorterIterator.reset(); - _sortedFiles.clear(); - - // Make us look done. - _groupsIterator = _groups->end(); -} - -void DocumentSourceGroupBase::spill() { - _stats.spills++; - - vector<const GroupsMap::value_type*> ptrs; // using pointers to speed sorting - ptrs.reserve(_groups->size()); - for (GroupsMap::const_iterator it = _groups->begin(), end = _groups->end(); it != end; ++it) { - ptrs.push_back(&*it); - } - - stable_sort(ptrs.begin(), ptrs.end(), SpillSTLComparator(pExpCtx->getValueComparator())); - - // Initialize '_file' in a lazy manner only when it is needed. - if (!_file) { - _file = - std::make_shared<Sorter<Value, Value>::File>(pExpCtx->tempDir + "/" + nextFileName()); - } - SortedFileWriter<Value, Value> writer(SortOptions().TempDir(pExpCtx->tempDir), _file); - switch (_accumulatedFields.size()) { // same as ptrs[i]->second.size() for all i. - case 0: // no values, essentially a distinct - for (size_t i = 0; i < ptrs.size(); i++) { - writer.addAlreadySorted(ptrs[i]->first, Value()); - } - break; - - case 1: // just one value, use optimized serialization as single Value - for (size_t i = 0; i < ptrs.size(); i++) { - writer.addAlreadySorted(ptrs[i]->first, - ptrs[i]->second[0]->getValue(/*toBeMerged=*/true)); - } - break; - - default: // multiple values, serialize as array-typed Value - for (size_t i = 0; i < ptrs.size(); i++) { - vector<Value> accums; - for (size_t j = 0; j < ptrs[i]->second.size(); j++) { - accums.push_back(ptrs[i]->second[j]->getValue(/*toBeMerged=*/true)); - } - writer.addAlreadySorted(ptrs[i]->first, Value(std::move(accums))); - } - break; - } - - auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx); - metricsCollector.incrementKeysSorted(ptrs.size()); - metricsCollector.incrementSorterSpills(1); - - _groups->clear(); - // Zero out the current per-accumulation statement memory consumption, as the memory has been - // freed by spilling. - for (const auto& accum : _accumulatedFields) { - _memoryTracker.set(accum.fieldName, 0); - } - - _sortedFiles.emplace_back(writer.done()); -} - -Value DocumentSourceGroupBase::computeId(const Document& root) { - // If only one expression, return result directly - if (_idExpressions.size() == 1) { - Value retValue = _idExpressions[0]->evaluate(root, &pExpCtx->variables); - return retValue.missing() ? Value(BSONNULL) : std::move(retValue); - } - - // Multiple expressions get results wrapped in a vector - vector<Value> vals; - vals.reserve(_idExpressions.size()); - for (size_t i = 0; i < _idExpressions.size(); i++) { - vals.push_back(_idExpressions[i]->evaluate(root, &pExpCtx->variables)); - } - return Value(std::move(vals)); -} - -Value DocumentSourceGroupBase::expandId(const Value& val) { - // _id doesn't get wrapped in a document - if (_idFieldNames.empty()) - return val; - - // _id is a single-field document containing val - if (_idFieldNames.size() == 1) - return Value(DOC(_idFieldNames[0] << val)); - - // _id is a multi-field document containing the elements of val - const vector<Value>& vals = val.getArray(); - invariant(_idFieldNames.size() == vals.size()); - MutableDocument md(vals.size()); - for (size_t i = 0; i < vals.size(); i++) { - md[_idFieldNames[i]] = vals[i]; - } - return md.freezeToValue(); -} - -Document DocumentSourceGroupBase::makeDocument(const Value& id, - const Accumulators& accums, - bool mergeableOutput) { - const size_t n = _accumulatedFields.size(); - MutableDocument out(1 + n); - - /* add the _id field */ - out.addField("_id", expandId(id)); - - /* add the rest of the fields */ - for (size_t i = 0; i < n; ++i) { - Value val = accums[i]->getValue(mergeableOutput); - if (val.missing()) { - // we return null in this case so return objects are predictable - out.addField(_accumulatedFields[i].fieldName, Value(BSONNULL)); - } else { - out.addField(_accumulatedFields[i].fieldName, std::move(val)); - } - } - - _stats.totalOutputDataSizeBytes += out.getApproximateSize(); - return out.freeze(); -} - -bool DocumentSourceGroupBase::pathIncludedInGroupKeys(const std::string& dottedPath) const { - return std::any_of( - _idExpressions.begin(), _idExpressions.end(), [&dottedPath](const auto& exp) { - if (auto fieldExp = dynamic_cast<ExpressionFieldPath*>(exp.get())) { - if (fieldExp->representsPath(dottedPath)) { - return true; - } - } - return false; - }); -} - -bool DocumentSourceGroupBase::canRunInParallelBeforeWriteStage( - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const { - if (_doingMerge) { - return true; // This is fine. - } - - // Certain $group stages are allowed to execute on each exchange consumer. In order to - // guarantee each consumer will only group together data from its own shard, the $group must - // group on a superset of the shard key. - for (auto&& currentPathOfShardKey : nameOfShardKeyFieldsUponEntryToStage) { - if (!pathIncludedInGroupKeys(currentPathOfShardKey)) { - // This requires an exact path match, but as a future optimization certain path - // prefixes should be okay. For example, if the shard key path is "a.b", and we're - // grouping by "a", then each group of "a" is strictly more specific than "a.b", so - // we can deduce that grouping by "a" will not need to group together documents - // across different values of the shard key field "a.b", and thus as long as any - // other shard key fields are similarly preserved will not need to consume a merged - // stream to perform the group. - return false; - } - } - return true; -} - -std::unique_ptr<GroupFromFirstDocumentTransformation> -DocumentSourceGroupBase::rewriteGroupAsTransformOnFirstDocument() const { - if (_idExpressions.size() != 1) { - // This transformation is only intended for $group stages that group on a single field. - return nullptr; - } - - auto fieldPathExpr = dynamic_cast<ExpressionFieldPath*>(_idExpressions.front().get()); - if (!fieldPathExpr || fieldPathExpr->isVariableReference()) { - return nullptr; - } - - const auto fieldPath = fieldPathExpr->getFieldPath(); - if (fieldPath.getPathLength() == 1) { - // The path is $$CURRENT or $$ROOT. This isn't really a sensible value to group by (since - // each document has a unique _id, it will just return the entire collection). We only - // apply the rewrite when grouping by a single field, so we cannot apply it in this case, - // where we are grouping by the entire document. - tassert(5943200, - "Optimization attempted on group by always-dissimilar system variable", - fieldPath.getFieldName(0) == "CURRENT" || fieldPath.getFieldName(0) == "ROOT"); - return nullptr; - } - - const auto groupId = fieldPath.tail().fullPath(); - - // We can't do this transformation if there are any non-$first accumulators. - for (auto&& accumulator : _accumulatedFields) { - if (AccumulatorDocumentsNeeded::kFirstDocument != - accumulator.makeAccumulator()->documentsNeeded()) { - return nullptr; - } - } - - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fields; - - boost::intrusive_ptr<Expression> idField; - // The _id field can be specified either as a fieldpath (ex. _id: "$a") or as a singleton - // object (ex. _id: {v: "$a"}). - if (_idFieldNames.empty()) { - idField = ExpressionFieldPath::deprecatedCreate(pExpCtx.get(), groupId); - } else { - invariant(_idFieldNames.size() == 1); - idField = ExpressionObject::create(pExpCtx.get(), - {{_idFieldNames.front(), _idExpressions.front()}}); - } - fields.push_back(std::make_pair("_id", idField)); - - for (auto&& accumulator : _accumulatedFields) { - fields.push_back(std::make_pair(accumulator.fieldName, accumulator.expr.argument)); - - // Since we don't attempt this transformation for non-$first accumulators, - // the initializer should always be trivial. - } - - return GroupFromFirstDocumentTransformation::create( - pExpCtx, groupId, getSourceName(), std::move(fields)); -} - -size_t DocumentSourceGroupBase::getMaxMemoryUsageBytes() const { - return _memoryTracker._maxAllowedMemoryUsageBytes; -} - -boost::optional<DocumentSource::DistributedPlanLogic> -DocumentSourceGroupBase::distributedPlanLogic() { - VariablesParseState vps = pExpCtx->variablesParseState; - /* the merger will use the same grouping key */ - auto mergerGroupByExpression = ExpressionFieldPath::parse(pExpCtx.get(), "$$ROOT._id", vps); - - std::vector<AccumulationStatement> mergerAccumulators; - mergerAccumulators.reserve(_accumulatedFields.size()); - for (auto&& accumulatedField : _accumulatedFields) { - // The merger's output field names will be the same, as will the accumulator factories. - // However, for some accumulators, the expression to be accumulated will be different. The - // original accumulator may be collecting an expression based on a field expression or - // constant. Here, we accumulate the output of the same name from the prior group. - auto copiedAccumulatedField = accumulatedField; - copiedAccumulatedField.expr.argument = ExpressionFieldPath::parse( - pExpCtx.get(), "$$ROOT." + copiedAccumulatedField.fieldName, vps); - mergerAccumulators.emplace_back(std::move(copiedAccumulatedField)); - } - - // When merging, we always use generic hash based algorithm. - boost::intrusive_ptr<DocumentSourceGroup> mergingGroup = DocumentSourceGroup::create( - pExpCtx, std::move(mergerGroupByExpression), std::move(mergerAccumulators)); - mergingGroup->setDoingMerge(true); - // {shardsStage, mergingStage, sortPattern} - return DistributedPlanLogic{this, mergingGroup, boost::none}; -} - -} // namespace mongo - -#include "mongo/db/sorter/sorter.cpp" -// Explicit instantiation unneeded since we aren't exposing Sorter outside of this file. diff --git a/src/mongo/db/pipeline/document_source_group_base.h b/src/mongo/db/pipeline/document_source_group_base.h deleted file mode 100644 index c7a296c2583..00000000000 --- a/src/mongo/db/pipeline/document_source_group_base.h +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Copyright (C) 2018-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. - */ - -#pragma once - -#include <memory> -#include <utility> - -#include "mongo/db/pipeline/accumulation_statement.h" -#include "mongo/db/pipeline/accumulator.h" -#include "mongo/db/pipeline/document_source.h" -#include "mongo/db/pipeline/group_from_first_document_transformation.h" -#include "mongo/db/pipeline/memory_usage_tracker.h" -#include "mongo/db/sorter/sorter.h" - -namespace mongo { - -/** - * This class represents a $group stage generically - could be a streaming or hash based group. - * - * It contains some common execution code between the two algorithms, such as: - * - Handling spilling to disk. - * - Computing the group key - * - Accumulating values and populating output documents. - */ -class DocumentSourceGroupBase : public DocumentSource { -public: - using Accumulators = std::vector<boost::intrusive_ptr<AccumulatorState>>; - using GroupsMap = ValueUnorderedMap<Accumulators>; - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - boost::intrusive_ptr<DocumentSource> optimize() final; - DepsTracker::State getDependencies(DepsTracker* deps) const final; - GetModPathsReturn getModifiedPaths() const final; - StringMap<boost::intrusive_ptr<Expression>> getIdFields() const; - - boost::optional<DistributedPlanLogic> distributedPlanLogic() final; - - /** - * Can be used to change or swap out individual _id fields, but should not be used - * once execution has begun. - */ - std::vector<boost::intrusive_ptr<Expression>>& getMutableIdFields(); - const std::vector<AccumulationStatement>& getAccumulatedFields() const; - - /** - * Can be used to change or swap out individual accumulated fields, but should not be used - * once execution has begun. - */ - std::vector<AccumulationStatement>& getMutableAccumulatedFields(); - - StageConstraints constraints(Pipeline::SplitState pipeState) const final { - StageConstraints constraints(StreamType::kBlocking, - PositionRequirement::kNone, - HostTypeRequirement::kNone, - DiskUseRequirement::kWritesTmpData, - FacetRequirement::kAllowed, - TransactionRequirement::kAllowed, - LookupRequirement::kAllowed, - UnionRequirement::kAllowed); - constraints.canSwapWithMatch = true; - return constraints; - } - - /** - * Add an accumulator, which will become a field in each Document that results from grouping. - */ - void addAccumulator(AccumulationStatement accumulationStatement); - - /** - * Sets the expression to use to determine the group id of each document. - */ - void setIdExpression(boost::intrusive_ptr<Expression> idExpression); - - /** - * Returns the expression to use to determine the group id of each document. - */ - boost::intrusive_ptr<Expression> getIdExpression() const; - - /** - * Returns true if this $group stage represents a 'global' $group which is merging together - * results from earlier partial groups. - */ - bool doingMerge() const { - return _doingMerge; - } - - /** - * Tell this source if it is doing a merge from shards. Defaults to false. - */ - void setDoingMerge(bool doingMerge) { - _doingMerge = doingMerge; - } - - /** - * Returns true if this $group stage used disk during execution and false otherwise. - */ - bool usedDisk() final { - return _stats.spills > 0; - } - - const SpecificStats* getSpecificStats() const final { - return &_stats; - } - - const std::vector<std::string>& getIdFieldNames() const { - return _idFieldNames; - } - - bool canRunInParallelBeforeWriteStage( - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const final; - - /** - * When possible, creates a document transformer that transforms the first document in a group - * into one of the output documents of the $group stage. This is possible when we are grouping - * on a single field and all accumulators are $first (or there are no accumluators). - * - * It is sometimes possible to use a DISTINCT_SCAN to scan the first document of each group, - * in which case this transformation can replace the actual $group stage in the pipeline - * (SERVER-9507). - */ - std::unique_ptr<GroupFromFirstDocumentTransformation> rewriteGroupAsTransformOnFirstDocument() - const; - - /** - * Returns maximum allowed memory footprint. - */ - size_t getMaxMemoryUsageBytes() const; - - // True if this $group can be pushed down to SBE. - bool sbeCompatible() const { - return _sbeCompatible; - } - -protected: - DocumentSourceGroupBase(StringData stageName, - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes = boost::none); - - void initializeFromBson(BSONElement elem); - virtual bool isSpecFieldReserved(StringData fieldName) = 0; - - void doDispose() final; - - /** - * Cleans up any pending memory usage. Throws error, if memory usage is above - * 'maxMemoryUsageBytes' and cannot spill to disk. - * - * Returns true, if the caller should spill to disk, false otherwise. - */ - bool shouldSpillWithAttemptToSaveMemory(); - - /** - * Spill groups map to disk and returns an iterator to the file. Note: Since a sorted $group - * does not exhaust the previous stage before returning, and thus does not maintain as large a - * store of documents at any one time, only an unsorted group can spill to disk. - */ - void spill(); - - /** - * Computes the internal representation of the group key. - */ - Value computeId(const Document& root); - - void processDocument(const Value& id, const Document& root); - - void readyGroups(); - void resetReadyGroups(); - - GetNextResult getNextReadyGroup(); - - void setExecutionStarted() { - _executionStarted = true; - } - - virtual void serializeAdditionalFields( - MutableDocument& out, const SerializationOptions& opts = SerializationOptions{}) const {}; - - // If the expression for the '_id' field represents a non-empty object, we track its fields' - // names in '_idFieldNames'. - std::vector<std::string> _idFieldNames; - // Expressions for the individual fields when '_id' produces a document in the order of - // '_idFieldNames' or the whole expression otherwise. - std::vector<boost::intrusive_ptr<Expression>> _idExpressions; - -private: - GetNextResult getNextSpilled(); - GetNextResult getNextStandard(); - - /** - * If we ran out of memory, finish all the pending operations so that some memory - * can be freed. - */ - void freeMemory(); - - Document makeDocument(const Value& id, const Accumulators& accums, bool mergeableOutput); - - /** - * Converts the internal representation of the group key to the _id shape specified by the - * user. - */ - Value expandId(const Value& val); - - /** - * Returns true if 'dottedPath' is one of the group keys present in '_idExpressions'. - */ - bool pathIncludedInGroupKeys(const std::string& dottedPath) const; - - std::vector<AccumulationStatement> _accumulatedFields; - - bool _doingMerge; - - MemoryUsageTracker _memoryTracker; - - GroupStats _stats; - - /** - * This flag should be set during first execution of getNext() to assert that non-const methods - * that expose internal structures are not called during runtime. - */ - bool _executionStarted; - - // We use boost::optional to defer initialization until the ExpressionContext containing the - // correct comparator is injected, since the groups must be built using the comparator's - // definition of equality. - boost::optional<GroupsMap> _groups; - - std::shared_ptr<Sorter<Value, Value>::File> _file; - std::vector<std::shared_ptr<Sorter<Value, Value>::Iterator>> _sortedFiles; - bool _spilled; - - // Only used when '_spilled' is false. - GroupsMap::iterator _groupsIterator; - - // Only used when '_spilled' is true. - std::unique_ptr<Sorter<Value, Value>::Iterator> _sorterIterator; - - std::pair<Value, Value> _firstPartOfNextGroup; - Accumulators _currentAccumulators; - - bool _sbeCompatible; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_group_test.cpp b/src/mongo/db/pipeline/document_source_group_test.cpp index f3bb0863adf..d3ed11f2031 100644 --- a/src/mongo/db/pipeline/document_source_group_test.cpp +++ b/src/mongo/db/pipeline/document_source_group_test.cpp @@ -48,7 +48,6 @@ #include "mongo/db/pipeline/dependencies.h" #include "mongo/db/pipeline/document_source_group.h" #include "mongo/db/pipeline/document_source_mock.h" -#include "mongo/db/pipeline/document_source_streaming_group.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/query_test_service_context.h" @@ -246,103 +245,6 @@ TEST_F(DocumentSourceGroupTest, ShouldNotReportDottedGroupKeyAsARename) { ASSERT_EQ(modifiedPathsRet.renames.size(), 0UL); } -TEST_F(DocumentSourceGroupTest, GroupRedactsCorrectWithIdNull) { - auto spec = fromjson(R"({ - $group: { - _id: null, - foo: { $count: {} } - } - })"); - auto docSource = DocumentSourceGroup::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$group":{"_id":"?null","HASH<foo>":{"$sum":"?number"}}})", - redact(*docSource)); -} - -TEST_F(DocumentSourceGroupTest, GroupRedactsCorrectWithIdSingleField) { - auto spec = fromjson(R"({ - $group: { - _id: '$foo' - } - })"); - auto docSource = DocumentSourceGroup::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$group": { - "_id": "$HASH<foo>" - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceGroupTest, GroupRedactsCorrectWithIdDocument) { - auto spec = fromjson(R"({ - $group: { - _id: { - x: '$x', - y: '$z' - }, - foo: { - $sum: { - $multiply: ['$a.b', '$c', '$d'] - } - }, - bar: { - $first: '$baz' - } - } - })"); - auto docSource = DocumentSourceGroup::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$group": { - "_id": { - "HASH<x>": "$HASH<x>", - "HASH<y>": "$HASH<z>" - }, - "HASH<foo>": { - "$sum": { - "$multiply": ["$HASH<a>.HASH<b>", "$HASH<c>", "$HASH<d>"] - } - }, - "HASH<bar>": { - "$first": "$HASH<baz>" - } - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceGroupTest, StreamingGroupRedactsCorrectly) { - auto spec = fromjson(R"({ - $_internalStreamingGroup: { - _id: { - a: "$a", - b: "$b" - }, - a: { - $first: '$b' - }, - $monotonicIdFields: [ "a", "b" ] - } - })"); - auto docSource = DocumentSourceStreamingGroup::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalStreamingGroup": { - "_id": { - "HASH<a>": "$HASH<a>", - "HASH<b>": "$HASH<b>" - }, - "HASH<a>": { - "$first": "$HASH<b>" - }, - "$monotonicIdFields": [ "HASH<a>", "HASH<b>" ] - } - })", - redact(*docSource)); -} - BSONObj toBson(const intrusive_ptr<DocumentSource>& source) { vector<Value> arr; source->serializeToArray(arr); @@ -350,65 +252,32 @@ BSONObj toBson(const intrusive_ptr<DocumentSource>& source) { return arr[0].getDocument().toBson(); } -enum class GroupStageType { Default, Streaming }; - class Base : public ServiceContextTest { public: - Base(GroupStageType groupStageType = GroupStageType::Default) + Base() : _opCtx(makeOperationContext()), _ctx(new ExpressionContextForTest(_opCtx.get(), AggregateCommandRequest(NamespaceString(ns), {}))), - _tempDir("DocumentSourceGroupTest"), - _groupStageType(groupStageType) {} + _tempDir("DocumentSourceGroupTest") {} protected: - StringData getStageName() const { - switch (_groupStageType) { - case GroupStageType::Default: - return DocumentSourceGroup::kStageName; - case GroupStageType::Streaming: - return DocumentSourceStreamingGroup::kStageName; - default: - MONGO_UNREACHABLE; - } - } - - virtual boost::optional<size_t> getMaxMemoryUsageBytes() { - return boost::none; - } - - intrusive_ptr<DocumentSource> createFromBson( - BSONElement specElement, intrusive_ptr<ExpressionContext> expressionContext) { - switch (_groupStageType) { - case GroupStageType::Default: - return DocumentSourceGroup::createFromBsonWithMaxMemoryUsage( - std::move(specElement), expressionContext, getMaxMemoryUsageBytes()); - case GroupStageType::Streaming: - return DocumentSourceStreamingGroup::createFromBsonWithMaxMemoryUsage( - std::move(specElement), expressionContext, getMaxMemoryUsageBytes()); - default: - MONGO_UNREACHABLE; - } - } - void createGroup(const BSONObj& spec, bool inShard = false, bool inMongos = false) { - BSONObj namedSpec = BSON(getStageName() << spec); + BSONObj namedSpec = BSON("$group" << spec); BSONElement specElement = namedSpec.firstElement(); intrusive_ptr<ExpressionContextForTest> expressionContext = new ExpressionContextForTest( _opCtx.get(), AggregateCommandRequest(NamespaceString(ns), {})); - expressionContext->allowDiskUse = true; // For $group, 'inShard' implies 'fromMongos' and 'needsMerge'. expressionContext->fromMongos = expressionContext->needsMerge = inShard; expressionContext->inMongos = inMongos; // Won't spill to disk properly if it needs to. expressionContext->tempDir = _tempDir.path(); - _group = createFromBson(specElement, expressionContext); + _group = DocumentSourceGroup::createFromBson(specElement, expressionContext); assertRoundTrips(_group, expressionContext); } - DocumentSourceGroupBase* group() { - return static_cast<DocumentSourceGroupBase*>(_group.get()); + DocumentSourceGroup* group() { + return static_cast<DocumentSourceGroup*>(_group.get()); } /** Assert that iterator state accessors consistently report the source is exhausted. */ void assertEOF(const intrusive_ptr<DocumentSource>& source) const { @@ -430,7 +299,8 @@ private: // $const operators may be introduced in the first serialization. BSONObj spec = toBson(group); BSONElement specElement = spec.firstElement(); - intrusive_ptr<DocumentSource> generated = createFromBson(specElement, expCtx); + intrusive_ptr<DocumentSource> generated = + DocumentSourceGroup::createFromBson(specElement, expCtx); ASSERT_BSONOBJ_EQ(spec, toBson(generated)); } std::unique_ptr<QueryTestServiceContext> _queryServiceContext; @@ -438,7 +308,6 @@ private: intrusive_ptr<ExpressionContextForTest> _ctx; intrusive_ptr<DocumentSource> _group; TempDir _tempDir; - GroupStageType _groupStageType; }; class ParseErrorBase : public Base { @@ -486,9 +355,10 @@ class IdConstantBase : public ExpressionBase { class NonObject : public Base { public: void _doTest() final { - BSONObj spec = BSON(getStageName() << "foo"); + BSONObj spec = BSON("$group" + << "foo"); BSONElement specElement = spec.firstElement(); - ASSERT_THROWS(createFromBson(specElement, ctx()), AssertionException); + ASSERT_THROWS(DocumentSourceGroup::createFromBson(specElement, ctx()), AssertionException); } }; @@ -687,10 +557,8 @@ typedef map<Value, Document, ValueCmp> IdMap; class CheckResultsBase : public Base { public: - CheckResultsBase(GroupStageType groupStageType = GroupStageType::Default) - : Base(groupStageType) {} virtual ~CheckResultsBase() {} - void _doTest() override { + void _doTest() { runSharded(false); runSharded(true); } @@ -703,7 +571,7 @@ public: if (sharded) { sink = createMerger(); // Serialize and re-parse the shard stage. - createGroup(toBson(group())[group()->getSourceName()].Obj(), true); + createGroup(toBson(group())["$group"].Obj(), true); group()->setSource(source.get()); sink->setSource(group()); } @@ -1003,277 +871,6 @@ public: } }; -class StreamingSimple final : public CheckResultsBase { -public: - StreamingSimple() : CheckResultsBase(GroupStageType::Streaming) {} - -private: - deque<DocumentSource::GetNextResult> inputData() final { - return {Document(BSON("a" << 1 << "b" << 1)), - Document(BSON("a" << 1 << "b" << 2)), - Document(BSON("a" << 2 << "b" << 3)), - Document(BSON("a" << 2 << "b" << 1))}; - } - BSONObj groupSpec() final { - return BSON("_id" - << "$a" - << "sum" - << BSON("$sum" - << "$b") - << "$monotonicIdFields" << BSON_ARRAY("_id")); - } - string expectedResultSetString() final { - return "[{_id:1,sum:3},{_id:2,sum:4}]"; - } -}; - -constexpr size_t kBigStringSize = 1024; -const std::string kBigString(kBigStringSize, 'a'); - -class CheckResultsAndSpills : public CheckResultsBase { -public: - CheckResultsAndSpills(GroupStageType groupStageType, uint64_t expectedSpills) - : CheckResultsBase(groupStageType), _expectedSpills(expectedSpills) {} - - void _doTest() final { - for (int sharded = 0; sharded < 2; ++sharded) { - runSharded(sharded); - const auto* groupStats = static_cast<const GroupStats*>(group()->getSpecificStats()); - ASSERT_EQ(groupStats->spills, _expectedSpills); - } - } - -private: - uint64_t _expectedSpills; -}; - -template <GroupStageType groupStageType, uint64_t expectedSpills> -class StreamingSpillTest : public CheckResultsAndSpills { -public: - StreamingSpillTest() : CheckResultsAndSpills(groupStageType, expectedSpills) {} - -private: - static constexpr int kCount = 11; - - deque<DocumentSource::GetNextResult> inputData() final { - deque<DocumentSource::GetNextResult> queue; - for (int i = 0; i < kCount; ++i) { - queue.emplace_back(Document(BSON("a" << i << "b" << kBigString))); - } - return queue; - } - - BSONObj groupSpec() final { - if constexpr (groupStageType == GroupStageType::Streaming) { - return fromjson("{_id: '$a', big_array: {$push: '$b'}, $monotonicIdFields: ['_id']}"); - } else { - return fromjson("{_id: '$a', big_array: {$push: '$b'}}"); - } - } - - boost::optional<size_t> getMaxMemoryUsageBytes() final { - return 10 * kBigStringSize; - } - - BSONObj expectedResultSet() final { - BSONArrayBuilder result; - for (int i = 0; i < kCount; ++i) { - result << BSON("_id" << i << "big_array" << BSON_ARRAY(kBigString)); - } - return result.arr(); - } -}; - -class WithoutStreamingSpills final - : public StreamingSpillTest<GroupStageType::Default, 2 /*expectedSpills*/> {}; -class StreamingDoesNotSpill final - : public StreamingSpillTest<GroupStageType::Streaming, 0 /*expectedSpills*/> {}; - -class StreamingCanSpill final : public CheckResultsAndSpills { -public: - StreamingCanSpill() : CheckResultsAndSpills(GroupStageType::Streaming, 2 /*expectedSpills*/) {} - -private: - static constexpr int kCount = 11; - - deque<DocumentSource::GetNextResult> inputData() final { - deque<DocumentSource::GetNextResult> queue; - for (int i = 0; i < kCount; ++i) { - queue.emplace_back(Document(BSON("x" << 0 << "y" << i << "b" << kBigString))); - } - return queue; - } - - BSONObj groupSpec() final { - auto id = BSON("x" - << "$x" - << "y" - << "$y"); - return BSON("_id" << id << "big_array" - << BSON("$push" - << "$b") - << "$monotonicIdFields" << BSON_ARRAY("x")); - } - - boost::optional<size_t> getMaxMemoryUsageBytes() final { - return 10 * kBigStringSize; - } - - BSONObj expectedResultSet() final { - BSONArrayBuilder result; - for (int i = 0; i < kCount; ++i) { - auto id = BSON("x" << 0 << "y" << i); - result << BSON("_id" << id << "big_array" << BSON_ARRAY(kBigString)); - } - return result.arr(); - } -}; - -class StreamingAlternatingSpillAndNoSpillBatches : public CheckResultsAndSpills { -public: - StreamingAlternatingSpillAndNoSpillBatches() - : CheckResultsAndSpills(GroupStageType::Streaming, 3 /*expectedSpills*/) {} - -private: - static constexpr int kCount = 12; - - deque<DocumentSource::GetNextResult> inputData() final { - deque<DocumentSource::GetNextResult> queue; - for (int i = 0; i < kCount; ++i) { - // For groups with i % 3 == 0 and i % 3 == 1 there should be no spilling, but groups - // with i % 3 == 2 should spill. - for (int j = 0; j < (i % 3) + 1; ++j) { - queue.emplace_back(Document(BSON("a" << i << "b" << kBigString))); - } - } - return queue; - } - - BSONObj groupSpec() final { - return BSON("_id" - << "$a" - << "big_array" - << BSON("$push" - << "$b") - << "$monotonicIdFields" << BSON_ARRAY("_id")); - } - - boost::optional<size_t> getMaxMemoryUsageBytes() final { - return (25 * kBigStringSize) / 10; - } - - BSONObj expectedResultSet() final { - BSONArrayBuilder result; - for (int i = 0; i < kCount; ++i) { - BSONArrayBuilder bigArrayBuilder; - for (int j = 0; j < (i % 3) + 1; ++j) { - bigArrayBuilder << kBigString; - } - result << BSON("_id" << i << "big_array" << bigArrayBuilder.arr()); - } - return result.arr(); - } -}; - -class StreamingComplex final : public CheckResultsBase { -public: - StreamingComplex() : CheckResultsBase(GroupStageType::Streaming) {} - -private: - static constexpr int kCount = 3; - - deque<DocumentSource::GetNextResult> inputData() final { - deque<DocumentSource::GetNextResult> queue; - for (int i = 0; i < kCount; ++i) { - for (int j = 0; j < kCount; ++j) { - for (int k = 0; k < kCount; ++k) { - queue.emplace_back(Document(BSON("x" << i << "y" << j << "z" << k))); - } - } - } - return queue; - } - - BSONObj groupSpec() final { - BSONObj id = BSON("x" - << "$x" - << "y" - << "$y"); - return BSON("_id" << id << "sum" - << BSON("$sum" - << "$z") - << "$monotonicIdFields" << BSON_ARRAY("x")); - } - - boost::optional<size_t> getMaxMemoryUsageBytes() final { - return 10 * kBigStringSize; - } - - BSONObj expectedResultSet() final { - BSONArrayBuilder result; - for (int i = 0; i < kCount; ++i) { - for (int j = 0; j < kCount; ++j) { - result << BSON("_id" << BSON("x" << i << "y" << j) << "sum" - << (kCount * (kCount - 1)) / 2); - } - } - return result.arr(); - } -}; - -class StreamingMultipleMonotonicFields final : public CheckResultsBase { -public: - StreamingMultipleMonotonicFields() : CheckResultsBase(GroupStageType::Streaming) {} - -private: - static constexpr int kCount = 6; - deque<DocumentSource::GetNextResult> inputData() final { - deque<DocumentSource::GetNextResult> queue; - generateInputOutput([&queue](int x, int y) { - for (int i = 0; i < kCount; ++i) { - queue.emplace_back(Document(BSON("x" << x << "y" << y << "z" << i))); - } - }); - return queue; - } - - BSONObj groupSpec() final { - return fromjson( - "{_id: {x: '$x', y: '$y'}, sum: {$sum: '$z'}, $monotonicIdFields: ['x', 'y']}"); - } - - boost::optional<size_t> getMaxMemoryUsageBytes() final { - return 10 * kBigStringSize; - } - - BSONObj expectedResultSet() final { - BSONArrayBuilder result; - const int sum = (kCount * (kCount - 1)) / 2; - generateInputOutput([&](int x, int y) { - result << BSON("_id" << BSON("x" << x << "y" << y) << "sum" << sum); - }); - return result.arr(); - } - - template <typename Callback> - void generateInputOutput(const Callback& callback) { - int x = 0; - int y = 0; - for (int i = 0; i < kCount; ++i) { - callback(x, y); - int state = i % 3; - if (state == 0) { - x++; - } else if (state == 1) { - y++; - } else { - x++; - y++; - } - } - } -}; - class All : public OldStyleSuiteSpecification { public: All() : OldStyleSuiteSpecification("DocumentSourceGroupTests") {} @@ -1314,14 +911,6 @@ public: add<Dependencies>(); add<StringConstantIdAndAccumulatorExpressions>(); add<ArrayConstantAccumulatorExpression>(); - - add<StreamingSimple>(); - add<WithoutStreamingSpills>(); - add<StreamingDoesNotSpill>(); - add<StreamingCanSpill>(); - add<StreamingAlternatingSpillAndNoSpillBatches>(); - add<StreamingComplex>(); - add<StreamingMultipleMonotonicFields>(); #if 0 // Disabled tests until SERVER-23318 is implemented. add<StreamingOptimization>(); diff --git a/src/mongo/db/pipeline/document_source_index_stats.cpp b/src/mongo/db/pipeline/document_source_index_stats.cpp index bd4ee6bf760..e87b614e59a 100644 --- a/src/mongo/db/pipeline/document_source_index_stats.cpp +++ b/src/mongo/db/pipeline/document_source_index_stats.cpp @@ -51,10 +51,7 @@ const char* DocumentSourceIndexStats::getSourceName() const { DocumentSource::GetNextResult DocumentSourceIndexStats::doGetNext() { if (_indexStats.empty()) { _indexStats = pExpCtx->mongoProcessInterface->getIndexStats( - pExpCtx->opCtx, - pExpCtx->ns, - _processName, - serverGlobalParams.clusterRole != ClusterRole::None); + pExpCtx->opCtx, pExpCtx->ns, _processName, pExpCtx->fromMongos); _indexStatsIter = _indexStats.cbegin(); } @@ -78,7 +75,8 @@ intrusive_ptr<DocumentSource> DocumentSourceIndexStats::createFromBson( return new DocumentSourceIndexStats(pExpCtx); } -Value DocumentSourceIndexStats::serialize(const SerializationOptions& opts) const { +Value DocumentSourceIndexStats::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { return Value(DOC(getSourceName() << Document())); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_index_stats.h b/src/mongo/db/pipeline/document_source_index_stats.h index f56e5e22059..bd5b7577365 100644 --- a/src/mongo/db/pipeline/document_source_index_stats.h +++ b/src/mongo/db/pipeline/document_source_index_stats.h @@ -75,7 +75,7 @@ public: // virtuals from DocumentSource const char* getSourceName() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { StageConstraints constraints(StreamType::kStreaming, diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp deleted file mode 100644 index e24bf112bcf..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/db/pipeline/document_source_internal_all_collection_stats.h" - -namespace mongo { - -using boost::intrusive_ptr; - -DocumentSourceInternalAllCollectionStats::DocumentSourceInternalAllCollectionStats( - const boost::intrusive_ptr<ExpressionContext>& pExpCtx, - DocumentSourceInternalAllCollectionStatsSpec spec) - : DocumentSource(kStageNameInternal, pExpCtx), - _internalAllCollectionStatsSpec(std::move(spec)) {} - -REGISTER_DOCUMENT_SOURCE(_internalAllCollectionStats, - DocumentSourceInternalAllCollectionStats::LiteParsed::parse, - DocumentSourceInternalAllCollectionStats::createFromBsonInternal, - AllowedWithApiStrict::kInternal); - -DocumentSource::GetNextResult DocumentSourceInternalAllCollectionStats::doGetNext() { - if (!_catalogDocs) { - _catalogDocs = pExpCtx->mongoProcessInterface->listCatalog(pExpCtx->opCtx); - } - - while (!_catalogDocs->empty()) { - BSONObj obj(std::move(_catalogDocs->front())); - NamespaceString nss(obj["ns"].String()); - - _catalogDocs->pop_front(); - - // Avoid computing stats for collections that do not match the absorbed filter on the 'ns' - // field. - if (_absorbedMatch && !_absorbedMatch->getMatchExpression()->matchesBSON(std::move(obj))) { - continue; - } - - if (const auto& stats = _internalAllCollectionStatsSpec.getStats()) { - try { - return { - Document{DocumentSourceCollStats::makeStatsForNs(pExpCtx, nss, stats.get())}}; - } catch (const ExceptionFor<ErrorCodes::CommandNotSupportedOnView>&) { - // We don't want to retrieve data for views, only for collections. - continue; - } - } - } - - return GetNextResult::makeEOF(); -} - -Pipeline::SourceContainer::iterator DocumentSourceInternalAllCollectionStats::doOptimizeAt( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - invariant(*itr == this); - - if (std::next(itr) == container->end()) { - return container->end(); - } - - // Attempt to internalize any predicates of a $match upon the "ns" field. - auto nextMatch = dynamic_cast<DocumentSourceMatch*>((*std::next(itr)).get()); - - if (!nextMatch) { - return std::next(itr); - } - - auto splitMatch = std::move(*nextMatch).splitSourceBy({"ns"}, {}); - invariant(splitMatch.first || splitMatch.second); - - // Remove the original $match. - container->erase(std::next(itr)); - - // Absorb the part of $match that is dependant on 'ns' - if (splitMatch.second) { - if (!_absorbedMatch) { - _absorbedMatch = std::move(splitMatch.second); - } else { - // We have already absorbed a $match. We need to join it with splitMatch.second. - _absorbedMatch->joinMatchWith(std::move(splitMatch.second)); - } - } - - // splitMatch.first is independent of 'ns'. Put it back on the pipeline. - if (splitMatch.first) { - container->insert(std::next(itr), std::move(splitMatch.first)); - return std::next(itr); - } else { - // There may be further optimization between this stage and the new neighbor, so we return - // an iterator pointing to ourself. - return itr; - } -} - -void DocumentSourceInternalAllCollectionStats::serializeToArray( - std::vector<Value>& array, const SerializationOptions& opts) const { - auto explain = opts.verbosity; - if (explain) { - BSONObjBuilder bob; - _internalAllCollectionStatsSpec.serialize(&bob, opts); - if (_absorbedMatch) { - bob.append("match", _absorbedMatch->getQuery()); - } - auto doc = Document{{getSourceName(), bob.obj()}}; - array.push_back(Value(doc)); - } else { - array.push_back(serialize(opts)); - if (_absorbedMatch) { - _absorbedMatch->serializeToArray(array); - } - } -} - -intrusive_ptr<DocumentSource> DocumentSourceInternalAllCollectionStats::createFromBsonInternal( - BSONElement elem, const intrusive_ptr<ExpressionContext>& pExpCtx) { - uassert(6789103, - str::stream() << "$_internalAllCollectionStats must take a nested object but found: " - << elem, - elem.type() == BSONType::Object); - - uassert(6789104, - "The $_internalAllCollectionStats stage must be run on the admin database", - pExpCtx->ns.isAdminDB() && pExpCtx->ns.isCollectionlessAggregateNS()); - - auto spec = DocumentSourceInternalAllCollectionStatsSpec::parse( - IDLParserErrorContext(kStageNameInternal), elem.embeddedObject()); - - return make_intrusive<DocumentSourceInternalAllCollectionStats>(pExpCtx, std::move(spec)); -} - -const char* DocumentSourceInternalAllCollectionStats::getSourceName() const { - return kStageNameInternal.rawData(); -} - -Value DocumentSourceInternalAllCollectionStats::serialize(const SerializationOptions& opts) const { - return Value(Document{{getSourceName(), _internalAllCollectionStatsSpec.toBSON(opts)}}); -} -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h deleted file mode 100644 index 1648939ddd1..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include "mongo/db/pipeline/document_source.h" -#include "mongo/db/pipeline/document_source_coll_stats.h" -#include "mongo/db/pipeline/document_source_internal_all_collection_stats_gen.h" -#include "mongo/db/pipeline/document_source_match.h" - -namespace mongo { - -/** - * This aggregation stage is the ‘$_internalAllCollectionStats´. It takes no arguments. Its - * response will be a cursor, each document of which represents the collection statistics for a - * single collection for all the existing collections. - * - * When executing the '$_internalAllCollectionStats' aggregation stage, we will need to obtain the - * catalog containing all collections namespaces. - * - * Then, for each collection, we will call `makeStatsForNs` method from DocumentSourceCollStats that - * will retrieve all storage stats for that particular collection. - */ -class DocumentSourceInternalAllCollectionStats final : public DocumentSource { -public: - static constexpr StringData kStageNameInternal = "$_internalAllCollectionStats"_sd; - - DocumentSourceInternalAllCollectionStats(const boost::intrusive_ptr<ExpressionContext>& pExpCtx, - DocumentSourceInternalAllCollectionStatsSpec spec); - - class LiteParsed final : public LiteParsedDocumentSource { - public: - static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, - const BSONElement& spec) { - return std::make_unique<LiteParsed>(spec.fieldName()); - } - - explicit LiteParsed(std::string parseTimeName) - : LiteParsedDocumentSource(std::move(parseTimeName)) {} - - stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { - return stdx::unordered_set<NamespaceString>(); - } - - PrivilegeVector requiredPrivileges(bool isMongos, - bool bypassDocumentValidation) const final { - return { - Privilege(ResourcePattern::forClusterResource(), ActionType::allCollectionStats)}; - } - - bool isInitialSource() const final { - return true; - } - }; - - const char* getSourceName() const final; - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - - StageConstraints constraints(Pipeline::SplitState pipeState) const final { - StageConstraints constraints(StreamType::kStreaming, - PositionRequirement::kFirst, - HostTypeRequirement::kAnyShard, - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kNotAllowed, - TransactionRequirement::kNotAllowed, - LookupRequirement::kAllowed, - UnionRequirement::kAllowed); - - constraints.isIndependentOfAnyCollection = true; - constraints.requiresInputDocSource = false; - return constraints; - } - - boost::optional<DistributedPlanLogic> distributedPlanLogic() final { - return boost::none; - } - - static boost::intrusive_ptr<DocumentSource> createFromBsonInternal( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - - Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container) final; - - void serializeToArray( - std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const final override; - -private: - GetNextResult doGetNext() final; - - // The specification object given to $_internalAllCollectionStats containing user specified - // options. - const DocumentSourceInternalAllCollectionStatsSpec _internalAllCollectionStatsSpec; - boost::optional<std::deque<BSONObj>> _catalogDocs; - - // A $match stage can be absorbed in order to avoid unnecessarily computing the stats for - // collections that do not match that predicate. - boost::intrusive_ptr<DocumentSourceMatch> _absorbedMatch; -}; -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl deleted file mode 100644 index c473a5e5ba3..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (C) 2022-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. -# - -global: - cpp_namespace: "mongo" - cpp_includes: - - "mongo/db/pipeline/document_source_parsing_validators.h" - -imports: - - "mongo/idl/basic_types.idl" - - "mongo/db/pipeline/storage_stats_spec.idl" - - "mongo/db/pipeline/document_source_coll_stats.idl" - -structs: - DocumentSourceInternalAllCollectionStatsSpec: - description: Specification for an $_internalAllCollectionStats stage. - strict: true - query_shape_component: true - fields: - stats: - description: Specification for a $collStats stage. - type: DocumentSourceCollStatsSpec - optional: true - query_shape: literal diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats_test.cpp b/src/mongo/db/pipeline/document_source_internal_all_collection_stats_test.cpp deleted file mode 100644 index e303febfacb..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_all_collection_stats_test.cpp +++ /dev/null @@ -1,143 +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 "mongo/db/pipeline/document_source_internal_all_collection_stats.h" -#include "mongo/db/pipeline/document_source_internal_all_collection_stats_gen.h" -#include <boost/cstdint.hpp> -#include <cstdint> - -#include <boost/move/utility_core.hpp> -#include <boost/smart_ptr/intrusive_ptr.hpp> - -#include "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/pipeline/document_source_coll_stats.h" -#include "mongo/db/pipeline/document_source_coll_stats_gen.h" -#include "mongo/db/pipeline/storage_stats_spec_gen.h" -#include "mongo/unittest/unittest.h" -#include "mongo/util/intrusive_counter.h" - -namespace mongo { -namespace { -using DocumentSourceInternalAllCollectionStatsTest = AggregationContextFixture; - -auto representativeShape(const DocumentSourceInternalAllCollectionStats& allCollStatsStage) { - SerializationOptions opts{LiteralSerializationPolicy::kToRepresentativeParseableValue}; - return allCollStatsStage.serialize(opts).getDocument().toBson(); -} - -TEST_F(DocumentSourceInternalAllCollectionStatsTest, QueryShape) { - auto innerSpec = DocumentSourceCollStatsSpec(); - auto spec = DocumentSourceInternalAllCollectionStatsSpec(); - spec.setStats(innerSpec); - - auto stage = make_intrusive<DocumentSourceInternalAllCollectionStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalAllCollectionStats":{"stats":{}}})", - redact(*stage)); - - innerSpec.setCount(BSONObj()); - innerSpec.setQueryExecStats(BSONObj()); - spec.setStats(innerSpec); - stage = make_intrusive<DocumentSourceInternalAllCollectionStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalAllCollectionStats":{"stats":{"count":{},"queryExecStats":{}}}})", - redact(*stage)); - - auto latencyStats = LatencyStatsSpec(); - latencyStats.setHistograms(true); - innerSpec.setLatencyStats(latencyStats); - spec.setStats(innerSpec); - stage = make_intrusive<DocumentSourceInternalAllCollectionStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalAllCollectionStats": { - "stats": { - "latencyStats": { - "histograms": true - }, - "count": {}, - "queryExecStats": {} - } - } - })", - redact(*stage)); - - auto storageStats = StorageStatsSpec(); - storageStats.setScale(2); - storageStats.setVerbose(true); - innerSpec.setStorageStats(storageStats); - spec.setStats(innerSpec); - stage = make_intrusive<DocumentSourceInternalAllCollectionStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalAllCollectionStats": { - "stats": { - "latencyStats": { - "histograms": true - }, - "storageStats": { - "scale": "?number", - "verbose": true, - "waitForLock": true, - "numericOnly": false - }, - "count": {}, - "queryExecStats": {} - } - } - })", - redact(*stage)); - - storageStats.setWaitForLock(false); - storageStats.setNumericOnly(false); - innerSpec.setStorageStats(storageStats); - spec.setStats(innerSpec); - stage = make_intrusive<DocumentSourceInternalAllCollectionStats>(getExpCtx(), spec); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalAllCollectionStats": { - "stats": { - "latencyStats": { - "histograms": true - }, - "storageStats": { - "scale": "?number", - "verbose": true, - "waitForLock": false, - "numericOnly": false - }, - "count": {}, - "queryExecStats": {} - } - } - })", - redact(*stage)); -} -} // namespace -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.cpp b/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.cpp index 2d499e0d88c..51b0dee8ac1 100644 --- a/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.cpp +++ b/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.cpp @@ -95,9 +95,9 @@ DocumentSource::GetNextResult DocumentSourceInternalApplyOplogUpdate::doGetNext( return Document(doc.getObject()); } -Value DocumentSourceInternalApplyOplogUpdate::serialize(const SerializationOptions& opts) const { - return Value(Document{ - {kStageName, Document{{kOplogUpdateFieldName, opts.serializeLiteral(_oplogUpdate)}}}}); +Value DocumentSourceInternalApplyOplogUpdate::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(Document{{kStageName, Document{{kOplogUpdateFieldName, _oplogUpdate}}}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.h b/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.h index 89860c90efd..b4801181bd7 100644 --- a/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.h +++ b/src/mongo/db/pipeline/document_source_internal_apply_oplog_update.h @@ -67,7 +67,6 @@ public: constraints.canSwapWithMatch = false; constraints.canSwapWithSkippingOrLimitingStage = true; constraints.isAllowedWithinUpdatePipeline = true; - constraints.checkExistenceForDiffInsertOperations = true; constraints.isIndependentOfAnyCollection = false; return constraints; } @@ -81,7 +80,8 @@ public: } private: - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize( + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override; GetNextResult doGetNext() override; diff --git a/src/mongo/db/pipeline/document_source_internal_apply_oplog_update_test.cpp b/src/mongo/db/pipeline/document_source_internal_apply_oplog_update_test.cpp index a297c329ebb..fc95d45e6fa 100644 --- a/src/mongo/db/pipeline/document_source_internal_apply_oplog_update_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_apply_oplog_update_test.cpp @@ -240,25 +240,5 @@ TEST_F(DocumentSourceInternalApplyOplogUpdateTest, ShouldErrorOnInvalidDiffs) { } } -TEST_F(DocumentSourceInternalApplyOplogUpdateTest, RedactsCorrectly) { - auto spec = fromjson(R"({ - $_internalApplyOplogUpdate: { - oplogUpdate: { - $v: 2, - diff: { sa: [] } - } - } - })"); - auto docSource = - DocumentSourceInternalApplyOplogUpdate::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalApplyOplogUpdate": { - "oplogUpdate":"?object" - } - })", - redact(*docSource)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.cpp b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.cpp index b44d2bb52cd..f643bcb32f3 100644 --- a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.cpp +++ b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.cpp @@ -139,16 +139,15 @@ DocumentSource::GetNextResult DocumentSourceInternalGeoNearDistance::doGetNext() return next; } -Value DocumentSourceInternalGeoNearDistance::serialize(const SerializationOptions& opts) const { +Value DocumentSourceInternalGeoNearDistance::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument out; - out.setField(DocumentSourceInternalGeoNearDistance::kNearFieldName, - opts.serializeLiteral(_coords)); - out.setField(DocumentSourceInternalGeoNearDistance::kKeyFieldName, - Value(opts.serializeFieldPathFromString(_key))); + out.setField(DocumentSourceInternalGeoNearDistance::kNearFieldName, Value(_coords)); + out.setField(DocumentSourceInternalGeoNearDistance::kKeyFieldName, Value(_key)); out.setField(DocumentSourceInternalGeoNearDistance::kDistanceFieldFieldName, - Value(opts.serializeFieldPath(_distanceField))); + Value(_distanceField.fullPath())); out.setField(DocumentSourceInternalGeoNearDistance::kDistanceMultiplierFieldName, - opts.serializeLiteral(_distanceMultiplier)); + Value(_distanceMultiplier)); return Value(DOC(getSourceName() << out.freeze())); } diff --git a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h index 5dc81ea7401..1083d58bb7f 100644 --- a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h +++ b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h @@ -76,7 +76,9 @@ public: } DocumentSource::GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{_distanceField.fullPath()}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, + std::set<std::string>{_distanceField.fullPath()}, + {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() override { @@ -84,7 +86,8 @@ public: } private: - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize( + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override; GetNextResult doGetNext() override; diff --git a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance_test.cpp b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance_test.cpp index 67f7300fd2b..687fe4f9e7a 100644 --- a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance_test.cpp @@ -32,7 +32,6 @@ #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" -#include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h" #include "mongo/db/pipeline/document_source_mock.h" @@ -146,31 +145,5 @@ TEST_F(DocumentSourceInternalGeoNearDistanceTest, DistanceBetweenTwoMixedPointsS ASSERT_APPROX_EQUAL(doc["dist"].coerceToDouble(), meterToLatDegree, 300); } -TEST_F(DocumentSourceInternalGeoNearDistanceTest, RedactsCorrectly) { - BSONObj computeGeoSpec = fromjson(R"( - { $_internalComputeGeoNearDistance: { - near: { - type: "Point", - coordinates: [0, 1] - }, - key: "loc", - distanceMultiplier: 1, - distanceField: "dist" - }})"); - auto geoDist = DocumentSourceInternalGeoNearDistance::createFromBson( - computeGeoSpec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalComputeGeoNearDistance": { - "near": "?object", - "key": "HASH<loc>", - "distanceField": "HASH<dist>", - "distanceMultiplier": "?number" - } - })", - redact(*geoDist, true)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp index 9d52a0f7124..be9784f5346 100644 --- a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp +++ b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.cpp @@ -33,7 +33,6 @@ #include "mongo/bson/bsonobj.h" #include "mongo/db/exec/document_value/document.h" -#include "mongo/db/index/index_descriptor.h" #include "mongo/db/list_indexes_gen.h" #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" @@ -147,13 +146,11 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalConvertBucketIndexSta } Value DocumentSourceInternalConvertBucketIndexStats::serialize( - const SerializationOptions& opts) const { + boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument out; - out.addField(timeseries::kTimeFieldName, - Value{opts.serializeFieldPathFromString(_timeseriesOptions.timeField)}); + out.addField(timeseries::kTimeFieldName, Value{_timeseriesOptions.timeField}); if (_timeseriesOptions.metaField) { - out.addField(timeseries::kMetaFieldName, - Value{opts.serializeFieldPathFromString(*_timeseriesOptions.metaField)}); + out.addField(timeseries::kMetaFieldName, Value{*_timeseriesOptions.metaField}); } return Value(DOC(getSourceName() << out.freeze())); } diff --git a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.h b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.h index 5e7b4c09959..498518ee951 100644 --- a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.h +++ b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.h @@ -77,7 +77,7 @@ public: ChangeStreamRequirement::kDenylist}; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; boost::optional<DistributedPlanLogic> distributedPlanLogic() final { return boost::none; diff --git a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats_test.cpp b/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats_test.cpp deleted file mode 100644 index d0b1bddbe0b..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_convert_bucket_index_stats_test.cpp +++ /dev/null @@ -1,63 +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 "mongo/db/exec/document_value/document_value_test_util.h" -#include "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/pipeline/document_source_internal_convert_bucket_index_stats.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/unittest/bson_test_util.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace { -using InternalConvertBucketIndexStatsTest = AggregationContextFixture; - -TEST_F(InternalConvertBucketIndexStatsTest, QueryShapeAndRedaction) { - auto expCtx = make_intrusive<ExpressionContextForTest>(); - - auto stage = std::make_unique<DocumentSourceInternalConvertBucketIndexStats>( - expCtx, TimeseriesConversionOptions{"timefield"}); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$_internalConvertBucketIndexStats":{"timeField":"HASH<timefield>"}})", - redact(*stage)); - - std::string metaField = "metafield"; - stage = std::make_unique<DocumentSourceInternalConvertBucketIndexStats>( - expCtx, TimeseriesConversionOptions{"timefield", metaField}); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalConvertBucketIndexStats": { - "timeField": "HASH<timefield>", - "metaField": "HASH<metafield>" - } - })", - redact(*stage)); -} -} // namespace -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.cpp b/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.cpp index 6cd368fde1a..af9d4c297b1 100644 --- a/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.cpp +++ b/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.cpp @@ -60,7 +60,8 @@ DocumentSource::GetNextResult DocumentSourceInternalInhibitOptimization::doGetNe return pSource->getNext(); } -Value DocumentSourceInternalInhibitOptimization::serialize(const SerializationOptions& opts) const { +Value DocumentSourceInternalInhibitOptimization::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { return Value(Document{{getSourceName(), Value{Document{}}}}); } diff --git a/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.h b/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.h index 80d74219af0..79e39f865d9 100644 --- a/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.h +++ b/src/mongo/db/pipeline/document_source_internal_inhibit_optimization.h @@ -71,7 +71,7 @@ public: private: GetNextResult doGetNext() final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_shard_filter.cpp b/src/mongo/db/pipeline/document_source_internal_shard_filter.cpp index f63125ff9b6..a8b112ae834 100644 --- a/src/mongo/db/pipeline/document_source_internal_shard_filter.cpp +++ b/src/mongo/db/pipeline/document_source_internal_shard_filter.cpp @@ -95,7 +95,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalShardFilter::doOptimiz return ret; } -Value DocumentSourceInternalShardFilter::serialize(const SerializationOptions& opts) const { +Value DocumentSourceInternalShardFilter::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { return Value(DOC(getSourceName() << Document())); } diff --git a/src/mongo/db/pipeline/document_source_internal_shard_filter.h b/src/mongo/db/pipeline/document_source_internal_shard_filter.h index a8b88869321..e223af734a9 100644 --- a/src/mongo/db/pipeline/document_source_internal_shard_filter.h +++ b/src/mongo/db/pipeline/document_source_internal_shard_filter.h @@ -64,8 +64,8 @@ public: ChangeStreamRequirement::kDenylist); } - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize( + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override; boost::optional<DistributedPlanLogic> distributedPlanLogic() override { return boost::none; diff --git a/src/mongo/db/pipeline/document_source_internal_shardserver_info.cpp b/src/mongo/db/pipeline/document_source_internal_shardserver_info.cpp deleted file mode 100644 index 6e3fe14c578..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_shardserver_info.cpp +++ /dev/null @@ -1,67 +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 "mongo/platform/basic.h" - -#include "mongo/db/pipeline/document_source_internal_shardserver_info.h" -#include "mongo/db/pipeline/pipeline.h" - -namespace mongo { - -REGISTER_DOCUMENT_SOURCE(_internalShardServerInfo, - DocumentSourceInternalShardServerInfo::LiteParsed::parse, - DocumentSourceInternalShardServerInfo::createFromBson, - AllowedWithApiStrict::kNeverInVersion1); - -boost::intrusive_ptr<DocumentSource> DocumentSourceInternalShardServerInfo::createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - uassert(ErrorCodes::TypeMismatch, - str::stream() << "$_internalShardServerInfo must take an empty object but found: " - << elem, - elem.type() == BSONType::Object && elem.Obj().isEmpty()); - - return new DocumentSourceInternalShardServerInfo(expCtx); -} - -DocumentSource::GetNextResult DocumentSourceInternalShardServerInfo::doGetNext() { - if (!_didEmit) { - auto shardName = pExpCtx->mongoProcessInterface->getShardName(pExpCtx->opCtx); - auto hostAndPort = pExpCtx->mongoProcessInterface->getHostAndPort(pExpCtx->opCtx); - _didEmit = true; - return DocumentSource::GetNextResult(DOC("shard" << shardName << "host" << hostAndPort)); - } - - return DocumentSource::GetNextResult::makeEOF(); -} - -Value DocumentSourceInternalShardServerInfo::serialize(const SerializationOptions& opts) const { - return Value(Document{{getSourceName(), Value{Document{{}}}}}); -} - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_shardserver_info.h b/src/mongo/db/pipeline/document_source_internal_shardserver_info.h deleted file mode 100644 index 5a1cbaa8f63..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_shardserver_info.h +++ /dev/null @@ -1,109 +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. - */ - -#pragma once - -#include "mongo/db/pipeline/document_source.h" - -namespace mongo { - -/** - * An internal stage available for testing. Gets the host and shard name for every shard server in - * the cluster. - */ -class DocumentSourceInternalShardServerInfo final : public DocumentSource { -public: - class LiteParsed : public LiteParsedDocumentSource { - public: - static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, - const BSONElement& spec) { - return std::make_unique<LiteParsed>(spec.fieldName()); - } - - LiteParsed(std::string parseTimeName) - : LiteParsedDocumentSource(std::move(parseTimeName)) {} - - stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { - return stdx::unordered_set<NamespaceString>(); - } - - PrivilegeVector requiredPrivileges(bool isMongos, - bool bypassDocumentValidation) const final { - return {}; - } - - bool allowedToPassthroughFromMongos() const final { - return false; - } - }; - - static constexpr StringData kStageName = "$_internalShardServerInfo"_sd; - - static boost::intrusive_ptr<DocumentSource> createFromBson( - BSONElement, const boost::intrusive_ptr<ExpressionContext>&); - - static boost::intrusive_ptr<DocumentSource> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx) { - return new DocumentSourceInternalShardServerInfo(expCtx); - } - - const char* getSourceName() const final { - return kStageName.rawData(); - } - - boost::optional<DistributedPlanLogic> distributedPlanLogic() final { - return boost::none; - } - - StageConstraints constraints(Pipeline::SplitState pipeState) const final { - StageConstraints constraints{StreamType::kStreaming, - PositionRequirement::kFirst, - HostTypeRequirement::kAllShardServers, - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kNotAllowed, - TransactionRequirement::kNotAllowed, - LookupRequirement::kNotAllowed, - UnionRequirement::kAllowed}; - constraints.isIndependentOfAnyCollection = true; - constraints.requiresInputDocSource = false; - return constraints; - } - -private: - DocumentSourceInternalShardServerInfo(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : DocumentSource(kStageName, expCtx) {} - - GetNextResult doGetNext() final; - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - - bool _didEmit = false; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_shred_documents.cpp b/src/mongo/db/pipeline/document_source_internal_shred_documents.cpp deleted file mode 100644 index 3bb636c5004..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_shred_documents.cpp +++ /dev/null @@ -1,76 +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. - */ - -#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery - -#include "mongo/platform/basic.h" - -#include "mongo/db/pipeline/document_source_internal_shred_documents.h" - -#include "mongo/db/exec/document_value/document.h" -#include "mongo/logv2/log.h" - -namespace mongo { - -REGISTER_DOCUMENT_SOURCE(_internalShredDocuments, - LiteParsedDocumentSourceDefault::parse, - DocumentSourceInternalShredDocuments::createFromBson, - AllowedWithApiStrict::kNeverInVersion1); - -DocumentSourceInternalShredDocuments::DocumentSourceInternalShredDocuments( - const boost::intrusive_ptr<ExpressionContext>& pExpCtx) - : DocumentSource(kStageName, pExpCtx) {} - -DocumentSource::GetNextResult DocumentSourceInternalShredDocuments::doGetNext() { - auto next = pSource->getNext(); - if (next.isAdvanced()) { - return GetNextResult(next.getDocument().shred()); - } - return next; -} - -Value DocumentSourceInternalShredDocuments::serialize(const SerializationOptions& opts) const { - return Value(DOC(getSourceName() << Document())); -} - -boost::intrusive_ptr<DocumentSource> DocumentSourceInternalShredDocuments::createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - uassert(7997500, - "$_internalShredDocuments specification must be an object", - elem.type() == BSONType::Object); - uassert(7997501, "$_internalShredDocuments specification must be empty", elem.Obj().isEmpty()); - return DocumentSourceInternalShredDocuments::create(expCtx); -} - -boost::intrusive_ptr<DocumentSource> DocumentSourceInternalShredDocuments::create( - const boost::intrusive_ptr<ExpressionContext>& expCtx) { - return make_intrusive<DocumentSourceInternalShredDocuments>(expCtx); -} - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_shred_documents.h b/src/mongo/db/pipeline/document_source_internal_shred_documents.h deleted file mode 100644 index ebb095d404c..00000000000 --- a/src/mongo/db/pipeline/document_source_internal_shred_documents.h +++ /dev/null @@ -1,83 +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. - */ - -#pragma once - -#include "mongo/db/pipeline/document_source.h" - -namespace mongo { - -/** - * Converts documents into a shredded format to avoid performance regressions from the switch to - * having a field cache in 4.4. - */ -class DocumentSourceInternalShredDocuments final : public DocumentSource { -public: - static constexpr StringData kStageName = "$_internalShredDocuments"_sd; - - static boost::intrusive_ptr<DocumentSource> createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - static boost::intrusive_ptr<DocumentSource> create( - const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - - DocumentSourceInternalShredDocuments(const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - - const char* getSourceName() const override { - return kStageName.rawData(); - } - - StageConstraints constraints(Pipeline::SplitState pipeState) const override { - StageConstraints constraints(StreamType::kStreaming, - PositionRequirement::kNone, - HostTypeRequirement::kNone, - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kAllowed, - TransactionRequirement::kAllowed, - LookupRequirement::kAllowed, - UnionRequirement::kAllowed, - ChangeStreamRequirement::kAllowlist); - constraints.isAllowedWithinUpdatePipeline = true; - return constraints; - } - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - - boost::optional<DistributedPlanLogic> distributedPlanLogic() override { - return boost::none; - } - - DepsTracker::State getDependencies(DepsTracker* deps) const override { - return DepsTracker::State::SEE_NEXT; - } - -private: - GetNextResult doGetNext() override; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_split_pipeline.cpp b/src/mongo/db/pipeline/document_source_internal_split_pipeline.cpp index 73ad3e7ffd2..8f5f9927ada 100644 --- a/src/mongo/db/pipeline/document_source_internal_split_pipeline.cpp +++ b/src/mongo/db/pipeline/document_source_internal_split_pipeline.cpp @@ -86,7 +86,8 @@ DocumentSource::GetNextResult DocumentSourceInternalSplitPipeline::doGetNext() { return pSource->getNext(); } -Value DocumentSourceInternalSplitPipeline::serialize(const SerializationOptions& opts) const { +Value DocumentSourceInternalSplitPipeline::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { std::string mergeTypeString; switch (_mergeType) { diff --git a/src/mongo/db/pipeline/document_source_internal_split_pipeline.h b/src/mongo/db/pipeline/document_source_internal_split_pipeline.h index ff752d1e22d..6ac88f8a82d 100644 --- a/src/mongo/db/pipeline/document_source_internal_split_pipeline.h +++ b/src/mongo/db/pipeline/document_source_internal_split_pipeline.h @@ -63,10 +63,6 @@ public: return DistributedPlanLogic{nullptr, this, boost::none}; } - DepsTracker::State getDependencies(DepsTracker* deps) const final { - return DepsTracker::State::SEE_NEXT; - } - StageConstraints constraints(Pipeline::SplitState pipeState) const final { return {StreamType::kStreaming, PositionRequirement::kNone, @@ -85,7 +81,8 @@ private: : DocumentSource(kStageName, expCtx), _mergeType(mergeType) {} GetNextResult doGetNext() final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; HostTypeRequirement _mergeType = HostTypeRequirement::kNone; }; diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp index d38f5992eaf..272d7a1a80c 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp @@ -29,7 +29,6 @@ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery -#include "mongo/db/pipeline/document_source_sequential_document_cache.h" #include <algorithm> #include <iterator> @@ -56,7 +55,6 @@ #include "mongo/db/pipeline/document_source_sample.h" #include "mongo/db/pipeline/document_source_single_document_transformation.h" #include "mongo/db/pipeline/document_source_sort.h" -#include "mongo/db/pipeline/document_source_streaming_group.h" #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/query/query_planner_common.h" @@ -232,7 +230,6 @@ boost::intrusive_ptr<DocumentSourceGroup> createBucketGroupForReorder( void optimizePrefix(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { auto prefix = Pipeline::SourceContainer(container->begin(), itr); Pipeline::optimizeContainer(&prefix); - Pipeline::optimizeEachStage(&prefix); container->erase(container->begin(), itr); container->splice(itr, prefix); } @@ -246,39 +243,9 @@ DocumentSourceInternalUnpackBucket::DocumentSourceInternalUnpackBucket( bool assumeNoMixedSchemaData) : DocumentSource(kStageNameInternal, expCtx), _assumeNoMixedSchemaData(assumeNoMixedSchemaData), - _bucketUnpacker(std::move(bucketUnpacker)), _bucketMaxSpanSeconds{bucketMaxSpanSeconds} {} -DocumentSourceInternalUnpackBucket::DocumentSourceInternalUnpackBucket( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - BucketUnpacker bucketUnpacker, - int bucketMaxSpanSeconds, - const boost::optional<BSONObj>& eventFilterBson, - const boost::optional<BSONObj>& wholeBucketFilterBson, - bool assumeNoMixedSchemaData) - : DocumentSourceInternalUnpackBucket( - expCtx, std::move(bucketUnpacker), bucketMaxSpanSeconds, assumeNoMixedSchemaData) { - if (eventFilterBson) { - _eventFilterBson = eventFilterBson->getOwned(); - _eventFilter = - uassertStatusOK(MatchExpressionParser::parse(_eventFilterBson, - pExpCtx, - ExtensionsCallbackNoop(), - Pipeline::kAllowedMatcherFeatures)); - _eventFilterDeps = {}; - _eventFilter->addDependencies(&_eventFilterDeps); - } - if (wholeBucketFilterBson) { - _wholeBucketFilterBson = wholeBucketFilterBson->getOwned(); - _wholeBucketFilter = - uassertStatusOK(MatchExpressionParser::parse(_wholeBucketFilterBson, - pExpCtx, - ExtensionsCallbackNoop(), - Pipeline::kAllowedMatcherFeatures)); - } -} - boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createFromBsonInternal( BSONElement specElem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { uassert(5346500, @@ -288,20 +255,14 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF // If neither "include" nor "exclude" is specified, the default is "exclude": [] and // if that's the case, no field will be added to 'bucketSpec.fieldSet' in the for-loop below. + BucketUnpacker::Behavior unpackerBehavior = BucketUnpacker::Behavior::kExclude; BucketSpec bucketSpec; - // Use extended-range support if any individual collection requires it, even if 'specElem' - // doesn't mention this flag. - if (expCtx->getRequiresTimeseriesExtendedRangeSupport()) { - bucketSpec.setUsesExtendedRange(true); - } auto hasIncludeExclude = false; auto hasTimeField = false; auto hasBucketMaxSpanSeconds = false; auto bucketMaxSpanSeconds = 0; auto assumeClean = false; std::vector<std::string> computedMetaProjFields; - boost::optional<BSONObj> eventFilterBson; - boost::optional<BSONObj> wholeBucketFilterBson; for (auto&& elem : specElem.embeddedObject()) { auto fieldName = elem.fieldNameStringData(); if (fieldName == kInclude || fieldName == kExclude) { @@ -326,8 +287,8 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF field.find('.') == std::string::npos); bucketSpec.addIncludeExcludeField(field); } - bucketSpec.setBehavior(fieldName == kInclude ? BucketSpec::Behavior::kInclude - : BucketSpec::Behavior::kExclude); + unpackerBehavior = fieldName == kInclude ? BucketUnpacker::Behavior::kInclude + : BucketUnpacker::Behavior::kExclude; hasIncludeExclude = true; } else if (fieldName == kAssumeNoMixedSchemaData) { uassert(6067202, @@ -389,24 +350,6 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF << " field must be a bool, got: " << elem.type(), elem.type() == BSONType::Bool); bucketSpec.includeMaxTimeAsMetadata = elem.boolean(); - } else if (fieldName == kUsesExtendedRange) { - uassert(6646901, - str::stream() << kUsesExtendedRange - << " field must be a bool, got: " << elem.type(), - elem.type() == BSONType::Bool); - bucketSpec.setUsesExtendedRange(elem.boolean()); - } else if (fieldName == kEventFilter) { - uassert(7026902, - str::stream() << kEventFilter - << " field must be an object, got: " << elem.type(), - elem.type() == BSONType::Object); - eventFilterBson = elem.Obj(); - } else if (fieldName == kWholeBucketFilter) { - uassert(7026903, - str::stream() << kWholeBucketFilter - << " field must be an object, got: " << elem.type(), - elem.type() == BSONType::Object); - wholeBucketFilterBson = elem.Obj(); } else { uasserted(5346506, str::stream() @@ -421,12 +364,11 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF "The $_internalUnpackBucket stage requires a bucketMaxSpanSeconds parameter", hasBucketMaxSpanSeconds); - return make_intrusive<DocumentSourceInternalUnpackBucket>(expCtx, - BucketUnpacker{std::move(bucketSpec)}, - bucketMaxSpanSeconds, - eventFilterBson, - wholeBucketFilterBson, - assumeClean); + return make_intrusive<DocumentSourceInternalUnpackBucket>( + expCtx, + BucketUnpacker{std::move(bucketSpec), unpackerBehavior}, + bucketMaxSpanSeconds, + assumeClean); } boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createFromBsonExternal( @@ -472,49 +414,39 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF hasTimeField); return make_intrusive<DocumentSourceInternalUnpackBucket>( - expCtx, BucketUnpacker{std::move(bucketSpec)}, 3600, assumeClean); + expCtx, + BucketUnpacker{std::move(bucketSpec), BucketUnpacker::Behavior::kExclude}, + 3600, + assumeClean); } -void DocumentSourceInternalUnpackBucket::serializeToArray(std::vector<Value>& array, - const SerializationOptions& opts) const { - auto explain = opts.verbosity; - +void DocumentSourceInternalUnpackBucket::serializeToArray( + std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument out; auto behavior = - _bucketUnpacker.behavior() == BucketSpec::Behavior::kInclude ? kInclude : kExclude; + _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kInclude ? kInclude : kExclude; const auto& spec = _bucketUnpacker.bucketSpec(); std::vector<Value> fields; for (auto&& field : spec.fieldSet()) { - fields.emplace_back(opts.serializeFieldPathFromString(field)); + fields.emplace_back(field); } if (((_bucketUnpacker.includeMetaField() && - _bucketUnpacker.behavior() == BucketSpec::Behavior::kInclude) || + _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kInclude) || (!_bucketUnpacker.includeMetaField() && - _bucketUnpacker.behavior() == BucketSpec::Behavior::kExclude && spec.metaField())) && + _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kExclude && spec.metaField())) && std::find(spec.computedMetaProjFields().cbegin(), spec.computedMetaProjFields().cend(), *spec.metaField()) == spec.computedMetaProjFields().cend()) - fields.emplace_back(opts.serializeFieldPathFromString(*spec.metaField())); + fields.emplace_back(*spec.metaField()); out.addField(behavior, Value{std::move(fields)}); - out.addField(timeseries::kTimeFieldName, - Value{opts.serializeFieldPathFromString(spec.timeField())}); + out.addField(timeseries::kTimeFieldName, Value{spec.timeField()}); if (spec.metaField()) { - out.addField(timeseries::kMetaFieldName, - Value{opts.serializeFieldPathFromString(*spec.metaField())}); + out.addField(timeseries::kMetaFieldName, Value{*spec.metaField()}); } - out.addField(kBucketMaxSpanSeconds, opts.serializeLiteral(Value{_bucketMaxSpanSeconds})); + out.addField(kBucketMaxSpanSeconds, Value{_bucketMaxSpanSeconds}); if (_assumeNoMixedSchemaData) - out.addField(kAssumeNoMixedSchemaData, - opts.serializeLiteral(Value(_assumeNoMixedSchemaData))); - - if (spec.usesExtendedRange()) { - // Include this flag so that 'explain' is more helpful. - // But this is not so useful for communicating from one process to another, - // because mongos and/or the primary shard don't know whether any other shard - // has extended-range data. - out.addField(kUsesExtendedRange, opts.serializeLiteral(Value{true})); - } + out.addField(kAssumeNoMixedSchemaData, Value(_assumeNoMixedSchemaData)); if (!spec.computedMetaProjFields().empty()) out.addField("computedMetaProjFields", Value{[&] { @@ -522,97 +454,51 @@ void DocumentSourceInternalUnpackBucket::serializeToArray(std::vector<Value>& ar std::transform(spec.computedMetaProjFields().cbegin(), spec.computedMetaProjFields().cend(), std::back_inserter(compFields), - [opts](auto&& projString) { - return Value{ - opts.serializeFieldPathFromString(projString)}; - }); + [](auto&& projString) { return Value{projString}; }); return compFields; }()}); if (_bucketUnpacker.includeMinTimeAsMetadata()) { - out.addField(kIncludeMinTimeAsMetadata, - opts.serializeLiteral(Value{_bucketUnpacker.includeMinTimeAsMetadata()})); + out.addField(kIncludeMinTimeAsMetadata, Value{_bucketUnpacker.includeMinTimeAsMetadata()}); } if (_bucketUnpacker.includeMaxTimeAsMetadata()) { - out.addField(kIncludeMaxTimeAsMetadata, - opts.serializeLiteral(Value{_bucketUnpacker.includeMaxTimeAsMetadata()})); - } - - if (_wholeBucketFilter) { - out.addField(kWholeBucketFilter, Value{_wholeBucketFilter->serialize(opts)}); - } - if (_eventFilter) { - out.addField(kEventFilter, Value{_eventFilter->serialize(opts)}); + out.addField(kIncludeMaxTimeAsMetadata, Value{_bucketUnpacker.includeMaxTimeAsMetadata()}); } if (!explain) { array.push_back(Value(DOC(getSourceName() << out.freeze()))); if (_sampleSize) { auto sampleSrc = DocumentSourceSample::create(pExpCtx, *_sampleSize); - sampleSrc->serializeToArray(array, opts); + sampleSrc->serializeToArray(array); } } else { if (_sampleSize) { - out.addField("sample", - opts.serializeLiteral(Value{static_cast<long long>(*_sampleSize)})); - out.addField("bucketMaxCount", opts.serializeLiteral(Value{_bucketMaxCount})); + out.addField("sample", Value{static_cast<long long>(*_sampleSize)}); + out.addField("bucketMaxCount", Value{_bucketMaxCount}); } array.push_back(Value(DOC(getSourceName() << out.freeze()))); } } -boost::optional<Document> DocumentSourceInternalUnpackBucket::getNextMatchingMeasure() { - while (_bucketUnpacker.hasNext()) { - if (_eventFilter) { - if (_unpackToBson) { - auto measure = _bucketUnpacker.getNextBson(); - if (_bucketUnpacker.bucketMatchedQuery() || _eventFilter->matchesBSON(measure)) { - return Document(measure); - } - } else { - auto measure = _bucketUnpacker.getNext(); - // MatchExpression only takes BSON documents, so we have to make one. As an - // optimization, only serialize the fields we need to do the match. - BSONObj measureBson = _eventFilterDeps.needWholeDocument - ? measure.toBson() - : document_path_support::documentToBsonWithPaths(measure, - _eventFilterDeps.fields); - if (_bucketUnpacker.bucketMatchedQuery() || - _eventFilter->matchesBSON(measureBson)) { - return measure; - } - } - } else { - return _bucketUnpacker.getNext(); - } - } - return {}; -} - DocumentSource::GetNextResult DocumentSourceInternalUnpackBucket::doGetNext() { tassert(5521502, "calling doGetNext() when '_sampleSize' is set is disallowed", !_sampleSize); // Otherwise, fallback to unpacking every measurement in all buckets until the child stage is // exhausted. - if (auto measure = getNextMatchingMeasure()) { - return GetNextResult(std::move(*measure)); + if (_bucketUnpacker.hasNext()) { + return _bucketUnpacker.getNext(); } auto nextResult = pSource->getNext(); - while (nextResult.isAdvanced()) { + if (nextResult.isAdvanced()) { auto bucket = nextResult.getDocument().toBson(); - auto bucketMatchedQuery = _wholeBucketFilter && _wholeBucketFilter->matchesBSON(bucket); - _bucketUnpacker.reset(std::move(bucket), bucketMatchedQuery); - + _bucketUnpacker.reset(std::move(bucket)); uassert(5346509, str::stream() << "A bucket with _id " << _bucketUnpacker.bucket()[timeseries::kBucketIdFieldName].toString() << " contains an empty data region", _bucketUnpacker.hasNext()); - if (auto measure = getNextMatchingMeasure()) { - return GetNextResult(std::move(*measure)); - } - nextResult = pSource->getNext(); + return _bucketUnpacker.getNext(); } return nextResult; @@ -624,7 +510,7 @@ bool DocumentSourceInternalUnpackBucket::pushDownComputedMetaProjection( if (std::next(itr) == container->end()) { return nextStageWasRemoved; } - if (!_bucketUnpacker.getMetaField() || !_bucketUnpacker.includeMetaField()) { + if (!_bucketUnpacker.bucketSpec().metaField()) { return nextStageWasRemoved; } @@ -676,8 +562,9 @@ void DocumentSourceInternalUnpackBucket::internalizeProject(const BSONObj& proje // Update '_bucketUnpacker' state with the new fields and behavior. auto spec = _bucketUnpacker.bucketSpec(); spec.setFieldSet(fields); - spec.setBehavior(isInclusion ? BucketSpec::Behavior::kInclude : BucketSpec::Behavior::kExclude); - _bucketUnpacker.setBucketSpec(std::move(spec)); + _bucketUnpacker.setBucketSpecAndBehavior(std::move(spec), + isInclusion ? BucketUnpacker::Behavior::kInclude + : BucketUnpacker::Behavior::kExclude); } std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProjectToInternalize( @@ -689,8 +576,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje // Check for a viable inclusion $project after the $_internalUnpackBucket. auto [existingProj, isInclusion] = getIncludeExcludeProjectAndType(std::next(itr)->get()); - if (!_eventFilter && isInclusion && !existingProj.isEmpty() && - canInternalizeProjectObj(existingProj)) { + if (isInclusion && !existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) { container->erase(std::next(itr)); return {existingProj, isInclusion}; } @@ -698,7 +584,8 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje // Attempt to get an inclusion $project representing the root-level dependencies of the pipeline // after the $_internalUnpackBucket. If this $project is not empty, then the dependency set was // finite. - auto deps = getRestPipelineDependencies(itr, container, true /* includeEventFilter */); + Pipeline::SourceContainer restOfPipeline(std::next(itr), container->end()); + auto deps = Pipeline::getDependenciesForContainer(pExpCtx, restOfPipeline, boost::none); if (auto dependencyProj = deps.toProjectionWithoutMetadata(DepsTracker::TruncateToRootLevel::yes); !dependencyProj.isEmpty()) { @@ -706,7 +593,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje } // Check for a viable exclusion $project after the $_internalUnpackBucket. - if (!_eventFilter && !existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) { + if (!existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) { container->erase(std::next(itr)); return {existingProj, isInclusion}; } @@ -714,7 +601,8 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje return {BSONObj{}, false}; } -BucketSpec::BucketPredicate DocumentSourceInternalUnpackBucket::createPredicatesOnBucketLevelField( +std::unique_ptr<MatchExpression> +DocumentSourceInternalUnpackBucket::createPredicatesOnBucketLevelField( const MatchExpression* matchExpr) const { return BucketSpec::createPredicatesOnBucketLevelField( matchExpr, @@ -743,102 +631,58 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractProjectForPu std::pair<bool, Pipeline::SourceContainer::iterator> DocumentSourceInternalUnpackBucket::rewriteGroupByMinMax(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - // The computed min/max for each bucket uses the default collation. If the collation of the - // query doesn't match the default we cannot rely on the computed values as they might differ - // (e.g. numeric and lexicographic collations compare "5" and "10" in opposite order). - // NB: Unfortuntealy, this means we have to forgo the optimization even if the source field is - // numeric and not affected by the collation as we cannot know the data type until runtime. - if (pExpCtx->collationMatchesDefault == ExpressionContext::CollationMatchesDefault::kNo) { - return {}; - } - const auto* groupPtr = dynamic_cast<DocumentSourceGroup*>(std::next(itr)->get()); if (groupPtr == nullptr) { return {}; } - if (!_bucketUnpacker.bucketSpec().metaField()) { - return {}; - } - const auto& metaField = *_bucketUnpacker.bucketSpec().metaField(); - const auto& idFields = groupPtr->getIdFields(); - - // Currently, we only support simple group key. TODO: SERVER-68811. Allow rewrites of object - // group key if all its fields depend on the metaField only. - if (idFields.size() != 1) { + if (idFields.size() != 1 || !_bucketUnpacker.bucketSpec().metaField().has_value()) { return {}; } const auto& exprId = idFields.cbegin()->second; const auto* exprIdPath = dynamic_cast<const ExpressionFieldPath*>(exprId.get()); - - // Currently, we only support group key expression of the form {_id : "<path>"}. - // TODO: SERVER-68811. Allow rewrites if expression is constant. if (exprIdPath == nullptr) { return {}; } const auto& idPath = exprIdPath->getFieldPath(); - // {The path must be at or under the metaField for this re-write to be correct (and the zero - // component is always CURRENT).} - if (idPath.getPathLength() < 2 || idPath.getFieldName(1) != metaField) { + if (idPath.getPathLength() < 2 || + idPath.getFieldName(1) != _bucketUnpacker.bucketSpec().metaField().get()) { return {}; } - std::vector<AccumulationStatement> accumulationStatementsBucket; + bool suitable = true; + std::vector<AccumulationStatement> accumulationStatements; for (const AccumulationStatement& stmt : groupPtr->getAccumulatedFields()) { - const auto& op = stmt.expr.name; - // If _any_ of the accumulators aren't $min/$max we won't perform the re-write (some other - // accs might be re-writable in terms of the bucket controls, we just haven't invested into - // implementing them). - if (op != "$min" && op != "$max") { - return {}; - } - - const auto* exprArgPath = - dynamic_cast<const ExpressionFieldPath*>(stmt.expr.argument.get()); - - // This is either a const or a compound expression. While some such expressions (e.g: {$min: - // {$add: - // ['$a', 2]}}) could be re-written in terms of the min/max on the control fields, in - // general we cannot do it (e.g. {$min: {$add: ['$a', '$b']}}), so we block the re-write. - if (!exprArgPath) { - return {}; - } - - // Path can have a single component if it's using $$CURRENT or a similar variable. We don't - // support these. - const auto& path = exprArgPath->getFieldPath(); - if (path.getPathLength() <= 1) { - return {}; - } - const auto& accFieldName = path.getFieldName(1); + const auto op = stmt.expr.name; + const bool isMin = op == "$min"; + const bool isMax = op == "$max"; - // Rewrite not valid for the timeField because control.min.time contains a rounded-down time - // and not the actual min time of events in the bucket. - if (accFieldName == _bucketUnpacker.bucketSpec().timeField()) { - return {}; + // Rewrite is valid only for min and max aggregates. + if (!isMin && !isMax) { + suitable = false; + break; } - // Build the paths for the bucket-level fields. - std::ostringstream os; - if (accFieldName == metaField) { - // Update aggregates to reference the meta field. - os << timeseries::kBucketMetaFieldName; - - for (size_t index = 2; index < path.getPathLength(); index++) { - os << "." << path.getFieldName(index); + const auto* exprArg = stmt.expr.argument.get(); + if (const auto* exprArgPath = dynamic_cast<const ExpressionFieldPath*>(exprArg)) { + const auto& path = exprArgPath->getFieldPath(); + if (path.getPathLength() <= 1 || + path.getFieldName(1) == _bucketUnpacker.bucketSpec().timeField()) { + // Rewrite not valid for time field. We want to eliminate the bucket + // unpack stage here. + suitable = false; + break; } - } else { + // Update aggregates to reference the control field. - const auto op = stmt.expr.name; - if (op == "$min") { + std::ostringstream os; + if (isMin) { os << timeseries::kControlMinFieldNamePrefix; - } else if (op == "$max") { - os << timeseries::kControlMaxFieldNamePrefix; } else { - MONGO_UNREACHABLE; + os << timeseries::kControlMaxFieldNamePrefix; } for (size_t index = 1; index < path.getPathLength(); index++) { @@ -847,55 +691,45 @@ DocumentSourceInternalUnpackBucket::rewriteGroupByMinMax(Pipeline::SourceContain } os << path.getFieldName(index); } - } - // Re-create the accumulator using the bucket-level paths. - const auto& newExpr = ExpressionFieldPath::createPathFromString( - pExpCtx.get(), os.str(), pExpCtx->variablesParseState); - - AccumulationExpression accExpr = stmt.expr; - accExpr.argument = newExpr; - accumulationStatementsBucket.emplace_back(stmt.fieldName, std::move(accExpr)); - } + const auto& newExpr = ExpressionFieldPath::createPathFromString( + pExpCtx.get(), os.str(), pExpCtx->variablesParseState); - // Re-create the group key using the bucket-level path. - std::ostringstream os; - os << timeseries::kBucketMetaFieldName; - for (size_t index = 2; index < idPath.getPathLength(); index++) { - os << "." << idPath.getFieldName(index); + AccumulationExpression accExpr = stmt.expr; + accExpr.argument = newExpr; + accumulationStatements.emplace_back(stmt.fieldName, std::move(accExpr)); + } } - auto exprIdBucket = ExpressionFieldPath::createPathFromString( - pExpCtx.get(), os.str(), pExpCtx->variablesParseState); - boost::intrusive_ptr<Expression> rewrittenIdExpression; - const auto& idFieldNames = groupPtr->getIdFieldNames(); - if (idFieldNames.empty()) { - rewrittenIdExpression = exprIdBucket; - } else { - // idFieldNames can only have size 1 here since we only support simple group key. - // TODO: SERVER-68811. Allow rewrites of object group key if all its fields depend on the - // metaField only. - rewrittenIdExpression = - ExpressionObject::create(pExpCtx.get(), {{idFieldNames[0], exprIdBucket}}); - } + if (suitable) { + std::ostringstream os; + os << timeseries::kBucketMetaFieldName; + for (size_t index = 2; index < idPath.getPathLength(); index++) { + os << "." << idPath.getFieldName(index); + } + auto exprId1 = ExpressionFieldPath::createPathFromString( + pExpCtx.get(), os.str(), pExpCtx->variablesParseState); - auto newGroup = DocumentSourceGroup::create(pExpCtx, - std::move(rewrittenIdExpression), - std::move(accumulationStatementsBucket), - groupPtr->getMaxMemoryUsageBytes()); + auto newGroup = DocumentSourceGroup::create(pExpCtx, + std::move(exprId1), + std::move(accumulationStatements), + groupPtr->getMaxMemoryUsageBytes()); - // Replace the current stage (DocumentSourceInternalUnpackBucket) and the following group stage - // with the new group. - container->erase(std::next(itr)); - *itr = std::move(newGroup); + // Erase current stage and following group stage, and replace with updated + // group. + container->erase(std::next(itr)); + *itr = std::move(newGroup); - if (itr == container->begin()) { - // Optimize the new group stage. - return {true, itr}; - } else { - // Give chance to the previous stage to optimize against the new group stage. - return {true, std::prev(itr)}; + if (itr == container->begin()) { + // Optimize group stage. + return {true, itr}; + } else { + // Give chance of the previous stage to optimize against group stage. + return {true, std::prev(itr)}; + } } + + return {}; } bool DocumentSourceInternalUnpackBucket::haveComputedMetaField() const { @@ -904,68 +738,6 @@ bool DocumentSourceInternalUnpackBucket::haveComputedMetaField() const { _bucketUnpacker.bucketSpec().metaField().get()); } -bool DocumentSourceInternalUnpackBucket::enableStreamingGroupIfPossible( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - // skip unpack stage - itr = std::next(itr); - - FieldPath timeField = _bucketUnpacker.bucketSpec().timeField(); - DocumentSourceGroup* groupStage = nullptr; - bool isSortedOnTime = false; - for (; itr != container->end(); ++itr) { - if (auto groupStagePtr = dynamic_cast<DocumentSourceGroup*>(itr->get())) { - groupStage = groupStagePtr; - break; - } - if (auto sortStagePtr = dynamic_cast<DocumentSourceSort*>(itr->get())) { - isSortedOnTime = sortStagePtr->getSortKeyPattern().front().fieldPath == timeField; - } else if (!itr->get()->constraints().preservesOrderAndMetadata) { - // If this is after the sort, the sort is invalidated. If it's before the sort, there's - // no harm in keeping the boolean false. - isSortedOnTime = false; - } - // We modify time field, so we can't proceed with optimization. It may be possible to - // proceed in some cases if the modification happens before the sort, but we won't worry - // about or bother with those - in large part because it is risky that it will change the - // type away from a date into something with more difficult/subtle semantics. - if (itr->get()->getModifiedPaths().canModify(timeField)) { - return false; - } - } - - if (groupStage == nullptr || !isSortedOnTime) { - return false; - } - - const auto& idFields = groupStage->getMutableIdFields(); - std::vector<size_t> monotonicIdFields; - for (size_t i = 0; i < idFields.size(); ++i) { - // To enable streaming, we need id field expression to be clustered, so that all documents - // with the same value of this id field are in a single continious cluster. However this - // property is hard to check for, so we check for monotonicity instead, which is stronger. - idFields[i]->optimize(); // We optimize here to make use of constant folding. - auto monotonicState = idFields[i]->getMonotonicState(timeField); - - // We don't add monotonic::State::Constant id fields, because they are useless when - // determining if a group batch is finished. - if (monotonicState == monotonic::State::Increasing || - monotonicState == monotonic::State::Decreasing) { - monotonicIdFields.push_back(i); - } - } - if (monotonicIdFields.empty()) { - return false; - } - - *itr = - DocumentSourceStreamingGroup::create(pExpCtx, - groupStage->getIdExpression(), - std::move(monotonicIdFields), - std::move(groupStage->getMutableAccumulatedFields()), - groupStage->getMaxMemoryUsageBytes()); - return true; -} - template <TopBottomSense sense, bool single> bool extractFromAcc(const AccumulatorN* acc, const boost::intrusive_ptr<Expression>& init, @@ -1208,55 +980,6 @@ bool DocumentSourceInternalUnpackBucket::optimizeLastpoint(Pipeline::SourceConta tryInsertBucketLevelSortAndGroup(AccumulatorDocumentsNeeded::kLastDocument); } - -bool findSequentialDocumentCache(Pipeline::SourceContainer::iterator start, - Pipeline::SourceContainer::iterator end) { - while (start != end && !dynamic_cast<DocumentSourceSequentialDocumentCache*>(start->get())) { - start = std::next(start); - } - return start != end; -} - -Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::optimizeAtRestOfPipeline( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - if (itr == container->end()) { - return itr; - } - - invariant(*itr == this); - Pipeline::SourceContainer::iterator unpackBucket = itr; - - itr = std::next(itr); - - try { - while (itr != container->end()) { - if (itr == unpackBucket) { - itr = std::next(itr); - if (itr == container->end()) - break; - } - itr = (*itr).get()->optimizeAt(itr, container); - } - } catch (DBException& ex) { - ex.addContext("Failed to optimize pipeline"); - throw; - } - - return itr; -} - -DepsTracker DocumentSourceInternalUnpackBucket::getRestPipelineDependencies( - Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container, - bool includeEventFilter) const { - auto deps = Pipeline::getDependenciesForContainer( - pExpCtx, Pipeline::SourceContainer{std::next(itr), container->end()}, boost::none); - if (_eventFilter && includeEventFilter) { - _eventFilter->addDependencies(&deps); - } - return deps; -} - Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimizeAt( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { invariant(*itr == this); @@ -1270,8 +993,7 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi bool haveComputedMetaField = this->haveComputedMetaField(); // Before any other rewrites for the current stage, consider reordering with $sort. - if (auto sortPtr = dynamic_cast<DocumentSourceSort*>(std::next(itr)->get()); - sortPtr && !_eventFilter) { + if (auto sortPtr = dynamic_cast<DocumentSourceSort*>(std::next(itr)->get())) { if (auto metaField = _bucketUnpacker.bucketSpec().metaField(); metaField && !haveComputedMetaField) { if (checkMetadataSortReorder(sortPtr->getSortKeyPattern(), metaField.get())) { @@ -1302,8 +1024,7 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi } // Attempt to push geoNear on the metaField past $_internalUnpackBucket. - if (auto nextNear = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get()); - nextNear && !_eventFilter) { + if (auto nextNear = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get())) { // Currently we only support geo indexes on the meta field, and we enforce this by // requiring the key field to be set so we can check before we try to look up indexes. auto keyField = nextNear->getKeyField(); @@ -1337,34 +1058,23 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi } } - // OptimizeAt the pipeline after this stage to merge $match stages and push them forward. + // Optimize the pipeline after this stage to merge $match stages and push them forward, and to + // take advantage of $expr rewrite optimizations. if (!_optimizedEndOfPipeline) { _optimizedEndOfPipeline = true; if (std::next(itr) == container->end()) { return container->end(); } - - auto cacheFound = findSequentialDocumentCache(itr, container->end()); - if (cacheFound) { - // We want to call optimizeAt() on the rest of the pipeline first, and exit this - // function since any calls to optimize() will interfere with the - // sequentialDocumentCache's ability to properly place itself or abandon. - return DocumentSourceInternalUnpackBucket::optimizeAtRestOfPipeline(itr, container); - } else { - if (auto nextStage = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get())) { - // If the end of the pipeline starts with a $geoNear stage, make sure it gets - // optimized in a context where it knows there are other stages before it. It will - // split itself up into separate $match and $sort stages. But it doesn't split - // itself up when it's the first stage, because it expects to use a special - // DocumentSouceGeoNearCursor plan. - nextStage->optimizeAt(std::next(itr), container); - } - // We want to optimize the rest of the pipeline to ensure the stages are in their - // optimal position and expressions have been optimized to allow for certain rewrites. - Pipeline::optimizeEndOfPipeline(itr, container); + if (auto nextStage = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get())) { + // If the end of the pipeline starts with a $geoNear stage, make sure it gets optimized + // in a context where it knows there are other stages before it. It will split itself + // up into separate $match and $sort stages. But it doesn't split itself up when it's + // the first stage, because it expects to use a special DocumentSouceGeoNearCursor plan. + nextStage->optimizeAt(std::next(itr), container); } + Pipeline::optimizeEndOfPipeline(itr, container); if (std::next(itr) == container->end()) { return container->end(); } else { @@ -1373,8 +1083,7 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi return itr; } } - - if (!_eventFilter) { + { // Check if we can avoid unpacking if we have a group stage with min/max aggregates. auto [success, result] = rewriteGroupByMinMax(itr, container); if (success) { @@ -1385,12 +1094,13 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi { // Check if the rest of the pipeline needs any fields. For example we might only be // interested in $count. - auto deps = getRestPipelineDependencies(itr, container, true /* includeEventFilter */); + auto deps = Pipeline::getDependenciesForContainer( + pExpCtx, Pipeline::SourceContainer{std::next(itr), container->end()}, boost::none); if (deps.hasNoRequirements()) { - _bucketUnpacker.setBucketSpec({_bucketUnpacker.bucketSpec().timeField(), - _bucketUnpacker.bucketSpec().metaField(), - {}, - BucketSpec::Behavior::kInclude}); + _bucketUnpacker.setBucketSpecAndBehavior({_bucketUnpacker.bucketSpec().timeField(), + _bucketUnpacker.bucketSpec().metaField(), + {}}, + BucketUnpacker::Behavior::kInclude); // Keep going for next optimization. } @@ -1407,71 +1117,31 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi // Attempt to optimize last-point type queries. if (feature_flags::gfeatureFlagLastPointQuery.isEnabled( serverGlobalParams.featureCompatibility) && - !_triedLastpointRewrite && !_eventFilter && optimizeLastpoint(itr, container)) { + !_triedLastpointRewrite && optimizeLastpoint(itr, container)) { _triedLastpointRewrite = true; // If we are able to rewrite the aggregation, give the resulting pipeline a chance to // perform further optimizations. return container->begin(); }; - // Attempt to map predicates on bucketed fields to the predicates on the control field. - if (auto nextMatch = dynamic_cast<DocumentSourceMatch*>(std::next(itr)->get())) { + // Attempt to map predicates on bucketed fields to predicates on the control field. + if (auto nextMatch = dynamic_cast<DocumentSourceMatch*>(std::next(itr)->get()); + nextMatch && !_triedBucketLevelFieldsPredicatesPushdown) { + _triedBucketLevelFieldsPredicatesPushdown = true; - // Merge multiple following $match stages. - auto itrToMatch = std::next(itr); - while (std::next(itrToMatch) != container->end() && - dynamic_cast<DocumentSourceMatch*>(std::next(itrToMatch)->get())) { - nextMatch->doOptimizeAt(itrToMatch, container); - } - - auto predicates = createPredicatesOnBucketLevelField(nextMatch->getMatchExpression()); - - // Try to create a tight bucket predicate to perform bucket level matching. - if (predicates.tightPredicate) { - _wholeBucketFilterBson = predicates.tightPredicate->serialize(); - _wholeBucketFilter = - uassertStatusOK(MatchExpressionParser::parse(_wholeBucketFilterBson, - pExpCtx, - ExtensionsCallbackNoop(), - Pipeline::kAllowedMatcherFeatures)); - _wholeBucketFilter = MatchExpression::optimize(std::move(_wholeBucketFilter)); - } - - // Push the original event predicate into the unpacking stage. - _eventFilterBson = nextMatch->getQuery().getOwned(); - _eventFilter = - uassertStatusOK(MatchExpressionParser::parse(_eventFilterBson, - pExpCtx, - ExtensionsCallbackNoop(), - Pipeline::kAllowedMatcherFeatures)); - _eventFilter = MatchExpression::optimize(std::move(_eventFilter)); - _eventFilterDeps = {}; - _eventFilter->addDependencies(&_eventFilterDeps); - container->erase(std::next(itr)); - - // If the $match is not followed by other stages referencing fields (e.g. $count), we can - // unpack directly to BSON so that data doesn't need to be materialized to Document. - auto deps = getRestPipelineDependencies(itr, container, false /* includeEventFilter */); - if (deps.fields.empty()) { - _unpackToBson = true; - } - - // Create a loose bucket predicate and push it before the unpacking stage. - if (predicates.loosePredicate) { - container->insert( - itr, DocumentSourceMatch::create(predicates.loosePredicate->serialize(), pExpCtx)); + if (auto match = createPredicatesOnBucketLevelField(nextMatch->getMatchExpression())) { + BSONObjBuilder bob; + match->serialize(&bob); + container->insert(itr, DocumentSourceMatch::create(bob.obj(), pExpCtx)); // Give other stages a chance to optimize with the new $match. return std::prev(itr) == container->begin() ? std::prev(itr) : std::prev(std::prev(itr)); } - - // We have removed a $match after this stage, so we try to optimize this stage again. - return itr; } // Attempt to push down a $project on the metaField past $_internalUnpackBucket. - if (!_eventFilter && !haveComputedMetaField) { + if (!haveComputedMetaField) { if (auto [metaProject, deleteRemainder] = extractProjectForPushDown(std::next(itr)->get()); !metaProject.isEmpty()) { container->insert(itr, @@ -1490,7 +1160,7 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi // Attempt to extract computed meta projections from subsequent $project, $addFields, or $set // and push them before the $_internalunpackBucket. - if (!_eventFilter && pushDownComputedMetaProjection(itr, container)) { + if (pushDownComputedMetaProjection(itr, container)) { // We've pushed down and removed a stage after this one. Try to optimize the new stage. return std::prev(itr) == container->begin() ? std::prev(itr) : std::prev(std::prev(itr)); } @@ -1509,8 +1179,6 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi } } - enableStreamingGroupIfPossible(itr, container); - return container->end(); } @@ -1519,8 +1187,8 @@ DocumentSource::GetModPathsReturn DocumentSourceInternalUnpackBucket::getModifie StringMap<std::string> renames; renames.emplace(*_bucketUnpacker.bucketSpec().metaField(), timeseries::kBucketMetaFieldName); - return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{}, std::move(renames)}; + return {GetModPathsReturn::Type::kAllExcept, std::set<std::string>{}, std::move(renames)}; } - return {GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h index 8d28e5e75cd..df3bf93bff4 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h @@ -47,12 +47,9 @@ public: static constexpr StringData kInclude = "include"_sd; static constexpr StringData kExclude = "exclude"_sd; static constexpr StringData kAssumeNoMixedSchemaData = "assumeNoMixedSchemaData"_sd; - static constexpr StringData kUsesExtendedRange = "usesExtendedRange"_sd; static constexpr StringData kBucketMaxSpanSeconds = "bucketMaxSpanSeconds"_sd; static constexpr StringData kIncludeMinTimeAsMetadata = "includeMinTimeAsMetadata"_sd; static constexpr StringData kIncludeMaxTimeAsMetadata = "includeMaxTimeAsMetadata"_sd; - static constexpr StringData kWholeBucketFilter = "wholeBucketFilter"_sd; - static constexpr StringData kEventFilter = "eventFilter"_sd; static boost::intrusive_ptr<DocumentSource> createFromBsonInternal( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); @@ -64,27 +61,19 @@ public: int bucketMaxSpanSeconds, bool assumeNoMixedSchemaData = false); - DocumentSourceInternalUnpackBucket(const boost::intrusive_ptr<ExpressionContext>& expCtx, - BucketUnpacker bucketUnpacker, - int bucketMaxSpanSeconds, - const boost::optional<BSONObj>& eventFilterBson, - const boost::optional<BSONObj>& wholeBucketFilterBson, - bool assumeNoMixedSchemaData = false); - const char* getSourceName() const override { return kStageNameInternal.rawData(); } void serializeToArray( std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const final override; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; /** * Use 'serializeToArray' above. */ - Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { - MONGO_UNREACHABLE_TASSERT(7484305); + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final { + MONGO_UNREACHABLE; } bool includeMetaField() const { @@ -106,8 +95,6 @@ public: UnionRequirement::kAllowed, ChangeStreamRequirement::kDenylist}; constraints.canSwapWithMatch = true; - // The user cannot specify multiple $unpackBucket stages in the pipeline. - constraints.canAppearOnlyOnceInPipeline = true; return constraints; } @@ -168,7 +155,7 @@ public: /** * Convenience wrapper around BucketSpec::createPredicatesOnBucketLevelField(). */ - BucketSpec::BucketPredicate createPredicatesOnBucketLevelField( + std::unique_ptr<MatchExpression> createPredicatesOnBucketLevelField( const MatchExpression* matchExpr) const; /** @@ -222,20 +209,13 @@ public: /** * Helper method which checks if we can avoid unpacking if we have a group stage with min/max - * aggregates. If the rewrite is possible, 'container' is modified, bool in the return pair is - * set to 'true' and the iterator is set to point to the new group. + * aggregates. If a rewrite is possible, 'container' is modified, and we returns result value + * for 'doOptimizeAt'. */ std::pair<bool, Pipeline::SourceContainer::iterator> rewriteGroupByMinMax( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container); /** - * Helper method which checks if we can replace DocumentSourceGroup with - * DocumentSourceStreamingGroup. Returns true if the optimization is performed. - */ - bool enableStreamingGroupIfPossible(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container); - - /** * If the current aggregation is a lastpoint-type query (ie. with a $sort on meta and time * fields, and a $group with a meta _id and only $first or $last accumulators) we can rewrite * it to avoid unpacking all buckets. @@ -260,59 +240,23 @@ public: GetModPathsReturn getModifiedPaths() const final override; - DepsTracker getRestPipelineDependencies(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container, - bool includeEventFilter) const; - private: GetNextResult doGetNext() final; - - boost::optional<Document> getNextMatchingMeasure(); - bool haveComputedMetaField() const; - /** - * Applies optimizeAt() to all stages in the given pipeline after the stage that 'itr' points - * to, which is the bucket unpack stage. - * - * Due to the manipulation of 'itr' through the optimizations, it may be possible that - * preceeding stages will be optimized. However, optimization of the bucket unpack stage will be - * skipped. - */ - Pipeline::SourceContainer::iterator optimizeAtRestOfPipeline( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container); - // If buckets contained a mixed type schema along some path, we have to push down special // predicates in order to ensure correctness. bool _assumeNoMixedSchemaData = false; - // If any bucket contains dates outside the range of 1970-2038, we are unable to rely on - // the _id index, as _id is truncates to 32 bits - bool _usesExtendedRange = false; - BucketUnpacker _bucketUnpacker; int _bucketMaxSpanSeconds; int _bucketMaxCount = 0; boost::optional<long long> _sampleSize; - // It's benefitial to do as much filtering at the bucket level as possible to avoid unpacking - // buckets that wouldn't contribute to the results anyway. There is a generic mechanism that - // allows to swap $match stages with this one (see 'getModifiedPaths()'). It lets us split out - // and push down a filter on the metaField "as is". The remaining filters might cause creation - // of additional bucket-level filters (see 'createPredicatesOnBucketLevelField()') that are - // inserted before this stage while the original filter is incorporated into this stage as - // '_eventFilter' (to be applied to each unpacked document) and/or '_wholeBucketFilter' for the - // cases when _all_ events in a bucket would match (currently, we only do this for the - // timeField). - std::unique_ptr<MatchExpression> _eventFilter; - BSONObj _eventFilterBson; - DepsTracker _eventFilterDeps; - std::unique_ptr<MatchExpression> _wholeBucketFilter; - BSONObj _wholeBucketFilterBson; - - bool _unpackToBson = false; - + // Used to avoid infinite loops after we step backwards to optimize a $match on bucket level + // fields, otherwise we may do an infinite number of $match pushdowns. + bool _triedBucketLevelFieldsPredicatesPushdown = false; bool _optimizedEndOfPipeline = false; bool _triedInternalizeProject = false; bool _triedLastpointRewrite = false; diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp index d51e8a5a90e..ffe291dcbd0 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp @@ -55,11 +55,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -77,11 +76,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -99,11 +97,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {'control.min.a': {$_internalExprLt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -121,11 +118,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {'control.min.a': {$_internalExprLte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -143,12 +139,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {$and:[{'control.min.a': {$_internalExprLte: 1}}," "{'control.max.a': {$_internalExprGte: 1}}]}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -166,7 +161,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate); + ASSERT(predicate); auto expected = fromjson( "{$or: [" " {$or: [" @@ -190,8 +185,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}}" " ]}" "]}"); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), expected); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_BSONOBJ_EQ(predicate->serialize(true), expected); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -214,11 +208,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -241,11 +234,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -268,8 +260,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_BSONOBJ_EQ(predicate->serialize(true), + fromjson("{$or: [ {'control.min.a': {$_internalExprLt: 1}}," + "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," + "{$type: [ \"$control.max.a\" ]} ]}} ]}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -292,8 +286,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_BSONOBJ_EQ(predicate->serialize(true), + fromjson("{$or: [ {'control.min.a': {$_internalExprLte: 1}}," + "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," + "{$type: [ \"$control.max.a\" ]} ]}} ]}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -316,12 +312,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [ {$and:[{'control.min.a': {$_internalExprLte: 1}}," "{'control.max.a': {$_internalExprGte: 1}}]}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -339,14 +334,13 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$and: [ {$or: [ {'control.max.b': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]}," "{$type: [ \"$control.max.b\" ]} ]}} ]}," "{$or: [ {'control.min.a': {$_internalExprLt: 5}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -364,8 +358,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate == nullptr); - ASSERT(predicate.tightPredicate == nullptr); + ASSERT(predicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -383,7 +376,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [" " {'control.max.b': {$_internalExprGt: 1}}," " {$expr: {$ne: [" @@ -391,7 +384,6 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " {$type: [ \"$control.max.b\" ]}" " ]}}" "]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -410,7 +402,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$and: [ {$or: [ {'control.max.b': {$_internalExprGte: 2}}," "{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]}," "{$type: [ \"$control.max.b\" ]} ]}} ]}," @@ -420,7 +412,6 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, "{$or: [ {'control.min.a': {$_internalExprLt: 5}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]} ]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -438,8 +429,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT(predicate); + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [" " {$or: [" " {'control.max.b': {$_internalExprGt: 1}}," @@ -456,7 +447,6 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}}" " ]}" "]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -474,8 +464,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate == nullptr); - ASSERT(predicate.tightPredicate == nullptr); + ASSERT(predicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -496,8 +485,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, // When a predicate can't be pushed down, it's the same as pushing down a trivially-true // predicate. So when any child of an $or can't be pushed down, we could generate something like // {$or: [ ... {$alwaysTrue: {}}, ... ]}, but then we might as well not push down the whole $or. - ASSERT(predicate.loosePredicate == nullptr); - ASSERT(predicate.tightPredicate == nullptr); + ASSERT(predicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -516,7 +504,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$or: [" " {$or: [" " {'control.max.b': {$_internalExprGte: 2}}," @@ -542,7 +530,6 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}" " ]}" "]}")); - ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -554,22 +541,19 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ASSERT_EQ(pipeline->getSources().size(), 2U); pipeline->optimizePipeline(); - ASSERT_EQ(pipeline->getSources().size(), 2U); + ASSERT_EQ(pipeline->getSources().size(), 3U); // To get the optimized $match from the pipeline, we have to serialize with explain. - auto stages = pipeline->writeExplainOps( - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}); - ASSERT_EQ(stages.size(), 2U); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(stages.size(), 3U); ASSERT_BSONOBJ_EQ(stages[0].getDocument().toBson(), fromjson("{$match: {$or: [ {'control.max.b': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]}," "{$type: [ \"$control.max.b\" ]} ]}} ]}}")); - ASSERT_BSONOBJ_EQ( - stages[1].getDocument().toBson(), - fromjson( - "{$_internalUnpackBucket: {exclude: [], timeField: 'time', bucketMaxSpanSeconds: 3600, " - "eventFilter: { $and: [ { b: { $gt: 1 } }, { a: { $not: { $eq: 5 } } } ] }}}")); + ASSERT_BSONOBJ_EQ(stages[1].getDocument().toBson(), unpackBucketObj); + ASSERT_BSONOBJ_EQ(stages[2].getDocument().toBson(), + fromjson("{$match: {$and: [{b: {$gt: 1}}, {a: {$not: {$eq: 5}}}]}}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -582,10 +566,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ASSERT_EQ(pipeline->getSources().size(), 2U); pipeline->optimizePipeline(); - ASSERT_EQ(pipeline->getSources().size(), 2U); + ASSERT_EQ(pipeline->getSources().size(), 3U); auto stages = pipeline->serializeToBson(); - ASSERT_EQ(stages.size(), 2U); + ASSERT_EQ(stages.size(), 3U); ASSERT_BSONOBJ_EQ( stages[0], @@ -598,11 +582,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, "{$or: [ {'control.min.a': {$_internalExprLt: 5}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}")); - ASSERT_BSONOBJ_EQ(stages[1], - fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "bucketMaxSpanSeconds: 3600," - "eventFilter: { $and: [ { b: { $gte: 2 } }, { c: { $gt: 1 } }, { a: " - "{ $lt: 5 } } ] } } }")); + ASSERT_BSONOBJ_EQ(stages[1], unpackBucketObj); + ASSERT_BSONOBJ_EQ(stages[2], matchObj); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -620,8 +601,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate == nullptr); - ASSERT(predicate.tightPredicate == nullptr); + ASSERT(predicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -639,8 +619,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate == nullptr); - ASSERT(predicate.tightPredicate == nullptr); + ASSERT(predicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -658,8 +637,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate.loosePredicate == nullptr); - ASSERT(predicate.tightPredicate == nullptr); + ASSERT(predicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -678,8 +656,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ->createPredicatesOnBucketLevelField(original->getMatchExpression()); // Meta predicates are mapped to the meta field, not the control min/max fields. - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), fromjson("{meta: {$gt: 5}}")); - ASSERT_BSONOBJ_EQ(predicate.tightPredicate->serialize(), fromjson("{meta: {$gt: 5}}")); + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{meta: {$gt: 5}}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -698,8 +675,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ->createPredicatesOnBucketLevelField(original->getMatchExpression()); // Meta predicates are mapped to the meta field, not the control min/max fields. - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), fromjson("{'meta.foo': {$gt: 5}}")); - ASSERT_BSONOBJ_EQ(predicate.tightPredicate->serialize(), fromjson("{'meta.foo': {$gt: 5}}")); + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{'meta.foo': {$gt: 5}}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -717,7 +693,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$and: [" " {$or: [" " {'control.max.a': {$_internalExprGt: 1}}," @@ -728,7 +704,6 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}," " {meta: {$eq: 5}}" "]}")); - ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePredicatesOnId) { @@ -765,14 +740,14 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); - ASSERT_BSONOBJ_EQ((*children)[0]->serialize(), + ASSERT_BSONOBJ_EQ((*children)[0]->serialize(true), BSON("control.min.time" << BSON("$_internalExprLt" << date))); ASSERT_BSONOBJ_EQ( - (*children)[1]->serialize(), + (*children)[1]->serialize(true), BSON("control.max.time" << BSON("$_internalExprLt" << datePlusBucketSpan))); auto idPred = dynamic_cast<ComparisonMatchExpressionBase*>((*children)[2].get()); @@ -822,14 +797,14 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); - ASSERT_BSONOBJ_EQ((*children)[0]->serialize(), + ASSERT_BSONOBJ_EQ((*children)[0]->serialize(true), BSON("control.min.time" << BSON("$_internalExprLte" << date))); ASSERT_BSONOBJ_EQ( - (*children)[1]->serialize(), + (*children)[1]->serialize(true), BSON("control.max.time" << BSON("$_internalExprLte" << datePlusBucketSpan))); auto idPred = dynamic_cast<ComparisonMatchExpressionBase*>((*children)[2].get()); @@ -871,19 +846,19 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 6); - ASSERT_BSONOBJ_EQ((*children)[0]->serialize(), + ASSERT_BSONOBJ_EQ((*children)[0]->serialize(true), BSON("control.min.time" << BSON("$_internalExprLte" << date))); ASSERT_BSONOBJ_EQ( - (*children)[1]->serialize(), + (*children)[1]->serialize(true), BSON("control.min.time" << BSON("$_internalExprGte" << dateMinusBucketSpan))); - ASSERT_BSONOBJ_EQ((*children)[2]->serialize(), + ASSERT_BSONOBJ_EQ((*children)[2]->serialize(true), BSON("control.max.time" << BSON("$_internalExprGte" << date))); ASSERT_BSONOBJ_EQ( - (*children)[3]->serialize(), + (*children)[3]->serialize(true), BSON("control.max.time" << BSON("$_internalExprLte" << datePlusBucketSpan))); auto idPred = dynamic_cast<ComparisonMatchExpressionBase*>((*children)[4].get()); @@ -933,14 +908,14 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); - ASSERT_BSONOBJ_EQ((*children)[0]->serialize(), + ASSERT_BSONOBJ_EQ((*children)[0]->serialize(true), BSON("control.max.time" << BSON("$_internalExprGt" << date))); ASSERT_BSONOBJ_EQ( - (*children)[1]->serialize(), + (*children)[1]->serialize(true), BSON("control.min.time" << BSON("$_internalExprGt" << dateMinusBucketSpan))); auto idPred = dynamic_cast<ComparisonMatchExpressionBase*>((*children)[2].get()); @@ -982,14 +957,14 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); - ASSERT_BSONOBJ_EQ((*children)[0]->serialize(), + ASSERT_BSONOBJ_EQ((*children)[0]->serialize(true), BSON("control.max.time" << BSON("$_internalExprGte" << date))); ASSERT_BSONOBJ_EQ( - (*children)[1]->serialize(), + (*children)[1]->serialize(true), BSON("control.min.time" << BSON("$_internalExprGte" << dateMinusBucketSpan))); auto idPred = dynamic_cast<ComparisonMatchExpressionBase*>((*children)[2].get()); @@ -1025,8 +1000,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_FALSE(predicate); } } { @@ -1047,8 +1021,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_FALSE(predicate); } } { @@ -1069,8 +1042,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_FALSE(predicate); } } { @@ -1093,8 +1065,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_FALSE(predicate); } } { @@ -1115,8 +1086,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate.loosePredicate); - ASSERT_FALSE(predicate.tightPredicate); + ASSERT_FALSE(predicate); } } } @@ -1137,11 +1107,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(), + ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{$_internalBucketGeoWithin: { withinRegion: { $geometry: { type : " "\"Polygon\" ,coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 " "] ] ]}},field: \"loc\"}}")); - ASSERT_FALSE(predicate.tightPredicate); } } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp index c1221e9c9f7..70b524c7510 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp @@ -94,49 +94,6 @@ TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetadata) { ASSERT_BSONOBJ_EQ(optimized, serialized[0]); } -// Test SERVER-73822 fix: complex $min and $max (i.e. not just straight field refs) work correctly. -TEST_F(InternalUnpackBucketGroupReorder, MinMaxComplexGroupOnMetadata) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], metaField: 'meta1', timeField: 't', " - "bucketMaxSpanSeconds: 3600}}"); - auto groupSpecObj = fromjson( - "{$group: {_id: '$meta1.a.b', accmin: {$min: {$add: ['$b', {$const: 0}]}}, accmax: {$max: " - "{$add: [{$const: 0}, '$c']}}}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, groupSpecObj), getExpCtx()); - pipeline->optimizePipeline(); - - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2, serialized.size()); - - // Order of fields may be different from the original stage. - auto unpackSpecResultObj = fromjson( - "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], timeField: 't', metaField: 'meta1', " - "bucketMaxSpanSeconds: 3600}}"); - ASSERT_BSONOBJ_EQ(unpackSpecResultObj, serialized[0]); - - auto groupSpecOutputObj = fromjson( - "{$group: {_id: '$meta1.a.b', accmin: {$min: {$add: ['$b', {$const: 0}]}}, accmax: {$max: " - "{$add: ['$c', {$const: 0}]}}}}"); - ASSERT_BSONOBJ_EQ(groupSpecOutputObj, serialized[1]); -} - -TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetafield) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], metaField: 'meta1', timeField: 't', " - "bucketMaxSpanSeconds: 3600}}"); - auto groupSpecObj = fromjson("{$group: {_id: '$meta1.a.b', accmin: {$min: '$meta1.f1'}}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, groupSpecObj), getExpCtx()); - pipeline->optimizePipeline(); - - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(1, serialized.size()); - - auto optimized = fromjson("{$group: {_id: '$meta.a.b', accmin: {$min: '$meta.f1'}}}"); - ASSERT_BSONOBJ_EQ(optimized, serialized[0]); -} - TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetadataNegative) { auto unpackSpecObj = fromjson( "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], timeField: 't', metaField: 'meta', " @@ -169,21 +126,5 @@ TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetadataNegative1) { ASSERT_BSONOBJ_EQ(groupSpecObj, serialized[1]); } -TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetafieldIdObj) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], metaField: 'meta1', timeField: 't', " - "bucketMaxSpanSeconds: 3600}}"); - - auto groupSpecObj = - fromjson("{$group: {_id: { d: '$meta1.a.b' }, accmin: {$min: '$meta1.f1'}}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, groupSpecObj), getExpCtx()); - pipeline->optimizePipeline(); - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(1, serialized.size()); - auto optimized = fromjson("{$group: {_id: {d: '$meta.a.b'}, accmin: {$min: '$meta.f1'}}}"); - ASSERT_BSONOBJ_EQ(optimized, serialized[0]); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp index 5b87a64a258..cfe92c09f5f 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp @@ -40,8 +40,7 @@ namespace mongo { namespace { using OptimizePipeline = AggregationContextFixture; -const auto kExplain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; + TEST_F(OptimizePipeline, MixedMatchPushedDown) { auto unpack = fromjson( "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " @@ -54,8 +53,8 @@ TEST_F(OptimizePipeline, MixedMatchPushedDown) { pipeline->optimizePipeline(); // To get the optimized $match from the pipeline, we have to serialize with explain. - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(2u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(3u, stages.size()); // We should push down the $match on the metaField and the predicates on the control field. // The created $match stages should be added before $_internalUnpackBucket and merged. @@ -64,10 +63,8 @@ TEST_F(OptimizePipeline, MixedMatchPushedDown) { "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ] } ] } } ] }]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $lte: 4 } } } }"), - stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); } TEST_F(OptimizePipeline, MetaMatchPushedDown) { @@ -105,8 +102,8 @@ TEST_F(OptimizePipeline, MixedMatchOr) { pipeline->optimizePipeline(); - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(2u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(3u, stages.size()); auto expected = fromjson( "{$match: {$and: [" // Result of pushing down {x: {$lte: 1}}. @@ -126,11 +123,8 @@ TEST_F(OptimizePipeline, MixedMatchOr) { " ]}" "]}}"); ASSERT_BSONOBJ_EQ(expected, stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"foo\", " - "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { $and: [ { x: { $lte: 1 } }, { $or: [ { " - "\"myMeta.a\": { $gt: 1 } }, { y: { $lt: 1 } } ] } ] } } }"), - stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(match, stages[2].getDocument().toBson()); } TEST_F(OptimizePipeline, MixedMatchOnlyMetaMatchPushedDown) { @@ -148,13 +142,11 @@ TEST_F(OptimizePipeline, MixedMatchOnlyMetaMatchPushedDown) { // We should push down the $match on the metaField but not the predicate on '$a', which is // ineligible because of the $type. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); + ASSERT_EQ(3u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$gte: 0}}, {meta: {$lte: 5}}]}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $type: [ 2 ] } } } }"), - serialized[1]); + ASSERT_BSONOBJ_EQ(unpack, serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$type: [ 2 ]}}}"), serialized[2]); } TEST_F(OptimizePipeline, MultipleMatchesPushedDown) { @@ -171,18 +163,16 @@ TEST_F(OptimizePipeline, MultipleMatchesPushedDown) { // We should push down both the $match on the metaField and the predicates on the control field. // The created $match stages should be added before $_internalUnpackBucket and merged. - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(2u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(3u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [ {meta: {$gte: 0}}," "{meta: {$lte: 5}}," "{$or: [ {'control.min.a': {$_internalExprLte: 4}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $lte: 4 } } } }"), - stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); } TEST_F(OptimizePipeline, MultipleMatchesPushedDownWithSort) { @@ -200,19 +190,17 @@ TEST_F(OptimizePipeline, MultipleMatchesPushedDownWithSort) { // We should push down both the $match on the metaField and the predicates on the control field. // The created $match stages should be added before $_internalUnpackBucket and merged. - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(3u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(4u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [ { meta: { $gte: 0 } }," "{meta: { $lte: 5 } }," "{$or: [ { 'control.min.a': { $_internalExprLte: 4 } }," "{$expr: { $ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ] } ] } } ] }]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $lte: 4 } } } }"), - stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$sort: {sortKey: {a: 1}}}"), stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$sort: {sortKey: {a: 1}}}"), stages[3].getDocument().toBson()); } TEST_F(OptimizePipeline, MetaMatchThenCountPushedDown) { @@ -273,7 +261,7 @@ TEST_F(OptimizePipeline, SortThenMixedMatchPushedDown) { // We should push down both the $sort and parts of the $match. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); + ASSERT_EQ(4u, serialized.size()); auto expected = fromjson( "{$match: {$and: [" " {meta: {$eq: 'abc'}}," @@ -286,10 +274,8 @@ TEST_F(OptimizePipeline, SortThenMixedMatchPushedDown) { "]}}"); ASSERT_BSONOBJ_EQ(expected, serialized[0]); ASSERT_BSONOBJ_EQ(fromjson("{$sort: {meta: -1}}"), serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $gte: 5 } } } }"), - serialized[2]); + ASSERT_BSONOBJ_EQ(unpack, serialized[2]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$gte: 5}}}"), serialized[3]); } TEST_F(OptimizePipeline, MetaMatchThenSortPushedDown) { @@ -344,20 +330,19 @@ TEST_F(OptimizePipeline, MixedMatchThenProjectPushedDown) { pipeline->optimizePipeline(); // We can push down part of the $match and use dependency analysis on the end of the pipeline. - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(3u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(4u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$eq: 'abc'}}," "{$or: [ {'control.min.a': { $_internalExprLte: 4 } }," "{$expr: { $ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ] } ] } } ] } ]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ( - fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"x\" ], timeField: " - "\"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $lte: 4 } } } }"), - stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['_id', 'a', 'x'], timeField: " + "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), + stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); ASSERT_BSONOBJ_EQ(fromjson("{$project: {_id: true, x: true}}"), - stages[2].getDocument().toBson()); + stages[3].getDocument().toBson()); } @@ -393,22 +378,22 @@ TEST_F(OptimizePipeline, ProjectThenMixedMatchPushedDown) { pipeline->optimizePipeline(); // We should push down part of the $match and do dependency analysis on the rest. - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(3u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(4u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$eq: \"abc\"}}," "{$or: [ {'control.min.a': {$_internalExprLte: 4}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"), stages[0].getDocument().toBson()); ASSERT_BSONOBJ_EQ( - fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"x\", \"myMeta\" ], " - "timeField: \"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $lte: 4 } } } }"), + fromjson("{$_internalUnpackBucket: { include: ['_id', 'a', 'x', 'myMeta'], timeField: " + "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); const UnorderedFieldsBSONObjComparator kComparator; ASSERT_EQ( kComparator.compare(fromjson("{$project: {_id: true, a: true, myMeta: true, x: true}}"), - stages[2].getDocument().toBson()), + stages[3].getDocument().toBson()), 0); } @@ -424,8 +409,8 @@ TEST_F(OptimizePipeline, ProjectWithRenameThenMixedMatchPushedDown) { pipeline->optimizePipeline(); // We should push down part of the $match and do dependency analysis on the end of the pipeline. - auto stages = pipeline->writeExplainOps(kExplain); - ASSERT_EQ(3u, stages.size()); + auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); + ASSERT_EQ(4u, stages.size()); ASSERT_BSONOBJ_EQ( fromjson("{$match: {$and: [{$or: [ {'control.max.y': {$_internalExprGte: \"abc\"}}," "{$expr: {$ne: [ {$type: [ \"$control.min.y\" ]}," @@ -434,13 +419,13 @@ TEST_F(OptimizePipeline, ProjectWithRenameThenMixedMatchPushedDown) { "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ( - fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"y\" ], timeField: " - "\"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { $and: [ { y: { $gte: \"abc\" } }, { a: { $lte: 4 } } ] } } }"), - stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$project: {_id: true, a: true, myMeta: '$y'}}"), + ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['_id', 'a', 'y'], timeField: " + "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), + stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{y: {$gte: 'abc'}}, {a: {$lte: 4}}]}}"), stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$project: {_id: true, a: true, myMeta: '$y'}}"), + stages[3].getDocument().toBson()); } TEST_F(OptimizePipeline, ComputedProjectThenMetaMatchPushedDown) { @@ -481,15 +466,15 @@ TEST_F(OptimizePipeline, ComputedProjectThenMetaMatchNotPushedDown) { // We should both push down the project and internalize the remaining project, but we can't // push down the meta match due to the (now invalid) renaming. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); + ASSERT_EQ(3u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {myMeta: {$sum: ['$meta.a', '$meta.b']}}}"), serialized[0]); ASSERT_BSONOBJ_EQ( - fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"myMeta\" ], timeField: " - "\"time\", metaField: \"myMeta\", " - "bucketMaxSpanSeconds: 3600, computedMetaProjFields: [ \"myMeta\" ], " - "eventFilter: { myMeta: { $gte: \"abc\" } } } }"), + fromjson( + "{$_internalUnpackBucket: { include: ['_id', 'myMeta'], timeField: 'time', metaField: " + "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['myMeta']}}"), serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {myMeta: {$gte: 'abc'}}}"), serialized[2]); } // namespace TEST_F(OptimizePipeline, ComputedProjectThenMatchNotPushedDown) { @@ -506,13 +491,13 @@ TEST_F(OptimizePipeline, ComputedProjectThenMatchNotPushedDown) { // We should push down the computed project but not the match, because it depends on the newly // computed values. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); + ASSERT_EQ(3u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {y: {$sum: ['$meta.a', '$meta.b']}}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"y\" ], " - "timeField: \"time\", metaField: \"myMeta\", " - "bucketMaxSpanSeconds: 3600, computedMetaProjFields: [ \"y\" ], " - "eventFilter: { y: { $gt: \"abc\" } } } }"), - serialized[1]); + ASSERT_BSONOBJ_EQ( + fromjson("{$_internalUnpackBucket: { include: ['_id', 'y'], timeField: 'time', metaField: " + "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['y']}}"), + serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {y: {$gt: 'abc'}}}"), serialized[2]); } TEST_F(OptimizePipeline, MetaSortThenProjectPushedDown) { @@ -765,55 +750,6 @@ TEST_F(OptimizePipeline, InternalizeProjectAndPushdownAddFields) { serialized[1]); } -TEST_F(OptimizePipeline, DoNotSwapAddFieldsIfDependencyIsExcluded) { - { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); - auto projectSpecObj = fromjson("{$project: {x: true, _id: false}}"); - auto addFieldsSpec = fromjson("{$addFields: {newMeta: '$myMeta'}}"); - - auto pipeline = - Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj, addFieldsSpec), getExpCtx()); - - pipeline->optimizePipeline(); - - // We should internalize the $project but _not_ push down the $addFields because it's field - // dependency has been excluded. Theoretically we could remove the $addFields for this - // trivial except but not always. - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['x'], timeField: 'time', " - "metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), - serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {newMeta: '$myMeta'}}"), serialized[1]); - } - - // Similar test except the dependency is on an excluded non-meta field. - { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); - auto projectSpecObj = fromjson("{$project: {x: true, _id: false}}"); - auto addFieldsSpec = fromjson("{$addFields: {newMeta: '$excluded'}}"); - - auto pipeline = - Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj, addFieldsSpec), getExpCtx()); - - pipeline->optimizePipeline(); - - // We should internalize the $project but _not_ push down the $addFields because it's field - // dependency has been excluded. Theoretically we could remove the $addFields for this - // trivial except but not always. - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['x'], timeField: 'time', " - "metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), - serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {newMeta: '$excluded'}}"), serialized[1]); - } -} - TEST_F(OptimizePipeline, PushdownSortAndAddFields) { auto unpackSpecObj = fromjson( "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " @@ -872,7 +808,7 @@ TEST_F(OptimizePipeline, MatchWithGeoWithinOnMeasurementsPushedDownUsingInternal pipeline->optimizePipeline(); auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(serialized.size(), 2U); + ASSERT_EQ(serialized.size(), 3U); // $match with $geoWithin on a non-metadata field is pushed down and $_internalBucketGeoWithin // is used. @@ -881,12 +817,12 @@ TEST_F(OptimizePipeline, MatchWithGeoWithinOnMeasurementsPushedDownUsingInternal "\"Polygon\" ,coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 " "] ] ]}},field: \"loc\"}}}"), serialized[0]); - ASSERT_BSONOBJ_EQ( - fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "bucketMaxSpanSeconds: 3600, " - "eventFilter: { loc: { $geoWithin: { $geometry: { type: \"Polygon\", coordinates: " - "[ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ] } } } } } }"), - serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: {exclude: [], timeField: " + "'time', bucketMaxSpanSeconds: 3600}}"), + serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {loc: {$geoWithin: {$geometry: {type: \"Polygon\", " + "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]}}}}}"), + serialized[2]); } TEST_F(OptimizePipeline, MatchWithGeoWithinOnMetaFieldIsPushedDown) { @@ -928,7 +864,7 @@ TEST_F(OptimizePipeline, pipeline->optimizePipeline(); auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(serialized.size(), 2U); + ASSERT_EQ(serialized.size(), 3U); // $match with $geoIntersects on a non-metadata field is pushed down and // $_internalBucketGeoWithin is used. @@ -937,12 +873,12 @@ TEST_F(OptimizePipeline, "\"Polygon\" ,coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 " "] ] ]}},field: \"loc\"}}}"), serialized[0]); - ASSERT_BSONOBJ_EQ( - fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " - "bucketMaxSpanSeconds: 3600, " - "eventFilter: { loc: { $geoIntersects: { $geometry: { type: \"Polygon\", " - "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ] } } } } } }"), - serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: {exclude: [], timeField: " + "'time', bucketMaxSpanSeconds: 3600}}"), + serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {loc: {$geoIntersects: {$geometry: {type: \"Polygon\", " + "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]}}}}}"), + serialized[2]); } TEST_F(OptimizePipeline, MatchWithGeoIntersectsOnMetaFieldIsPushedDown) { @@ -971,103 +907,5 @@ TEST_F(OptimizePipeline, MatchWithGeoIntersectsOnMetaFieldIsPushedDown) { "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), serialized[1]); } - -TEST_F(OptimizePipeline, StreamingGroupIsEnabledWhenPossible) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: {exclude: [], timeField: " - "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"); - auto groupSpecObj = fromjson( - "{$group: {_id: {hour: {$dateTrunc: {date: '$time', unit: 'hour'}}, symbol: " - "'$myMeta.symbol'}" - ", 'sum': {$sum: '$tradeAmount'}}}"); - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, - fromjson("{$sort: {time: 1}}"), - fromjson("{$match: {'tradePrice': 100}}"), - groupSpecObj), - getExpCtx()); - - ASSERT_EQ(pipeline->getSources().size(), 4U); - - pipeline->optimizePipeline(); - - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(serialized.size(), 4U); - - auto streamingGroupSpecObj = fromjson( - "{$_internalStreamingGroup: {_id: {hour: {$dateTrunc: {date: '$time', unit: {$const: " - "'hour'}}}, symbol: '$myMeta.symbol'}, 'sum': {$sum: '$tradeAmount'}, " - "'$monotonicIdFields': ['hour']}}"); - ASSERT_BSONOBJ_EQ(streamingGroupSpecObj, serialized.back()); -} - -TEST_F(OptimizePipeline, StreamingGroupIsNotEnabledWhenTimeFieldIsModified) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: {exclude: [], timeField: " - "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"); - auto groupSpecObj = fromjson( - "{$group: {_id: {hour: '$time', symbol: '$myMeta.symbol'}, 'sum': {$sum: " - "'$tradeAmount'}}}"); - auto pipeline = Pipeline::parse( - makeVector(unpackSpecObj, - fromjson("{$addFields: {'time': {$dateTrunc: {date: '$time', unit: 'hour'}}}}"), - fromjson("{$sort: {time: 1}}"), - groupSpecObj), - getExpCtx()); - - ASSERT_EQ(pipeline->getSources().size(), 4U); - - pipeline->optimizePipeline(); - - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(serialized.size(), 4U); - ASSERT_BSONOBJ_EQ(groupSpecObj, serialized.back()); -} - -TEST_F(OptimizePipeline, ComputedMetaProjFieldsAreNotInInclusionProjection) { - auto pipeline = Pipeline::parse( - makeVector( - fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: " - "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['time', 'y']}}"), - fromjson("{$project: {time: 1, x: 1}}")), - getExpCtx()); - ASSERT_EQ(2u, pipeline->getSources().size()); - - pipeline->optimizePipeline(); - - // The fields in 'computedMetaProjFields' that are not in the project should be removed. - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(1u, serialized.size()); - ASSERT_BSONOBJ_EQ( - fromjson("{$_internalUnpackBucket: { include: ['_id', 'time', 'x'], timeField: 'time', " - "metaField: " - "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['time']}}"), - serialized[0]); -} - -TEST_F(OptimizePipeline, ComputedMetaProjectFieldsAfterInclusionGetsAddedToIncludes) { - - auto pipeline = Pipeline::parse( - makeVector(fromjson("{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: " - "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: []}}"), - fromjson("{$project: {myMeta: 1}}"), - fromjson("{$addFields: {newMeta: {$toUpper : '$myMeta'}}}")), - getExpCtx()); - ASSERT_EQ(3u, pipeline->getSources().size()); - - pipeline->optimizePipeline(); - - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - - ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {newMeta: {$toUpper: ['$meta']}}}"), serialized[0]); - - // 'newMeta' field gets added to 'computedMetaProjFields' and to 'include'. - auto expectedSpecObj = fromjson( - "{$_internalUnpackBucket: { include: ['_id','newMeta', 'myMeta'], timeField: 'time', " - "metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['newMeta']}}"); - ASSERT_BSONOBJ_EQ(expectedSpecObj, serialized[1]); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/pushdown_computed_meta_projections_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/pushdown_computed_meta_projections_test.cpp index 9092bfbfbf4..3f34f8cc345 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/pushdown_computed_meta_projections_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/pushdown_computed_meta_projections_test.cpp @@ -113,8 +113,8 @@ TEST_F(InternalUnpackBucketPushdownProjectionsTest, OptimizeAddFieldsWith2MetaPr TEST_F(InternalUnpackBucketPushdownProjectionsTest, SplitAddFieldsWithMixedProjectionFields) { auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'foo', metaField: " - "'myMeta',bucketMaxSpanSeconds: 3600}}"); + "{$_internalUnpackBucket: { exclude: [], timeField: 'foo', metaField: 'myMeta', " + "bucketMaxSpanSeconds: 3600}}"); auto addFieldsSpecObj = fromjson("{$addFields: {device: '$myMeta.a', temp: {$add: ['$temperature', '$offset']}}}"); @@ -336,110 +336,5 @@ TEST_F(InternalUnpackBucketPushdownProjectionsTest, DoNotPushDownNestedProjectio ASSERT_BSONOBJ_EQ(projectSpecObj, serialized[1]); } -/****************** $project stage with $getField expression ****************************/ - -// We do not push down projections with the '$getField' expression when the input to '$getField' is -// just a string. In this case $getField will always prepend the $$CURRENT field path for string -// inputs and thus also require the 'needWholeDocument' dependency. So for all values of {$getField: -// "string"} we will not perform this rewrite. Even though, we could perform the rewrite here when -// the string is the metaField, the server cannot differentiate between 'meta' and '$meta' field -// paths, where one is the metaField and the other is not in the expression dependencies. To avoid -// incorrect query results in this edge case, we restrict all rewrites with {$getField: "string"}. -// Note that we do not expect users to use $getField to query their metaField. -TEST_F(InternalUnpackBucketPushdownProjectionsTest, - DoNotPushDownNestedProjectionWithGetFieldJustString) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); - auto projectSpecObj = fromjson( - "{$project: {_id: true, x: true, data: {z: {$add: [{$getField: 'myMeta'}, " - "'$myMeta.b']}}}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj), getExpCtx()); - auto& container = pipeline->getSources(); - auto unpack = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.begin()->get()); - auto nextStageIsRemoved = unpack->pushDownComputedMetaProjection(container.begin(), &container); - - ASSERT_EQ(nextStageIsRemoved, false); - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - auto projectSerialized = fromjson( - "{$project: {_id: true, x: true, data: {z: {$add: [{$getField: { field: { $const: " - "'myMeta'}, input:'$$CURRENT' } },'$myMeta.b']}}}}"); - - ASSERT_BSONOBJ_EQ(projectSerialized, serialized[1]); -} - -// However, we can push down $getField if we have the entire path and do not rely on $$CURRENT. If -// the entire path is only on the metaField, we can pushdown the projection. -TEST_F(InternalUnpackBucketPushdownProjectionsTest, DoPushDownNestedProjectionWithGetFieldInput) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); - auto projectSpecObj = - fromjson("{$project: {_id : true, device: {$getField: {input: '$myMeta', field:'a'}}}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj), getExpCtx()); - auto& container = pipeline->getSources(); - auto unpack = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.begin()->get()); - auto nextStageIsRemoved = unpack->pushDownComputedMetaProjection(container.begin(), &container); - - ASSERT_EQ(nextStageIsRemoved, false); - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); - ASSERT_BSONOBJ_EQ( - fromjson( - "{$addFields: { device: { $getField: { field: { $const: 'a' }, input: '$meta' }}}}"), - serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { exclude: [], timeField: 'time', " - "metaField: 'myMeta', bucketMaxSpanSeconds: 3600, " - "computedMetaProjFields: ['device']}}"), - serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$project: {_id : true, device: true}}"), serialized[2]); -} - -TEST_F(InternalUnpackBucketPushdownProjectionsTest, - DoNotPushDownNestedProjectionWithMeasurementGetField) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); - auto projectSpecObj = fromjson( - "{$project: {_id: true, x: true, data: {z: {$add: [{$getField: {input: '$other', " - "field:'a'}},'$myMeta.b']}}}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj), getExpCtx()); - auto& container = pipeline->getSources(); - auto unpack = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.begin()->get()); - auto nextStageIsRemoved = unpack->pushDownComputedMetaProjection(container.begin(), &container); - - ASSERT_EQ(nextStageIsRemoved, false); - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - auto projectSerialized = fromjson( - "{$project: {_id: true, x: true, data: {z: {$add: [{$getField: { field: {$const: 'a'}, " - "input:'$other' } },'$myMeta.b']}}}}"); - - ASSERT_BSONOBJ_EQ(projectSerialized, serialized[1]); -} - -TEST_F(InternalUnpackBucketPushdownProjectionsTest, - DoNotPushDownNestedProjectionWhichNeedsWholeDoc) { - auto unpackSpecObj = fromjson( - "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); - auto projectSpecObj = fromjson("{$project: {_id: true, x: true, data: '$$ROOT'}}"); - - auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj), getExpCtx()); - auto& container = pipeline->getSources(); - auto unpack = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.begin()->get()); - auto nextStageIsRemoved = unpack->pushDownComputedMetaProjection(container.begin(), &container); - - ASSERT_EQ(nextStageIsRemoved, false); - auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - ASSERT_BSONOBJ_EQ(unpackSpecObj, serialized[0]); - ASSERT_BSONOBJ_EQ(projectSpecObj, serialized[1]); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp index 4ce5d558ac4..ba4f31adf17 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp @@ -56,7 +56,7 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeSplitsMatchAndMaps // predicate on 'control.min.a'. These two created $match stages should be added before // $_internalUnpackBucket and merged. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); + ASSERT_EQ(3u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [" " {meta: {$gte: 0}}," " {meta: {$lte: 5}}," @@ -68,13 +68,8 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeSplitsMatchAndMaps " ]}" "]}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { " - "exclude: [], " - "timeField: \"foo\", " - "metaField: \"myMeta\", " - "bucketMaxSpanSeconds: 3600, " - "eventFilter: { a: { $lte: 4 } } } }"), - serialized[1]); + ASSERT_BSONOBJ_EQ(unpack, serialized[1]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), serialized[2]); } TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeMovesMetaMatchBeforeUnpack) { @@ -99,6 +94,10 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, auto unpack = fromjson( "{$_internalUnpackBucket: { exclude: [], timeField: 'foo', metaField: 'myMeta', " "bucketMaxSpanSeconds: 3600}}"); + auto unpackExcluded = fromjson( + "{$_internalUnpackBucket: { include: ['_id', 'data'], timeField: 'foo', metaField: " + "'myMeta', " + "bucketMaxSpanSeconds: 3600}}"); auto pipeline = Pipeline::parse(makeVector(unpack, fromjson("{$project: {data: 1}}"), fromjson("{$match: {myMeta: {$gte: 0}}}")), @@ -109,11 +108,9 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, // The $match on meta is not moved before $_internalUnpackBucket since the field is excluded. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(1u, serialized.size()); - ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"data\" ], " - "timeField: \"foo\", metaField: \"myMeta\", bucketMaxSpanSeconds: " - "3600, eventFilter: { myMeta: { $gte: 0 } } } }"), - serialized[0]); + ASSERT_EQ(2u, serialized.size()); + ASSERT_BSONOBJ_EQ(unpackExcluded, serialized[0]); + ASSERT_BSONOBJ_EQ(fromjson("{$match: {myMeta: {$gte: 0}}}"), serialized[1]); } TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, @@ -137,7 +134,7 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, // We should fail to split the match because of the $or clause. We should still be able to // map the predicate on 'x' to a predicate on the control field. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); + ASSERT_EQ(3u, serialized.size()); auto expected = fromjson( "{$match: {$and: [" // Result of pushing down {x: {$lte: 1}}. @@ -157,13 +154,8 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, " ]}" "]}}"); ASSERT_BSONOBJ_EQ(expected, serialized[0]); - ASSERT_BSONOBJ_EQ( - fromjson( - "{ $_internalUnpackBucket: { " - "exclude: [], timeField: \"foo\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " - "eventFilter: { $and: [ { x: { $lte: 1 } }, { $or: [ { \"myMeta.a\": { $gt: 1 } }, { " - "y: { $lt: 1 } } ] } ] } } }"), - serialized[1]); + ASSERT_BSONOBJ_EQ(unpack, serialized[1]); + ASSERT_BSONOBJ_EQ(match, serialized[2]); } } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp index 745cc50529a..06217344dad 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/unpack_bucket_exec_test.cpp @@ -890,13 +890,7 @@ TEST_F(InternalUnpackBucketExecTest, ParserRoundtripsComputedMetaProjFieldsInclu auto array = std::vector<Value>{}; DocumentSourceInternalUnpackBucket::createFromBsonInternal(bson.firstElement(), getExpCtx()) ->serializeToArray(array); - - // Since fields in 'computedMetaProjFields' are not in 'include' they should be removed. - auto expectedBson = fromjson( - "{$_internalUnpackBucket: {include: [], timeField: 'time', metaField: 'meta', " - "bucketMaxSpanSeconds: 3600}}"); - - ASSERT_BSONOBJ_EQ(array[0].getDocument().toBson(), expectedBson); + ASSERT_BSONOBJ_EQ(array[0].getDocument().toBson(), bson); } TEST_F(InternalUnpackBucketExecTest, ParserRoundtripsComputedMetaProjFieldsIncludeWithCompute) { @@ -931,18 +925,5 @@ TEST_F(InternalUnpackBucketExecTest, ParserRoundtripsComputedMetaProjFieldOverri unpackBucket->serializeToArray(array); ASSERT_BSONOBJ_EQ(array[0].getDocument().toBson(), bson); } - -TEST_F(InternalUnpackBucketExecTest, RedactsCorrectly) { - auto bson = fromjson( - "{$_internalUnpackBucket: {include: ['a', 'b', 'c'], timeField: 'time', metaField: 'meta', " - "bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['a', 'b', 'c']}}"); - auto docSource = DocumentSourceInternalUnpackBucket::createFromBsonInternal(bson.firstElement(), - getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - "{$_internalUnpackBucket: {include: [\"HASH<a>\", \"HASH<b>\", \"HASH<c>\"], timeField: " - "\"HASH<time>\", metaField: \"HASH<meta>\", bucketMaxSpanSeconds: \"?number\", " - "computedMetaProjFields: [\"HASH<a>\", \"HASH<b>\", \"HASH<c>\"]}}", - redact(*docSource)); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_limit.cpp b/src/mongo/db/pipeline/document_source_limit.cpp index 2347e185a55..f50e795897d 100644 --- a/src/mongo/db/pipeline/document_source_limit.cpp +++ b/src/mongo/db/pipeline/document_source_limit.cpp @@ -88,8 +88,8 @@ DocumentSource::GetNextResult DocumentSourceLimit::doGetNext() { return nextInput; } -Value DocumentSourceLimit::serialize(const SerializationOptions& opts) const { - return Value(Document{{getSourceName(), opts.serializeLiteral(_limit)}}); +Value DocumentSourceLimit::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(Document{{getSourceName(), _limit}}); } intrusive_ptr<DocumentSourceLimit> DocumentSourceLimit::create( diff --git a/src/mongo/db/pipeline/document_source_limit.h b/src/mongo/db/pipeline/document_source_limit.h index 82de8094431..6fe5370b0c2 100644 --- a/src/mongo/db/pipeline/document_source_limit.h +++ b/src/mongo/db/pipeline/document_source_limit.h @@ -69,7 +69,7 @@ public: */ Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; DepsTracker::State getDependencies(DepsTracker* deps) const final { return DepsTracker::State::SEE_NEXT; // This doesn't affect needed fields diff --git a/src/mongo/db/pipeline/document_source_limit_test.cpp b/src/mongo/db/pipeline/document_source_limit_test.cpp index 5465cf1bdf2..899dbe4de42 100644 --- a/src/mongo/db/pipeline/document_source_limit_test.cpp +++ b/src/mongo/db/pipeline/document_source_limit_test.cpp @@ -161,12 +161,5 @@ TEST_F(DocumentSourceLimitTest, ShouldPropagatePauses) { ASSERT_TRUE(limit->getNext().isEOF()); } -TEST_F(DocumentSourceLimitTest, RedactsCorrectly) { - auto limit = DocumentSourceLimit::create(getExpCtx(), 2); - ASSERT_VALUE_EQ_AUTO( // NOLINT - "{ $limit: \"?number\" }", - redact(*limit)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_list_cached_and_active_users.h b/src/mongo/db/pipeline/document_source_list_cached_and_active_users.h index ce8a4f9883a..f1f1e25e55e 100644 --- a/src/mongo/db/pipeline/document_source_list_cached_and_active_users.h +++ b/src/mongo/db/pipeline/document_source_list_cached_and_active_users.h @@ -87,8 +87,7 @@ public: return kStageName.rawData(); } - Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final { return Value(Document{{getSourceName(), Document{}}}); } diff --git a/src/mongo/db/pipeline/document_source_list_catalog.cpp b/src/mongo/db/pipeline/document_source_list_catalog.cpp index 1675f931d52..1e4e9fde9f1 100644 --- a/src/mongo/db/pipeline/document_source_list_catalog.cpp +++ b/src/mongo/db/pipeline/document_source_list_catalog.cpp @@ -77,8 +77,8 @@ DocumentSource::GetNextResult DocumentSourceListCatalog::doGetNext() { if (!_catalogDocs) { if (pExpCtx->ns.isCollectionlessAggregateNS()) { _catalogDocs = pExpCtx->mongoProcessInterface->listCatalog(pExpCtx->opCtx); - } else if (auto catalogDoc = pExpCtx->mongoProcessInterface->getCatalogEntry( - pExpCtx->opCtx, pExpCtx->ns, pExpCtx->uuid)) { + } else if (auto catalogDoc = pExpCtx->mongoProcessInterface->getCatalogEntry(pExpCtx->opCtx, + pExpCtx->ns)) { _catalogDocs = {{std::move(*catalogDoc)}}; } else { _catalogDocs.emplace(); @@ -133,7 +133,8 @@ intrusive_ptr<DocumentSource> DocumentSourceListCatalog::createFromBson( return new DocumentSourceListCatalog(pExpCtx); } -Value DocumentSourceListCatalog::serialize(const SerializationOptions& opts) const { +Value DocumentSourceListCatalog::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { return Value(DOC(getSourceName() << Document())); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_list_catalog.h b/src/mongo/db/pipeline/document_source_list_catalog.h index 89398ccb519..c3bcb52fe77 100644 --- a/src/mongo/db/pipeline/document_source_list_catalog.h +++ b/src/mongo/db/pipeline/document_source_list_catalog.h @@ -75,7 +75,7 @@ public: // virtuals from DocumentSource const char* getSourceName() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { StageConstraints constraints(StreamType::kStreaming, diff --git a/src/mongo/db/pipeline/document_source_list_local_sessions.h b/src/mongo/db/pipeline/document_source_list_local_sessions.h index 22d7657ea3d..5306fc1fb7e 100644 --- a/src/mongo/db/pipeline/document_source_list_local_sessions.h +++ b/src/mongo/db/pipeline/document_source_list_local_sessions.h @@ -101,9 +101,8 @@ public: return DocumentSourceListLocalSessions::kStageName.rawData(); } - Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { - return Value(Document{{getSourceName(), _spec.toBSON(opts)}}); + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final { + return Value(Document{{getSourceName(), _spec.toBSON()}}); } StageConstraints constraints(Pipeline::SplitState pipeState) const final { diff --git a/src/mongo/db/pipeline/document_source_list_sessions.cpp b/src/mongo/db/pipeline/document_source_list_sessions.cpp index 53744d3aa12..3c19beb4d5f 100644 --- a/src/mongo/db/pipeline/document_source_list_sessions.cpp +++ b/src/mongo/db/pipeline/document_source_list_sessions.cpp @@ -74,12 +74,13 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceListSessions::createFromBson( return new DocumentSourceListSessions(query, pExpCtx, spec.getAllUsers(), spec.getUsers()); } -Value DocumentSourceListSessions::serialize(const SerializationOptions& opts) const { +Value DocumentSourceListSessions::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { ListSessionsSpec spec; spec.setAllUsers(_allUsers); spec.setUsers(_users); spec.setPredicate(_predicate); - return Value(Document{{getSourceName(), spec.toBSON(opts)}}); + return Value(Document{{getSourceName(), spec.toBSON()}}); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_list_sessions.h b/src/mongo/db/pipeline/document_source_list_sessions.h index ec47f6c1f89..9c3e4d10d68 100644 --- a/src/mongo/db/pipeline/document_source_list_sessions.h +++ b/src/mongo/db/pipeline/document_source_list_sessions.h @@ -98,7 +98,7 @@ public: return DocumentSourceListSessions::kStageName.rawData(); } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { return {StreamType::kStreaming, diff --git a/src/mongo/db/pipeline/document_source_list_sessions.idl b/src/mongo/db/pipeline/document_source_list_sessions.idl index c293e85b589..0db282831dc 100644 --- a/src/mongo/db/pipeline/document_source_list_sessions.idl +++ b/src/mongo/db/pipeline/document_source_list_sessions.idl @@ -39,31 +39,21 @@ structs: ListSessionsUser: description: "A struct representing a $listSessions/$listLocalSessions User" strict: true - query_shape_component: true fields: - user: - type: string - query_shape: anonymize - db: - type: string - query_shape: anonymize + user: string + db: string ListSessionsSpec: description: "$listSessions and $listLocalSessions pipeline spec" strict: true - query_shape_component: true fields: allUsers: type: bool default: false - # This boolean parameterizes the stage rather than representing user input, so do not abstract the literal. - query_shape: parameter users: type: array<ListSessionsUser> optional: true - query_shape: literal $_internalPredicate: cpp_name: predicate type: object optional: true - query_shape: literal # This is a MatchExpression predicate and could be shape-ified rather than completely abstracted. diff --git a/src/mongo/db/pipeline/document_source_lookup.cpp b/src/mongo/db/pipeline/document_source_lookup.cpp index ca9be5509bf..cfb885e94c5 100644 --- a/src/mongo/db/pipeline/document_source_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_lookup.cpp @@ -35,12 +35,10 @@ #include "mongo/db/exec/document_value/value.h" #include "mongo/db/jsobj.h" #include "mongo/db/matcher/expression_algo.h" -#include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregation_request_helper.h" #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/document_source_documents.h" #include "mongo/db/pipeline/document_source_merge_gen.h" -#include "mongo/db/pipeline/document_source_queue.h" #include "mongo/db/pipeline/document_source_sort.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context.h" @@ -110,8 +108,7 @@ NamespaceString parseLookupFromAndResolveNamespace(const BSONElement& elem, Stri str::stream() << "$lookup with syntax {from: {db:<>, coll:<>},..} is not supported for db: " << nss.db() << " and coll: " << nss.coll(), nss.isConfigDotCacheDotChunks() || nss == NamespaceString::kRsOplogNamespace || - nss == NamespaceString::kTenantMigrationOplogView || - nss == NamespaceString::kConfigsvrCollectionsNamespace); + nss == NamespaceString::kTenantMigrationOplogView); return nss; } @@ -180,20 +177,15 @@ DocumentSourceLookUp::DocumentSourceLookUp( // We append an additional BSONObj to '_resolvedPipeline' as a placeholder for the $match stage // we'll eventually construct from the input document. _resolvedPipeline.reserve(_resolvedPipeline.size() + 1); - - // Initialize the introspection pipeline before we insert the $match. This is okay because we do - // not use the introspection pipeline during/after query execution, which is when the $match is - // necessary. - initializeResolvedIntrospectionPipeline(); - _resolvedPipeline.push_back(BSON("$match" << BSONObj())); _fieldMatchPipelineIdx = _resolvedPipeline.size() - 1; + + initializeResolvedIntrospectionPipeline(); } std::vector<BSONObj> extractSourceStage(const std::vector<BSONObj>& pipeline) { if (!pipeline.empty() && (pipeline[0].hasField(DocumentSourceDocuments::kStageName) || - pipeline[0].hasField(DocumentSourceQueue::kStageName) || pipeline[0].hasField("$search"_sd))) { return {pipeline[0]}; } @@ -220,9 +212,8 @@ DocumentSourceLookUp::DocumentSourceLookUp( // the local/foreignField $match. It must next after $documents if present. auto sourceStages = extractSourceStage(pipeline); _resolvedPipeline.insert(_resolvedPipeline.end(), sourceStages.begin(), sourceStages.end()); - // Save the correct position of the $match, but wait to insert it until we have finished - // constructing the pipeline and created the introspection pipeline below. - _fieldMatchPipelineIdx = _resolvedPipeline.size(); + _resolvedPipeline.push_back(BSON("$match" << BSONObj())); + _fieldMatchPipelineIdx = _resolvedPipeline.size() - 1; // Add the user pipeline to '_resolvedPipeline' after any potential view prefix and $match _resolvedPipeline.insert( _resolvedPipeline.end(), pipeline.begin() + sourceStages.size(), pipeline.end()); @@ -247,25 +238,15 @@ DocumentSourceLookUp::DocumentSourceLookUp( _variablesParseState.defineVariable(varName)); } - // Initialize the introspection pipeline before we insert the $match (if applicable). This is - // okay because we only use the introspection pipeline for reference while doing query analysis - // and analyzing involved dependencies/variables/collections/constraints. We do not use the - // introspection pipeline during/after query execution, which is when the $match is necessary. - // It wouldn't hurt anything to include the $match in this pipeline, but we also use the - // introspection pipeline in serialization, so it would be a bit odd to include an extra empty - // $match. initializeResolvedIntrospectionPipeline(); - - // Finally, insert the $match placeholder if we need it. - if (_fieldMatchPipelineIdx) { - _resolvedPipeline.insert(_resolvedPipeline.begin() + *_fieldMatchPipelineIdx, - BSON("$match" << BSONObj())); - } } DocumentSourceLookUp::DocumentSourceLookUp(const DocumentSourceLookUp& original, const boost::intrusive_ptr<ExpressionContext>& newExpCtx) - : DocumentSource(kStageName, newExpCtx), + : DocumentSource( + kStageName, + newExpCtx ? newExpCtx + : original.pExpCtx->copyWith(original.pExpCtx->ns, original.pExpCtx->uuid)), _fromNs(original._fromNs), _resolvedNs(original._resolvedNs), _as(original._as), @@ -285,10 +266,10 @@ DocumentSourceLookUp::DocumentSourceLookUp(const DocumentSourceLookUp& original, _cache.emplace(internalDocumentSourceCursorBatchSizeBytes.load()); } if (original._matchSrc) { - _matchSrc = static_cast<DocumentSourceMatch*>(original._matchSrc->clone(pExpCtx).get()); + _matchSrc = static_cast<DocumentSourceMatch*>(original._matchSrc->clone().get()); } if (original._unwindSrc) { - _unwindSrc = static_cast<DocumentSourceUnwind*>(original._unwindSrc->clone(pExpCtx).get()); + _unwindSrc = static_cast<DocumentSourceUnwind*>(original._unwindSrc->clone().get()); } } @@ -418,11 +399,6 @@ StageConstraints DocumentSourceLookUp::constraints(Pipeline::SplitState pipeStat // This stage will only be on the shards pipeline if $lookup on sharded foreign collections // is allowed. hostRequirement = HostTypeRequirement::kAnyShard; - } else if (_fromNs == NamespaceString::kConfigsvrCollectionsNamespace) { - // This is an unsharded collection, but the primary shard would be the config server, and - // the config servers are not prepared to take queries. Instead, we'll merge on any of the - // other shards. - hostRequirement = HostTypeRequirement::kAnyShard; } else { // If the pipeline is unsplit or this stage is on the merging part of the pipeline, // when $lookup on sharded foreign collections is allowed, the foreign collection is @@ -578,7 +554,6 @@ std::unique_ptr<Pipeline, PipelineDeleter> DocumentSourceLookUp::buildPipeline( // If we don't have a cache, build and return the pipeline immediately. if (!_cache || _cache->isAbandoned()) { MakePipelineOptions pipelineOpts; - pipelineOpts.alreadyOptimized = false; pipelineOpts.optimize = true; pipelineOpts.attachCursorSource = true; pipelineOpts.validator = lookupPipeValidator; @@ -613,7 +588,6 @@ std::unique_ptr<Pipeline, PipelineDeleter> DocumentSourceLookUp::buildPipeline( // Construct the basic pipeline without a cache stage. Avoid optimizing here since we need to // add the cache first, as detailed below. MakePipelineOptions pipelineOpts; - pipelineOpts.alreadyOptimized = false; pipelineOpts.optimize = false; pipelineOpts.attachCursorSource = false; pipelineOpts.validator = lookupPipeValidator; @@ -669,59 +643,25 @@ std::unique_ptr<Pipeline, PipelineDeleter> DocumentSourceLookUp::buildPipeline( return pipeline; } -/** - * Method that looks for a DocumentSourceSequentialDocumentCache stage and calls optimizeAt() on - * it if it has yet to be optimized. - */ -void findAndOptimizeSequentialDocumentCache(Pipeline& pipeline) { - auto& container = pipeline.getSources(); - auto itr = (&container)->begin(); - while (itr != (&container)->end()) { - if (dynamic_cast<DocumentSourceSequentialDocumentCache*>(itr->get())) { - auto sequentialCache = dynamic_cast<DocumentSourceSequentialDocumentCache*>(itr->get()); - if (!sequentialCache->hasOptimizedPos()) { - sequentialCache->optimizeAt(itr, &container); - } - } - itr = std::next(itr); - } -} - void DocumentSourceLookUp::addCacheStageAndOptimize(Pipeline& pipeline) { - // Adds the cache to the end of the pipeline and calls optimizeContainer which will ensure the - // stages of the pipeline are in the correct and optimal order, before the cache runs - // doOptimizeAt. During the optimization process, the cache will either move itself to the - // correct position in the pipeline, or abandon itself if no suitable cache position exists. - // Once the cache is finished optimizing, the entire pipeline is optimized. - // - // When pipeline optimization is disabled, 'Pipeline::optimizePipeline()' exits early and so the - // cache would not be placed correctly. So we only add the cache when pipeline optimization is - // enabled. + // Add the cache stage at the end and optimize. During the optimization process, the cache will + // either move itself to the correct position in the pipeline, or will abandon itself if no + // suitable cache position exists. Do it only if pipeline optimization is enabled, otherwise + // Pipeline::optimizePipeline() will exit early and correct placement of the cache will not + // occur. if (auto fp = globalFailPointRegistry().find("disablePipelineOptimization"); fp && fp->shouldFail()) { _cache->abandon(); } else { - // The cache needs to see the full pipeline in its correct order in order to properly place - // itself, therefore we are adding it to the end of the pipeline, and calling - // optimizeContainer on the pipeline to ensure the rest of the pipeline is in its correct - // order before optimizing the cache. pipeline.addFinalSource( DocumentSourceSequentialDocumentCache::create(_fromExpCtx, _cache.get_ptr())); - - auto& container = pipeline.getSources(); - - Pipeline::optimizeContainer(&container); - - // We want to ensure the cache has been optimized prior to any calls to optimize(). - findAndOptimizeSequentialDocumentCache(pipeline); - - // Optimize the pipeline, with the cache in its correct position if it exists. - Pipeline::optimizeEachStage(&container); } + + pipeline.optimizePipeline(); } DocumentSource::GetModPathsReturn DocumentSourceLookUp::getModifiedPaths() const { - OrderedPathSet modifiedPaths{_as.fullPath()}; + std::set<std::string> modifiedPaths{_as.fullPath()}; if (_unwindSrc) { auto pathsModifiedByUnwind = _unwindSrc->getModifiedPaths(); invariant(pathsModifiedByUnwind.type == GetModPathsReturn::Type::kFiniteSet); @@ -1079,68 +1019,44 @@ void DocumentSourceLookUp::appendSpecificExecStats(MutableDocument& doc) const { doc["indexesUsed"] = Value{std::move(indexesUsedVec)}; } -void DocumentSourceLookUp::serializeToArray(std::vector<Value>& array, - const SerializationOptions& opts) const { +void DocumentSourceLookUp::serializeToArray( + std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { + // Support alternative $lookup from config.cache.chunks* namespaces. auto fromValue = (pExpCtx->ns.db() == _fromNs.db()) - ? Value(opts.serializeIdentifier(_fromNs.coll())) - : Value(Document{{"db", opts.serializeIdentifier(_fromNs.db())}, - {"coll", opts.serializeIdentifier(_fromNs.coll())}}); + ? Value(_fromNs.coll()) + : Value(Document{{"db", _fromNs.db()}, {"coll", _fromNs.coll()}}); - MutableDocument output(Document{ - {getSourceName(), Document{{"from", fromValue}, {"as", opts.serializeFieldPath(_as)}}}}); + MutableDocument output( + Document{{getSourceName(), Document{{"from", fromValue}, {"as", _as.fullPath()}}}}); if (hasLocalFieldForeignFieldJoin()) { - output[getSourceName()]["localField"] = Value(opts.serializeFieldPath(_localField.value())); - output[getSourceName()]["foreignField"] = - Value(opts.serializeFieldPath(_foreignField.value())); + output[getSourceName()]["localField"] = Value(_localField->fullPath()); + output[getSourceName()]["foreignField"] = Value(_foreignField->fullPath()); } // Add a pipeline field if only-pipeline syntax was used (to ensure the output is valid $lookup // syntax) or if a $match was absorbed. - auto serializedPipeline = [&]() -> std::vector<BSONObj> { - if (!_userPipeline) { - return std::vector<BSONObj>{}; - } - if (opts.transformIdentifiers || - opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) { - return Pipeline::parse(*_userPipeline, _fromExpCtx)->serializeToBson(opts); - } - if (opts.serializeForQueryAnalysis) { - // If we are in query analysis, encrypted fields will have been marked in the - // introspection pipeline, so we need to serialize that here. - return _resolvedIntrospectionPipeline->serializeToBson(opts); - } - return *_userPipeline; - }(); + auto pipeline = _userPipeline.get_value_or(std::vector<BSONObj>()); if (_additionalFilter) { - auto serializedFilter = [&]() -> BSONObj { - if (opts.transformIdentifiers || - opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) { - auto filter = - uassertStatusOK(MatchExpressionParser::parse(*_additionalFilter, pExpCtx)); - return filter->serialize(opts); - } - return *_additionalFilter; - }(); - serializedPipeline.emplace_back(BSON("$match" << serializedFilter)); + pipeline.emplace_back(BSON("$match" << *_additionalFilter)); } - if (!hasLocalFieldForeignFieldJoin() || serializedPipeline.size() > 0) { + if (!hasLocalFieldForeignFieldJoin() || pipeline.size() > 0) { MutableDocument exprList; for (auto letVar : _letVariables) { - exprList.addField(opts.serializeFieldPathFromString(letVar.name), - letVar.expression->serialize(opts)); + exprList.addField(letVar.name, + letVar.expression->serialize(static_cast<bool>(explain))); } output[getSourceName()]["let"] = Value(exprList.freeze()); - output[getSourceName()]["pipeline"] = Value(serializedPipeline); + output[getSourceName()]["pipeline"] = Value(pipeline); } if (_hasExplicitCollation) { output[getSourceName()]["_internalCollation"] = Value(_fromExpCtx->getCollatorBSON()); } - if (opts.verbosity) { + if (explain) { if (_unwindSrc) { const boost::optional<FieldPath> indexPath = _unwindSrc->indexPath(); output[getSourceName()]["unwinding"] = @@ -1148,11 +1064,8 @@ void DocumentSourceLookUp::serializeToArray(std::vector<Value>& array, << _unwindSrc->preserveNullAndEmptyArrays() << "includeArrayIndex" << (indexPath ? Value(indexPath->fullPath()) : Value()))); } - // Conflict is .get() vs .value(), changed in 6.1 during - // https://jira.mongodb.org/browse/SERVER-68246. Going to stick with 7.0's version of - // '.value()', I think there is no meaningful difference here since line 1080 check that the - // optional is set. - if (opts.verbosity.value() >= ExplainOptions::Verbosity::kExecStats) { + + if (explain.get() >= ExplainOptions::Verbosity::kExecStats) { appendSpecificExecStats(output); } @@ -1255,21 +1168,6 @@ void DocumentSourceLookUp::reattachToOperationContext(OperationContext* opCtx) { } } -bool DocumentSourceLookUp::validateOperationContext(const OperationContext* opCtx) const { - if (getContext()->opCtx != opCtx || (_fromExpCtx && _fromExpCtx->opCtx != opCtx)) { - return false; - } - - if (_pipeline) { - const auto& sources = _pipeline->getSources(); - return std::all_of(sources.begin(), sources.end(), [opCtx](const auto& s) { - return s->validateOperationContext(opCtx); - }); - } - - return true; -} - boost::intrusive_ptr<DocumentSource> DocumentSourceLookUp::createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx) { uassert(ErrorCodes::FailedToParse, diff --git a/src/mongo/db/pipeline/document_source_lookup.h b/src/mongo/db/pipeline/document_source_lookup.h index 504b4b1d83f..fef7cd0067f 100644 --- a/src/mongo/db/pipeline/document_source_lookup.h +++ b/src/mongo/db/pipeline/document_source_lookup.h @@ -127,7 +127,7 @@ public: const char* getSourceName() const final; void serializeToArray( std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const final override; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; /** * Returns the 'as' path, and possibly fields modified by an absorbed $unwind. @@ -150,8 +150,6 @@ public: void reattachToOperationContext(OperationContext* opCtx) final; - bool validateOperationContext(const OperationContext* opCtx) const final; - bool usedDisk() final; const SpecificStats* getSpecificStats() const final { @@ -305,9 +303,8 @@ private: /** * Should not be called; use serializeToArray instead. */ - Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { - MONGO_UNREACHABLE_TASSERT(7484304); + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final { + MONGO_UNREACHABLE; } GetNextResult unwindResult(); diff --git a/src/mongo/db/pipeline/document_source_lookup_test.cpp b/src/mongo/db/pipeline/document_source_lookup_test.cpp index 4d2149f6a40..a565fb60c26 100644 --- a/src/mongo/db/pipeline/document_source_lookup_test.cpp +++ b/src/mongo/db/pipeline/document_source_lookup_test.cpp @@ -61,8 +61,7 @@ using std::vector; using DocumentSourceLookUpTest = AggregationContextFixture; const long long kDefaultMaxCacheSize = internalDocumentSourceLookupCacheSizeBytes.load(); -const auto kExplain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; +const auto kExplain = ExplainOptions::Verbosity::kQueryPlanner; // For tests which need to run in a replica set context. class ReplDocumentSourceLookUpTest : public DocumentSourceLookUpTest { @@ -657,51 +656,6 @@ TEST_F(DocumentSourceLookUpTest, LookupReParseSerializedStageWithCollation) { ASSERT_VALUE_EQ(newSerialization[0], serialization[0]); } -// Tests that $lookup with '$documents' can be round tripped. -TEST_F(DocumentSourceLookUpTest, LookupReParseSerializedStageWithDocumentsPipelineStage) { - auto expCtx = getExpCtx(); - NamespaceString fromNs = NamespaceString("unittest", "$cmd.aggregate"); - expCtx->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {fromNs.coll().toString(), {fromNs, std::vector<BSONObj>()}}}); - auto originalBSON = - BSON("$lookup" << BSON("localField" - << "y" - << "foreignField" - << "x" - << "pipeline" - << BSON_ARRAY(BSON("$documents" - << BSON_ARRAY(BSON("x" << 5) << BSON("y" << 15)))) - << "as" - << "as")); - auto lookupStage = DocumentSourceLookUp::createFromBson(originalBSON.firstElement(), expCtx); - - // - // Serialize the $lookup stage and confirm contents. - // - vector<Value> serialization; - auto opts = SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}; - lookupStage->serializeToArray(serialization, opts); - auto serializedDoc = serialization[0].getDocument(); - ASSERT_EQ(serializedDoc["$lookup"].getType(), BSONType::Object); - - // Ensure the $documents desugared to $queue properly. - auto serializedStage = serializedDoc["$lookup"].getDocument(); - ASSERT_EQ(serializedStage["pipeline"].getType(), BSONType::Array); - ASSERT_EQ(serializedStage["pipeline"].getArrayLength(), 4UL); - - ASSERT_EQ(serializedStage["pipeline"][0].getType(), BSONType::Object); - ASSERT_EQ(serializedStage["pipeline"][0]["$queue"].getType(), BSONType::Array); - - auto roundTripped = - DocumentSourceLookUp::createFromBson(serializedDoc.toBson().firstElement(), expCtx); - - vector<Value> newSerialization; - roundTripped->serializeToArray(newSerialization, opts); - - ASSERT_EQ(newSerialization.size(), 1UL); - ASSERT_VALUE_EQ(newSerialization[0], serialization[0]); -} - // $lookup : {from : {db: <>, coll: <>}} syntax doesn't work for a namespace that isn't // config.cache.chunks*. @@ -1152,10 +1106,13 @@ TEST_F(DocumentSourceLookUpTest, ExprEmbeddedInMatchExpressionShouldBeOptimized) auto& matchSource = dynamic_cast<const DocumentSourceMatch&>(*secondSource); // Ensure that the '$$var' in the embedded expression got optimized to ExpressionConstant. + BSONObjBuilder builder; + matchSource.getMatchExpression()->serialize(&builder); + auto serializedMatch = builder.obj(); auto expectedMatch = fromjson("{$and: [{_id: {$_internalExprEq: 5}}, {$expr: {$eq: ['$_id', {$const: 5}]}}]}"); - ASSERT_VALUE_EQ(Value(matchSource.getMatchExpression()->serialize()), Value(expectedMatch)); + ASSERT_VALUE_EQ(Value(serializedMatch), Value(expectedMatch)); } TEST_F(DocumentSourceLookUpTest, @@ -1469,57 +1426,5 @@ TEST_F(DocumentSourceLookUpTest, ShouldNotCacheIfCorrelatedStageIsAbsorbedIntoPl ASSERT_VALUE_EQ(Value(subPipeline->writeExplainOps(kExplain)), Value(BSONArray(expectedPipe))); } -TEST_F(DocumentSourceLookUpTest, RedactsCorrectlyWithPipeline) { - auto expCtx = getExpCtx(); - NamespaceString fromNs("test", "coll"); - expCtx->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {fromNs.coll().toString(), {fromNs, std::vector<BSONObj>()}}}); - - BSONArrayBuilder pipeline; - pipeline << BSON("$match" << BSON("a" - << "myStr")); - pipeline << BSON("$project" << BSON("_id" << 0 << "a" << 1)); - auto docSource = DocumentSourceLookUp::createFromBson( - BSON("$lookup" << BSON("from" << fromNs.coll() << "localField" - << "foo" - << "foreignField" - << "bar" - << "let" - << BSON("var1" - << "$x") - << "pipeline" << pipeline.arr() << "as" - << "out")) - .firstElement(), - expCtx); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$lookup": { - "from": "HASH<coll>", - "as": "HASH<out>", - "localField": "HASH<foo>", - "foreignField": "HASH<bar>", - "let": { - "HASH<var1>": "$HASH<x>" - }, - "pipeline": [ - { - "$match": { - "HASH<a>": { - "$eq": "?string" - } - } - }, - { - "$project": { - "HASH<a>": true, - "HASH<_id>": false - } - } - ] - } - })", - redact(*docSource)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index 36b39f3820c..10346eb6a46 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -67,10 +67,11 @@ const char* DocumentSourceMatch::getSourceName() const { return kStageName.rawData(); } -Value DocumentSourceMatch::serialize(const SerializationOptions& opts) const { - if (opts.verbosity || opts.transformIdentifiers || - opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) { - return Value(DOC(getSourceName() << Document(_expression->serialize(opts)))); +Value DocumentSourceMatch::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { + BSONObjBuilder builder; + _expression->serialize(&builder); + return Value(DOC(getSourceName() << Document(builder.obj()))); } return Value(DOC(getSourceName() << Document(getQuery()))); } @@ -92,12 +93,11 @@ DocumentSource::GetNextResult DocumentSourceMatch::doGetNext() { auto nextInput = pSource->getNext(); for (; nextInput.isAdvanced(); nextInput = pSource->getNext()) { // MatchExpression only takes BSON documents, so we have to make one. As an optimization, - // only serialize the fields we need to do the match. Specify BSONObj::LargeSizeTrait so - // that matching against a large document mid-pipeline does not throw a BSON max-size error. + // only serialize the fields we need to do the match. BSONObj toMatch = _dependencies.needWholeDocument - ? nextInput.getDocument().toBson<BSONObj::LargeSizeTrait>() - : document_path_support::documentToBsonWithPaths<BSONObj::LargeSizeTrait>( - nextInput.getDocument(), _dependencies.fields); + ? nextInput.getDocument().toBson() + : document_path_support::documentToBsonWithPaths(nextInput.getDocument(), + _dependencies.fields); if (_expression->matchesBSON(toMatch)) { return nextInput; @@ -410,13 +410,13 @@ void DocumentSourceMatch::joinMatchWith(intrusive_ptr<DocumentSourceMatch> other } pair<intrusive_ptr<DocumentSourceMatch>, intrusive_ptr<DocumentSourceMatch>> -DocumentSourceMatch::splitSourceBy(const OrderedPathSet& fields, +DocumentSourceMatch::splitSourceBy(const std::set<std::string>& fields, const StringMap<std::string>& renames) && { return std::move(*this).splitSourceByFunc(fields, renames, expression::isIndependentOf); } pair<intrusive_ptr<DocumentSourceMatch>, intrusive_ptr<DocumentSourceMatch>> -DocumentSourceMatch::splitSourceByFunc(const OrderedPathSet& fields, +DocumentSourceMatch::splitSourceByFunc(const std::set<std::string>& fields, const StringMap<std::string>& renames, expression::ShouldSplitExprFunc func) && { pair<unique_ptr<MatchExpression>, unique_ptr<MatchExpression>> newExpr( @@ -448,11 +448,15 @@ DocumentSourceMatch::splitSourceByFunc(const OrderedPathSet& fields, // the corresponding BSONObj may not exist. Therefore, we take each of these expressions, // serialize them, and then re-parse them, constructing new BSON that is owned by the // DocumentSourceMatch. - auto firstMatch = DocumentSourceMatch::create(newExpr.first->serialize(), pExpCtx); + BSONObjBuilder firstBob; + newExpr.first->serialize(&firstBob); + auto firstMatch = DocumentSourceMatch::create(firstBob.obj(), pExpCtx); intrusive_ptr<DocumentSourceMatch> secondMatch; if (newExpr.second) { - secondMatch = DocumentSourceMatch::create(newExpr.second->serialize(), pExpCtx); + BSONObjBuilder secondBob; + newExpr.second->serialize(&secondBob); + secondMatch = DocumentSourceMatch::create(secondBob.obj(), pExpCtx); } return {std::move(firstMatch), std::move(secondMatch)}; @@ -485,7 +489,9 @@ boost::intrusive_ptr<DocumentSourceMatch> DocumentSourceMatch::descendMatchOnPat } }); - return new DocumentSourceMatch(matchExpr->serialize(), expCtx); + BSONObjBuilder query; + matchExpr->serialize(&query); + return new DocumentSourceMatch(query.obj(), expCtx); } std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>> @@ -493,7 +499,7 @@ DocumentSourceMatch::splitMatchByModifiedFields( const boost::intrusive_ptr<DocumentSourceMatch>& match, const DocumentSource::GetModPathsReturn& modifiedPathsRet) { // Attempt to move some or all of this $match before this stage. - OrderedPathSet modifiedPaths; + std::set<std::string> modifiedPaths; switch (modifiedPathsRet.type) { case DocumentSource::GetModPathsReturn::Type::kNotSupported: // We don't know what paths this stage might modify, so refrain from swapping. diff --git a/src/mongo/db/pipeline/document_source_match.h b/src/mongo/db/pipeline/document_source_match.h index f8cf34e23f2..5bf27ddfe5c 100644 --- a/src/mongo/db/pipeline/document_source_match.h +++ b/src/mongo/db/pipeline/document_source_match.h @@ -44,7 +44,7 @@ namespace mongo { class DocumentSourceMatch : public DocumentSource { public: virtual boost::intrusive_ptr<DocumentSource> clone( - const boost::intrusive_ptr<ExpressionContext>& newExpCtx) const { + const boost::intrusive_ptr<ExpressionContext>& newExpCtx = nullptr) const { // Raw new is needed to access non-public constructors. return new DocumentSourceMatch(*this, newExpCtx); } @@ -126,7 +126,8 @@ public: ChangeStreamRequirement::kAllowlist}; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const override; + Value serialize( + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override; /** * Attempts to combine with any subsequent $match stages, joining the query objects with a @@ -139,7 +140,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // This stage does not modify or rename any paths. - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } /** @@ -198,7 +199,7 @@ public: * z: "baz"}} and {$match: {a: "foo"}}. */ std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>> - splitSourceBy(const OrderedPathSet& fields, const StringMap<std::string>& renames) &&; + splitSourceBy(const std::set<std::string>& fields, const StringMap<std::string>& renames) &&; boost::optional<DistributedPlanLogic> distributedPlanLogic() final { return boost::none; @@ -219,7 +220,7 @@ protected: private: std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>> - splitSourceByFunc(const OrderedPathSet& fields, + splitSourceByFunc(const std::set<std::string>& fields, const StringMap<std::string>& renames, expression::ShouldSplitExprFunc func) &&; diff --git a/src/mongo/db/pipeline/document_source_match_test.cpp b/src/mongo/db/pipeline/document_source_match_test.cpp index 04e5f895469..f5b53a393e8 100644 --- a/src/mongo/db/pipeline/document_source_match_test.cpp +++ b/src/mongo/db/pipeline/document_source_match_test.cpp @@ -54,6 +54,7 @@ using std::string; // This provides access to getExpCtx(), but we'll use a different name for this test suite. using DocumentSourceMatchTest = AggregationContextFixture; +constexpr auto kExplain = ExplainOptions::Verbosity::kQueryPlanner; TEST_F(DocumentSourceMatchTest, RedactSafePortion) { auto expCtx = getExpCtx(); @@ -603,6 +604,8 @@ DEATH_TEST_REGEX_F(DocumentSourceMatchTest, const auto matchSpec = BSON("a" << BSON("$elemMatch" << BSON("a.b" << 1))); const auto matchExpression = unittest::assertGet(MatchExpressionParser::parse(matchSpec, expCtx)); + BSONObjBuilder out; + matchExpression->serialize(&out); DocumentSourceMatch::descendMatchOnPath(matchExpression.get(), "a", expCtx); } @@ -695,71 +698,9 @@ TEST_F(DocumentSourceMatchTest, ShouldShowOptimizationsInExplainOutputWhenOptimi auto expectedMatch = fromjson("{$match: {a:{$eq: 1}}}"); - ASSERT_VALUE_EQ(Value((static_cast<DocumentSourceMatch*>(optimizedMatch.get())) - ->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})), - Value(expectedMatch)); -} - -TEST_F(DocumentSourceMatchTest, RedactionWithAnd) { - auto spec = fromjson(R"({ - $match: { - $and: [ - { - "a.c": "abc" - }, - { - "b": { - $gt: 10 - } - } - ] - }})"); - auto docSource = DocumentSourceMatch::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$match": { - "$and": [ - { - "HASH<a>.HASH<c>": { - "$eq": "?string" - } - }, - { - "HASH<b>": { - "$gt": "?number" - } - } - ] - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceMatchTest, RedactionWithExprPipeline) { - auto spec = fromjson(R"({ - $match: { - $expr: { - $eq: [ - '$foo', - '$bar' - ] - } - } - })"); - auto docSource = DocumentSourceMatch::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$match": { - "$expr": { - "$eq": [ - "$HASH<foo>", - "$HASH<bar>" - ] - } - } - })", - redact(*docSource)); + ASSERT_VALUE_EQ( + Value((static_cast<DocumentSourceMatch*>(optimizedMatch.get()))->serialize(kExplain)), + Value(expectedMatch)); } } // namespace diff --git a/src/mongo/db/pipeline/document_source_merge.cpp b/src/mongo/db/pipeline/document_source_merge.cpp index c46cfa8c641..96a1dd55547 100644 --- a/src/mongo/db/pipeline/document_source_merge.cpp +++ b/src/mongo/db/pipeline/document_source_merge.cpp @@ -57,11 +57,10 @@ namespace { using MergeStrategyDescriptor = DocumentSourceMerge::MergeStrategyDescriptor; using MergeMode = MergeStrategyDescriptor::MergeMode; using MergeStrategy = MergeStrategyDescriptor::MergeStrategy; -using BatchedCommandGenerator = MergeStrategyDescriptor::BatchedCommandGenerator; using MergeStrategyDescriptorsMap = std::map<const MergeMode, const MergeStrategyDescriptor>; using WhenMatched = MergeStrategyDescriptor::WhenMatched; using WhenNotMatched = MergeStrategyDescriptor::WhenNotMatched; -using BatchTransform = DocumentSourceMerge::BatchTransform; +using BatchTransform = std::function<void(DocumentSourceMerge::BatchedObjects&)>; using UpdateModification = write_ops::UpdateModification; using UpsertType = MongoProcessInterface::UpsertType; @@ -84,71 +83,21 @@ constexpr auto kPipelineDiscardMode = MergeMode{WhenMatched::kPipeline, WhenNotM const auto kDefaultPipelineLet = BSON("new" << "$$ROOT"); -BatchedCommandGenerator makeInsertCommandGenerator() { - return [](const auto& expCtx, const auto& ns) -> BatchedCommandRequest { - return DocumentSourceMerge::DocumentSourceWriter::makeInsertCommand( - ns, expCtx->bypassDocumentValidation); - }; -} - -BatchedCommandGenerator makeUpdateCommandGenerator() { - return [](const auto& expCtx, const auto& ns) -> BatchedCommandRequest { - write_ops::UpdateCommandRequest updateOp(ns); - updateOp.setWriteCommandRequestBase([&] { - write_ops::WriteCommandRequestBase wcb; - wcb.setOrdered(false); - wcb.setBypassDocumentValidation(expCtx->bypassDocumentValidation); - return wcb; - }()); - auto [constants, letParams] = - expCtx->variablesParseState.transitionalCompatibilitySerialize(expCtx->variables); - updateOp.setLegacyRuntimeConstants(std::move(constants)); - if (!letParams.isEmpty()) { - updateOp.setLet(std::move(letParams)); - } - return BatchedCommandRequest(std::move(updateOp)); - }; -} - /** - * Converts 'batch' into a vector of UpdateOpEntries. + * Creates a merge strategy which uses update semantics to perform a merge operation. If + * 'BatchTransform' function is provided, it will be called to transform batched objects before + * passing them to the 'update'. */ -std::vector<write_ops::UpdateOpEntry> constructUpdateEntries( - DocumentSourceMerge::DocumentSourceWriter::BatchedObjects&& batch, - UpsertType upsert, - bool multi) { - std::vector<write_ops::UpdateOpEntry> updateEntries; - for (auto&& obj : batch) { - write_ops::UpdateOpEntry entry; - auto&& [q, u, c] = obj; - entry.setQ(std::move(q)); - entry.setU(std::move(u)); - entry.setC(std::move(c)); - entry.setUpsert(upsert != UpsertType::kNone); - entry.setUpsertSupplied({{entry.getUpsert(), upsert == UpsertType::kInsertSuppliedDoc}}); - entry.setMulti(multi); - - updateEntries.push_back(std::move(entry)); - } - return updateEntries; -} +MergeStrategy makeUpdateStrategy(UpsertType upsert, BatchTransform transform) { + return [upsert, transform]( + const auto& expCtx, const auto& ns, const auto& wc, auto epoch, auto&& batch) { + if (transform) { + transform(batch); + } -/** - * Creates a merge strategy which uses update semantics to perform a merge operation. - */ -MergeStrategy makeUpdateStrategy() { - return [](const auto& expCtx, - const auto& ns, - const auto& wc, - auto epoch, - auto&& batch, - auto&& bcr, - UpsertType upsert) { constexpr auto multi = false; - auto updateCommand = bcr.extractUpdateRequest(); - updateCommand->setUpdates(constructUpdateEntries(std::move(batch), upsert, multi)); uassertStatusOK(expCtx->mongoProcessInterface->update( - expCtx, ns, std::move(updateCommand), wc, upsert, multi, epoch)); + expCtx, ns, std::move(batch), wc, upsert, multi, epoch)); }; } @@ -157,22 +106,20 @@ MergeStrategy makeUpdateStrategy() { * that each document in the batch has a matching document in the 'ns' collection (note that a * matching document may not be modified as a result of an update operation, yet it still will be * counted as matching). If at least one document doesn't have a match, this strategy returns an - * error. + * error. If 'BatchTransform' function is provided, it will be called to transform batched objects + * before passing them to the 'update'. */ -MergeStrategy makeStrictUpdateStrategy() { - return [](const auto& expCtx, - const auto& ns, - const auto& wc, - auto epoch, - auto&& batch, - auto&& bcr, - UpsertType upsert) { +MergeStrategy makeStrictUpdateStrategy(UpsertType upsert, BatchTransform transform) { + return [upsert, transform]( + const auto& expCtx, const auto& ns, const auto& wc, auto epoch, auto&& batch) { + if (transform) { + transform(batch); + } + const int64_t batchSize = batch.size(); constexpr auto multi = false; - auto updateCommand = bcr.extractUpdateRequest(); - updateCommand->setUpdates(constructUpdateEntries(std::move(batch), upsert, multi)); auto updateResult = uassertStatusOK(expCtx->mongoProcessInterface->update( - expCtx, ns, std::move(updateCommand), wc, upsert, multi, epoch)); + expCtx, ns, std::move(batch), wc, upsert, multi, epoch)); uassert(ErrorCodes::MergeStageNoMatchingDocument, "{} could not find a matching document in the target collection " "for at least one document in the source collection"_format(kStageName), @@ -184,34 +131,28 @@ MergeStrategy makeStrictUpdateStrategy() { * Creates a merge strategy which uses insert semantics to perform a merge operation. */ MergeStrategy makeInsertStrategy() { - return [](const auto& expCtx, - const auto& ns, - const auto& wc, - auto epoch, - auto&& batch, - auto&& bcr, - UpsertType upsertType) { + return [](const auto& expCtx, const auto& ns, const auto& wc, auto epoch, auto&& batch) { std::vector<BSONObj> objectsToInsert(batch.size()); // The batch stores replacement style updates, but for this "insert" style of $merge we'd // like to just insert the new document without attempting any sort of replacement. std::transform(batch.begin(), batch.end(), objectsToInsert.begin(), [](const auto& obj) { return std::get<UpdateModification>(obj).getUpdateReplacement(); }); - auto insertCommand = bcr.extractInsertRequest(); - insertCommand->setDocuments(std::move(objectsToInsert)); - uassertStatusOK( - expCtx->mongoProcessInterface->insert(expCtx, ns, std::move(insertCommand), wc, epoch)); + uassertStatusOK(expCtx->mongoProcessInterface->insert( + expCtx, ns, std::move(objectsToInsert), wc, epoch)); }; } /** - * Creates a batched object transformation function which wraps 'obj' into the given 'updateOp' - * operator. + * Creates a batched objects transformation function which wraps each element of the + * 'batch.modifications' array into the given 'updateOp' operator. */ BatchTransform makeUpdateTransform(const std::string& updateOp) { - return [updateOp](auto& obj) { - std::get<UpdateModification>(obj) = UpdateModification::parseFromClassicUpdate( - BSON(updateOp << std::get<UpdateModification>(obj).getUpdateReplacement())); + return [updateOp](auto& batch) { + for (auto&& obj : batch) { + std::get<UpdateModification>(obj) = UpdateModification::parseFromClassicUpdate( + BSON(updateOp << std::get<UpdateModification>(obj).getUpdateReplacement())); + } }; } @@ -230,95 +171,53 @@ const MergeStrategyDescriptorsMap& getDescriptors() { // be initialized first. By wrapping the map into a function we can guarantee that it won't be // initialized until the first use, which is when the program already started and all global // variables had been initialized. - static const auto mergeStrategyDescriptors = - MergeStrategyDescriptorsMap{// whenMatched: replace, whenNotMatched: insert - {kReplaceInsertMode, - {kReplaceInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(), - {}, - UpsertType::kGenerateNewDoc, - makeUpdateCommandGenerator()}}, - // whenMatched: replace, whenNotMatched: fail - {kReplaceFailMode, - {kReplaceFailMode, - {ActionType::update}, - makeStrictUpdateStrategy(), - {}, - UpsertType::kNone, - makeUpdateCommandGenerator()}}, - // whenMatched: replace, whenNotMatched: discard - {kReplaceDiscardMode, - {kReplaceDiscardMode, - {ActionType::update}, - makeUpdateStrategy(), - {}, - UpsertType::kNone, - makeUpdateCommandGenerator()}}, - // whenMatched: merge, whenNotMatched: insert - {kMergeInsertMode, - {kMergeInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(), - makeUpdateTransform("$set"), - UpsertType::kGenerateNewDoc, - makeUpdateCommandGenerator()}}, - // whenMatched: merge, whenNotMatched: fail - {kMergeFailMode, - {kMergeFailMode, - {ActionType::update}, - makeStrictUpdateStrategy(), - makeUpdateTransform("$set"), - UpsertType::kNone, - makeUpdateCommandGenerator()}}, - // whenMatched: merge, whenNotMatched: discard - {kMergeDiscardMode, - {kMergeDiscardMode, - {ActionType::update}, - makeUpdateStrategy(), - makeUpdateTransform("$set"), - UpsertType::kNone, - makeUpdateCommandGenerator()}}, - // whenMatched: keepExisting, whenNotMatched: insert - {kKeepExistingInsertMode, - {kKeepExistingInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(), - makeUpdateTransform("$setOnInsert"), - UpsertType::kGenerateNewDoc, - makeUpdateCommandGenerator()}}, - // whenMatched: [pipeline], whenNotMatched: insert - {kPipelineInsertMode, - {kPipelineInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(), - {}, - UpsertType::kInsertSuppliedDoc, - makeUpdateCommandGenerator()}}, - // whenMatched: [pipeline], whenNotMatched: fail - {kPipelineFailMode, - {kPipelineFailMode, - {ActionType::update}, - makeStrictUpdateStrategy(), - {}, - UpsertType::kNone, - makeUpdateCommandGenerator()}}, - // whenMatched: [pipeline], whenNotMatched: discard - {kPipelineDiscardMode, - {kPipelineDiscardMode, - {ActionType::update}, - makeUpdateStrategy(), - {}, - UpsertType::kNone, - makeUpdateCommandGenerator()}}, - // whenMatched: fail, whenNotMatched: insert - {kFailInsertMode, - {kFailInsertMode, - {ActionType::insert}, - makeInsertStrategy(), - {}, - UpsertType::kNone, - makeInsertCommandGenerator()}}}; + static const auto mergeStrategyDescriptors = MergeStrategyDescriptorsMap{ + // whenMatched: replace, whenNotMatched: insert + {kReplaceInsertMode, + {kReplaceInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(UpsertType::kGenerateNewDoc, {})}}, + // whenMatched: replace, whenNotMatched: fail + {kReplaceFailMode, + {kReplaceFailMode, {ActionType::update}, makeStrictUpdateStrategy(UpsertType::kNone, {})}}, + // whenMatched: replace, whenNotMatched: discard + {kReplaceDiscardMode, + {kReplaceDiscardMode, {ActionType::update}, makeUpdateStrategy(UpsertType::kNone, {})}}, + // whenMatched: merge, whenNotMatched: insert + {kMergeInsertMode, + {kMergeInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(UpsertType::kGenerateNewDoc, makeUpdateTransform("$set"))}}, + // whenMatched: merge, whenNotMatched: fail + {kMergeFailMode, + {kMergeFailMode, + {ActionType::update}, + makeStrictUpdateStrategy(UpsertType::kNone, makeUpdateTransform("$set"))}}, + // whenMatched: merge, whenNotMatched: discard + {kMergeDiscardMode, + {kMergeDiscardMode, + {ActionType::update}, + makeUpdateStrategy(UpsertType::kNone, makeUpdateTransform("$set"))}}, + // whenMatched: keepExisting, whenNotMatched: insert + {kKeepExistingInsertMode, + {kKeepExistingInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(UpsertType::kGenerateNewDoc, makeUpdateTransform("$setOnInsert"))}}, + // whenMatched: [pipeline], whenNotMatched: insert + {kPipelineInsertMode, + {kPipelineInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(UpsertType::kInsertSuppliedDoc, {})}}, + // whenMatched: [pipeline], whenNotMatched: fail + {kPipelineFailMode, + {kPipelineFailMode, + {ActionType::update}, + makeStrictUpdateStrategy(UpsertType::kNone, {})}}, + // whenMatched: [pipeline], whenNotMatched: discard + {kPipelineDiscardMode, + {kPipelineDiscardMode, {ActionType::update}, makeUpdateStrategy(UpsertType::kNone, {})}}, + // whenMatched: fail, whenNotMatched: insert + {kFailInsertMode, {kFailInsertMode, {ActionType::insert}, makeInsertStrategy()}}}; return mergeStrategyDescriptors; } @@ -605,7 +504,7 @@ boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceMerge::distr return DocumentSourceWriter::distributedPlanLogic(); } -Value DocumentSourceMerge::serialize(const SerializationOptions& opts) const { +Value DocumentSourceMerge::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { DocumentSourceMergeSpec spec; spec.setTargetNss(_outputNs); spec.setLet([&]() -> boost::optional<BSONObj> { @@ -615,27 +514,11 @@ Value DocumentSourceMerge::serialize(const SerializationOptions& opts) const { BSONObjBuilder bob; for (auto&& [name, expr] : *_letVariables) { - bob << opts.serializeFieldPathFromString(name) << expr->serialize(opts); + bob << name << expr->serialize(static_cast<bool>(explain)); } return bob.obj(); }()); - spec.setWhenMatched(MergeWhenMatchedPolicy{ - _descriptor.mode.first, [&]() -> boost::optional<std::vector<BSONObj>> { - if (!_pipeline.has_value()) { - return boost::none; - } - auto expCtxWithLetVariables = pExpCtx->copyWith(getOutputNs()); - if (spec.getLet()) { - BSONObjBuilder cleanLetSpecBuilder; - for (auto&& [name, expr] : *_letVariables) { - cleanLetSpecBuilder.append(name, BSONObj{}); - } - expCtxWithLetVariables->variables.seedVariablesWithLetParameters( - expCtxWithLetVariables.get(), cleanLetSpecBuilder.obj()); - } - return Pipeline::parse(_pipeline.value(), expCtxWithLetVariables) - ->serializeToBson(opts); - }()}); + spec.setWhenMatched(MergeWhenMatchedPolicy{_descriptor.mode.first, _pipeline}); spec.setWhenNotMatched(_descriptor.mode.second); spec.setOn([&]() { std::vector<std::string> mergeOnFields; @@ -645,7 +528,7 @@ Value DocumentSourceMerge::serialize(const SerializationOptions& opts) const { return mergeOnFields; }()); spec.setTargetCollectionVersion(_targetCollectionVersion); - return Value(Document{{getSourceName(), spec.toBSON(opts)}}); + return Value(Document{{getSourceName(), spec.toBSON()}}); } std::pair<DocumentSourceMerge::BatchObject, int> DocumentSourceMerge::makeBatchObject( @@ -660,29 +543,17 @@ std::pair<DocumentSourceMerge::BatchObject, int> DocumentSourceMerge::makeBatchO auto mergeOnFields = extractMergeOnFieldsFromDoc(doc, _mergeOnFields); auto mod = makeBatchUpdateModification(doc); auto vars = resolveLetVariablesIfNeeded(doc); - BatchObject batchObject{std::move(mergeOnFields), std::move(mod), std::move(vars)}; - if (_descriptor.transform) { - _descriptor.transform(batchObject); - } - - tassert(6628901, "_writeSizeEstimator should be initialized", _writeSizeEstimator); - return {batchObject, - _writeSizeEstimator->estimateUpdateSizeBytes(batchObject, _descriptor.upsertType)}; + auto modSize = mod.objsize() + (vars ? vars->objsize() : 0); + return {{std::move(mergeOnFields), std::move(mod), std::move(vars)}, modSize}; } -void DocumentSourceMerge::spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) try { +void DocumentSourceMerge::spill(BatchedObjects&& batch) try { DocumentSourceWriteBlock writeBlock(pExpCtx->opCtx); auto targetEpoch = _targetCollectionVersion ? boost::optional<OID>(_targetCollectionVersion->epoch()) : boost::none; - _descriptor.strategy(pExpCtx, - _outputNs, - _writeConcern, - targetEpoch, - std::move(batch), - std::move(bcr), - _descriptor.upsertType); + _descriptor.strategy(pExpCtx, _outputNs, _writeConcern, targetEpoch, std::move(batch)); } catch (const ExceptionFor<ErrorCodes::ImmutableField>& ex) { uassertStatusOKWithContext(ex.toStatus(), "$merge failed to update the matching document, did you " @@ -706,10 +577,6 @@ void DocumentSourceMerge::spill(BatchedCommandRequest&& bcr, BatchedObjects&& ba } } -BatchedCommandRequest DocumentSourceMerge::initializeBatchedWriteRequest() const { - return _descriptor.batchedCommandGenerator(pExpCtx, _outputNs); -} - void DocumentSourceMerge::waitWhileFailPointEnabled() { CurOpFailpointHelpers::waitWhileFailPointEnabled( &hangWhileBuildingDocumentSourceMergeBatch, diff --git a/src/mongo/db/pipeline/document_source_merge.h b/src/mongo/db/pipeline/document_source_merge.h index fe5bec997e2..05a87f1e340 100644 --- a/src/mongo/db/pipeline/document_source_merge.h +++ b/src/mongo/db/pipeline/document_source_merge.h @@ -44,38 +44,24 @@ class DocumentSourceMerge final : public DocumentSourceWriter<MongoProcessInterf public: static constexpr StringData kStageName = "$merge"_sd; - using BatchTransform = std::function<void(MongoProcessInterface::BatchObject&)>; - - // A descriptor for a merge strategy. Holds a merge strategy function and a set of actions the - // client should be authorized to perform in order to be able to execute a merge operation using - // this merge strategy. Additionally holds a 'BatchedCommandGenerator' that will initialize a - // BatchedWriteRequest for executing the batch write. If a 'BatchTransform' function is - // provided, it will be called when constructing a batch object to transform updates. + // A descriptor for a merge strategy. Holds a merge strategy function and a set of actions + // the client should be authorized to perform in order to be able to execute a merge operation + // using this merge strategy. struct MergeStrategyDescriptor { using WhenMatched = MergeWhenMatchedModeEnum; using WhenNotMatched = MergeWhenNotMatchedModeEnum; using MergeMode = std::pair<WhenMatched, WhenNotMatched>; - using UpsertType = MongoProcessInterface::UpsertType; // A function encapsulating a merge strategy for the $merge stage based on the pair of // whenMatched/whenNotMatched modes. using MergeStrategy = std::function<void(const boost::intrusive_ptr<ExpressionContext>&, const NamespaceString&, const WriteConcernOptions&, boost::optional<OID>, - BatchedObjects&&, - BatchedCommandRequest&&, - UpsertType upsert)>; - - // A function object that will be invoked to generate a BatchedCommandRequest. - using BatchedCommandGenerator = std::function<BatchedCommandRequest( - const boost::intrusive_ptr<ExpressionContext>&, const NamespaceString&)>; + BatchedObjects&&)>; MergeMode mode; ActionSet actions; MergeStrategy strategy; - BatchTransform transform; - UpsertType upsertType; - BatchedCommandGenerator batchedCommandGenerator; }; /** @@ -135,7 +121,7 @@ public: boost::optional<DistributedPlanLogic> distributedPlanLogic() final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; /** * Creates a new $merge stage from the given arguments. @@ -191,11 +177,8 @@ private: * Creates an UpdateModification object from the given 'doc' to be used with the batched update. */ auto makeBatchUpdateModification(const Document& doc) const { - return _pipeline - ? write_ops::UpdateModification(*_pipeline) - : write_ops::UpdateModification(doc.toBson(), - write_ops::UpdateModification::ClassicTag{}, - true /* isReplacement */); + return _pipeline ? write_ops::UpdateModification(*_pipeline) + : write_ops::UpdateModification::parseFromClassicUpdate(doc.toBson()); } /** @@ -218,9 +201,7 @@ private: return bob.obj(); } - void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) override; - - BatchedCommandRequest initializeBatchedWriteRequest() const override; + void spill(BatchedObjects&& batch) override; void waitWhileFailPointEnabled() override; diff --git a/src/mongo/db/pipeline/document_source_merge.idl b/src/mongo/db/pipeline/document_source_merge.idl index 6aa54ec327d..7948517e773 100644 --- a/src/mongo/db/pipeline/document_source_merge.idl +++ b/src/mongo/db/pipeline/document_source_merge.idl @@ -81,19 +81,16 @@ structs: DocumentSourceMergeSpec: description: A document used to specify the $merge stage of an aggregation pipeline. strict: true - query_shape_component: true fields: into: cpp_name: targetNss type: MergeTargetNss description: Target namespace to merge documents from an aggregation pipeline into. - query_shape: custom on: type: MergeOnFields optional: true description: A single field or array of fields that uniquely identify a document. - query_shape: custom let: type: object @@ -101,21 +98,18 @@ structs: description: Specifies variables to use in the update pipeline defined in MergeWhenMatchedPolicy when the 'whenMatched' mode is a custom pipeline. - query_shape: custom whenMatched: type: MergeWhenMatchedPolicy optional: true description: The merge mode for the merge operation when source and target elements match. - query_shape: parameter whenNotMatched: type: MergeWhenNotMatchedMode optional: true description: The merge mode for the merge operation when source and target elements do not match. - query_shape: parameter targetCollectionVersion: type: ChunkVersionArrayWronglyEncodedAsBSONObjFormat @@ -125,4 +119,3 @@ structs: in which case the shard key may have changed, or had its shard key refined. This also can be used to detect if the collection has gone from unsharded to sharded, and thus now has a shard key. - query_shape: parameter diff --git a/src/mongo/db/pipeline/document_source_merge_cursors_test.cpp b/src/mongo/db/pipeline/document_source_merge_cursors_test.cpp index 5f349309ac7..61b2aef0c97 100644 --- a/src/mongo/db/pipeline/document_source_merge_cursors_test.cpp +++ b/src/mongo/db/pipeline/document_source_merge_cursors_test.cpp @@ -53,7 +53,6 @@ #include "mongo/s/client/shard_registry.h" #include "mongo/s/sharding_router_test_fixture.h" #include "mongo/stdx/thread.h" -#include "mongo/unittest/bson_test_util.h" #include "mongo/unittest/unittest.h" namespace mongo { @@ -381,42 +380,5 @@ TEST_F(DocumentSourceMergeCursorsTest, ShouldEnforceSortSpecifiedViaARMParams) { future.default_timed_get(); } - -using DocumentSourceMergeCursorsShapeTest = AggregationContextFixture; -TEST_F(DocumentSourceMergeCursorsShapeTest, QueryShape) { - auto expCtx = getExpCtx(); - AsyncResultsMergerParams armParams; - armParams.setNss(kTestNss); - std::vector<RemoteCursor> cursors; - cursors.emplace_back( - makeRemoteCursor(kTestShardIds[0], kTestShardHosts[0], CursorResponse(expCtx->ns, 1, {}))); - cursors.emplace_back( - makeRemoteCursor(kTestShardIds[1], kTestShardHosts[1], CursorResponse(expCtx->ns, 2, {}))); - armParams.setRemotes(std::move(cursors)); - auto stage = DocumentSourceMergeCursors::create(expCtx, std::move(armParams)); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$mergeCursors": { - "compareWholeSortKey": "?bool", - "remotes": [ - { - "shardId": "HASH<FakeShard1>", - "hostAndPort": "HASH<FakeShard1Host:12345>", - "cursorResponse": "?object" - }, - { - "shardId": "HASH<FakeShard2>", - "hostAndPort": "HASH<FakeShard2Host:12345>", - "cursorResponse": "?object" - } - ], - "nss": "HASH<test.mergeCursors>", - "allowPartialResults": false, - "recordRemoteOpWaitTime": false - } - })", - redact(*stage)); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_merge_spec.cpp b/src/mongo/db/pipeline/document_source_merge_spec.cpp index 84184804dd2..41926a7cfd9 100644 --- a/src/mongo/db/pipeline/document_source_merge_spec.cpp +++ b/src/mongo/db/pipeline/document_source_merge_spec.cpp @@ -37,7 +37,6 @@ #include "mongo/db/pipeline/aggregation_request_helper.h" #include "mongo/db/pipeline/document_source_merge.h" #include "mongo/db/pipeline/document_source_merge_gen.h" -#include "mongo/db/query/query_shape/serialization_options.h" namespace mongo { using namespace fmt::literals; @@ -67,11 +66,8 @@ NamespaceString mergeTargetNssParseFromBSON(const BSONElement& elem) { void mergeTargetNssSerializeToBSON(const NamespaceString& targetNss, StringData fieldName, - BSONObjBuilder* bob, - const SerializationOptions& opts) { - bob->append(fieldName, - BSON("db" << opts.serializeIdentifier(targetNss.db()) << "coll" - << opts.serializeIdentifier(targetNss.coll()))); + BSONObjBuilder* bob) { + bob->append(fieldName, BSON("db" << targetNss.db() << "coll" << targetNss.coll())); } std::vector<std::string> mergeOnFieldsParseFromBSON(const BSONElement& elem) { @@ -108,12 +104,11 @@ std::vector<std::string> mergeOnFieldsParseFromBSON(const BSONElement& elem) { void mergeOnFieldsSerializeToBSON(const std::vector<std::string>& fields, StringData fieldName, - BSONObjBuilder* bob, - const SerializationOptions& opts) { + BSONObjBuilder* bob) { if (fields.size() == 1) { - bob->append(fieldName, opts.serializeFieldPathFromString(fields.front())); + bob->append(fieldName, fields.front()); } else { - bob->append(fieldName, opts.serializeFieldPathFromString(fields)); + bob->append(fieldName, fields); } } diff --git a/src/mongo/db/pipeline/document_source_merge_spec.h b/src/mongo/db/pipeline/document_source_merge_spec.h index 2ccd4f8548a..35c353bb78a 100644 --- a/src/mongo/db/pipeline/document_source_merge_spec.h +++ b/src/mongo/db/pipeline/document_source_merge_spec.h @@ -29,7 +29,6 @@ #pragma once -#include "mongo/db/query/query_shape/serialization_options.h" #include <boost/optional.hpp> #include <string> #include <vector> @@ -55,8 +54,7 @@ struct MergeWhenMatchedPolicy { */ void mergeTargetNssSerializeToBSON(const NamespaceString& targetNss, StringData fieldName, - BSONObjBuilder* bob, - const SerializationOptions& opts = {}); + BSONObjBuilder* bob); NamespaceString mergeTargetNssParseFromBSON(const BSONElement& elem); /** @@ -65,8 +63,7 @@ NamespaceString mergeTargetNssParseFromBSON(const BSONElement& elem); */ void mergeOnFieldsSerializeToBSON(const std::vector<std::string>& fields, StringData fieldName, - BSONObjBuilder* bob, - const SerializationOptions& opts = {}); + BSONObjBuilder* bob); std::vector<std::string> mergeOnFieldsParseFromBSON(const BSONElement& elem); /** diff --git a/src/mongo/db/pipeline/document_source_merge_test.cpp b/src/mongo/db/pipeline/document_source_merge_test.cpp index 1987d09da07..e7d87707900 100644 --- a/src/mongo/db/pipeline/document_source_merge_test.cpp +++ b/src/mongo/db/pipeline/document_source_merge_test.cpp @@ -29,10 +29,8 @@ #include "mongo/platform/basic.h" -#include "mongo/unittest/bson_test_util.h" #include <boost/intrusive_ptr.hpp> -#include "mongo/bson/unordered_fields_bsonobj_comparator.h" #include "mongo/db/exec/document_value/document.h" #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" @@ -794,12 +792,12 @@ TEST_F(DocumentSourceMergeTest, SerializeDefaultLetVariable) { // Test the behaviour of 'let' serialization for each whenNotMatched mode. TEST_F(DocumentSourceMergeTest, SerializeLetVariables) { - auto pipeline = BSON_ARRAY(BSON("$project" << BSON("_id" << true << "x" - << "$$v1" - << "y" - << "$$v2" - << "z" - << "$$v3"))); + auto pipeline = BSON_ARRAY(BSON("$project" << BSON("x" + << "$$v1" + << "y" + << "$$v2" + << "z" + << "$$v3"))); const auto createAndSerializeMergeStage = [this, &pipeline](StringData whenNotMatched) { auto spec = BSON("$merge" << BSON("into" @@ -845,8 +843,8 @@ TEST_F(DocumentSourceMergeTest, SerializeLetVariables) { TEST_F(DocumentSourceMergeTest, SerializeLetArrayVariable) { for (auto&& whenNotMatched : {"insert", "fail", "discard"}) { - auto pipeline = BSON_ARRAY(BSON("$project" << BSON("_id" << true << "x" - << "$$v1"))); + auto pipeline = BSON_ARRAY(BSON("$project" << BSON("x" + << "$$v1"))); auto spec = BSON( "$merge" << BSON("into" << "target_collection" @@ -874,9 +872,8 @@ TEST_F(DocumentSourceMergeTest, SerializeLetArrayVariable) { // SERVER-41272, this test should be updated to accordingly. TEST_F(DocumentSourceMergeTest, SerializeNullLetVariablesAsDefault) { for (auto&& whenNotMatched : {"insert", "fail", "discard"}) { - auto pipeline = BSON_ARRAY(BSON("$project" << BSON("_id" << true << "x" - << BSON("$const" - << "1")))); + auto pipeline = BSON_ARRAY(BSON("$project" << BSON("x" + << "1"))); auto spec = BSON("$merge" << BSON("into" << "target_collection" << "let" << BSONNULL << "whenMatched" << pipeline @@ -893,9 +890,8 @@ TEST_F(DocumentSourceMergeTest, SerializeNullLetVariablesAsDefault) { TEST_F(DocumentSourceMergeTest, SerializeEmptyLetVariables) { for (auto&& whenNotMatched : {"insert", "fail", "discard"}) { - auto pipeline = BSON_ARRAY(BSON("$project" << BSON("_id" << true << "x" - << BSON("$const" - << "1")))); + auto pipeline = BSON_ARRAY(BSON("$project" << BSON("x" + << "1"))); auto spec = BSON("$merge" << BSON("into" << "target_collection" << "let" << BSONObj() << "whenMatched" << pipeline @@ -911,41 +907,6 @@ TEST_F(DocumentSourceMergeTest, SerializeEmptyLetVariables) { } } -TEST_F(DocumentSourceMergeTest, SerializeEmptyLetVariableMentionNew) { - auto pipeline = BSON_ARRAY(fromjson("{$project: {_id: true, x: '$$new'}}")); - auto spec = - BSON("$merge" << BSON("into" - << "target_collection" - << "let" << BSONObj() << "whenMatched" << pipeline << "whenNotMatched" - << "insert")); - auto mergeStage = createMergeStage(spec); - ASSERT(mergeStage); - auto serialized = mergeStage->serialize().getDocument(); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$merge": { - "into": { - "db": "unittests", - "coll": "target_collection" - }, - "on": "_id", - "let": { - "new": "$$ROOT" - }, - "whenMatched": [ - { - "$project": { - "_id": true, - "x": "$$new" - } - } - ], - "whenNotMatched": "insert" - } - })", - serialized.toBson()); -} - TEST_F(DocumentSourceMergeTest, OnlyObjectCanBeUsedAsLetVariables) { for (auto&& whenNotMatched : {"insert", "fail", "discard"}) { auto pipeline = BSON_ARRAY(BSON("$project" << BSON("x" @@ -989,49 +950,5 @@ TEST_F(DocumentSourceMergeTest, FailsToParseIfOnFieldHaveDuplicates) { ASSERT_THROWS_CODE(createMergeStage(spec), AssertionException, 31465); } -TEST_F(DocumentSourceMergeTest, QueryShape) { - auto pipeline = BSON_ARRAY(BSON("$project" << BSON("x" - << "1"))); - auto let = BSON("new" - << "$$ROOT" - << "year" - << "2020"); - auto spec = - BSON("$merge" << BSON("into" - << "target_collection" - << "let" << let << "whenMatched" << pipeline << "whenNotMatched" - << "insert")); - auto mergeStage = createMergeStage(spec); - ASSERT(mergeStage); - auto serialized = mergeStage->serialize().getDocument(); - - auto expectedBson = fromjson(R"({ - "$merge": { - "into": { - "db": "HASH<unittests>", - "coll": "HASH<target_collection>" - }, - "on": "HASH<_id>", - "let": { - "HASH<year>": "?string", - "HASH<new>": "$$ROOT" - }, - "whenMatched": [ - { - "$project": { - "HASH<_id>": true, - "HASH<x>": "?string" - } - } - ], - "whenNotMatched": "insert" - } - })"); - auto result = redact(*mergeStage); - UnorderedFieldsBSONObjComparator comparator; - ASSERT_EQ(0, comparator.compare(redact(*mergeStage), expectedBson)) - << "Expected [" << expectedBson << "] but found [" << result << "]"; -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_mock.h b/src/mongo/db/pipeline/document_source_mock.h index 67f3aad8649..a6be10cbb84 100644 --- a/src/mongo/db/pipeline/document_source_mock.h +++ b/src/mongo/db/pipeline/document_source_mock.h @@ -76,7 +76,7 @@ public: DocumentSourceMock(std::deque<GetNextResult>, const boost::intrusive_ptr<ExpressionContext>&); Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override { // Unlike the queue, it's okay to serialize this stage for testing purposes. return Value(Document{{getSourceName(), Document()}}); } @@ -106,7 +106,7 @@ public: * This stage does not modify anything. */ GetModPathsReturn getModifiedPaths() const override { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() override { diff --git a/src/mongo/db/pipeline/document_source_operation_metrics.cpp b/src/mongo/db/pipeline/document_source_operation_metrics.cpp index 7a9dd328f9f..a5e523465e4 100644 --- a/src/mongo/db/pipeline/document_source_operation_metrics.cpp +++ b/src/mongo/db/pipeline/document_source_operation_metrics.cpp @@ -113,7 +113,8 @@ intrusive_ptr<DocumentSource> DocumentSourceOperationMetrics::createFromBson( return new DocumentSourceOperationMetrics(pExpCtx, clearMetrics); } -Value DocumentSourceOperationMetrics::serialize(const SerializationOptions& opts) const { +Value DocumentSourceOperationMetrics::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { return Value(DOC(getSourceName() << Document())); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_operation_metrics.h b/src/mongo/db/pipeline/document_source_operation_metrics.h index 95ac5585b42..d4e25d47bc5 100644 --- a/src/mongo/db/pipeline/document_source_operation_metrics.h +++ b/src/mongo/db/pipeline/document_source_operation_metrics.h @@ -89,7 +89,7 @@ public: return boost::none; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); diff --git a/src/mongo/db/pipeline/document_source_out.cpp b/src/mongo/db/pipeline/document_source_out.cpp index a6188dfd382..9be7c7a24ba 100644 --- a/src/mongo/db/pipeline/document_source_out.cpp +++ b/src/mongo/db/pipeline/document_source_out.cpp @@ -177,11 +177,6 @@ void DocumentSourceOut::finalize() { _tempNs = {}; } -BatchedCommandRequest DocumentSourceOut::initializeBatchedWriteRequest() const { - // Note that our insert targets '_tempNs' since we will never write to 'outputNs' directly. - return DocumentSourceWriter::makeInsertCommand(_tempNs, pExpCtx->bypassDocumentValidation); -} - boost::intrusive_ptr<DocumentSource> DocumentSourceOut::create( NamespaceString outputNs, const boost::intrusive_ptr<ExpressionContext>& expCtx) { @@ -210,11 +205,8 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceOut::createFromBson( return create(targetNS, expCtx); } -Value DocumentSourceOut::serialize(const SerializationOptions& opts) const { - MutableDocument spec; - spec["coll"] = Value(opts.serializeIdentifier(_outputNs.coll())); - spec["db"] = Value(opts.serializeIdentifier(_outputNs.db())); - return Value(Document{{kStageName, spec.freezeToValue()}}); +Value DocumentSourceOut::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(kStageName << DOC("db" << _outputNs.db() << "coll" << _outputNs.coll()))); } void DocumentSourceOut::waitWhileFailPointEnabled() { diff --git a/src/mongo/db/pipeline/document_source_out.h b/src/mongo/db/pipeline/document_source_out.h index 4b2e043dd22..64dda167eb3 100644 --- a/src/mongo/db/pipeline/document_source_out.h +++ b/src/mongo/db/pipeline/document_source_out.h @@ -91,7 +91,8 @@ public: UnionRequirement::kNotAllowed}; } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize( + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final override; /** * Creates a new $out stage from the given arguments. @@ -120,25 +121,19 @@ private: void finalize() override; - void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) override { + void spill(BatchedObjects&& batch) override { DocumentSourceWriteBlock writeBlock(pExpCtx->opCtx); - auto insertCommand = bcr.extractInsertRequest(); - insertCommand->setDocuments(std::move(batch)); auto targetEpoch = boost::none; - uassertStatusOK(pExpCtx->mongoProcessInterface->insert( - pExpCtx, _tempNs, std::move(insertCommand), _writeConcern, targetEpoch)); + pExpCtx, _tempNs, std::move(batch), _writeConcern, targetEpoch)); } std::pair<BSONObj, int> makeBatchObject(Document&& doc) const override { auto obj = doc.toBson(); - tassert(6628900, "_writeSizeEstimator should be initialized", _writeSizeEstimator); - return {obj, _writeSizeEstimator->estimateInsertSizeBytes(obj)}; + return {obj, obj.objsize()}; } - BatchedCommandRequest initializeBatchedWriteRequest() const override; - void waitWhileFailPointEnabled() override; // Holds on to the original collection options and index specs so we can check they didn't diff --git a/src/mongo/db/pipeline/document_source_out_test.cpp b/src/mongo/db/pipeline/document_source_out_test.cpp index 1338addb727..aef9be321f0 100644 --- a/src/mongo/db/pipeline/document_source_out_test.cpp +++ b/src/mongo/db/pipeline/document_source_out_test.cpp @@ -114,24 +114,5 @@ TEST_F(DocumentSourceOutTest, SerializeToString) { ASSERT_EQ(reSerialized["$out"]["coll"].getStringData(), "some_collection"); } -TEST_F(DocumentSourceOutTest, Redaction) { - auto spec = fromjson(R"({ - $out: { - db: "foo", - coll: "bar" - } - })"); - auto docSource = DocumentSourceOut::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - $out: { - coll: "HASH<bar>", - db: "HASH<foo>" - } - })", - redact(*docSource)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_plan_cache_stats.cpp b/src/mongo/db/pipeline/document_source_plan_cache_stats.cpp index f49a3cba01f..51f7bf3d650 100644 --- a/src/mongo/db/pipeline/document_source_plan_cache_stats.cpp +++ b/src/mongo/db/pipeline/document_source_plan_cache_stats.cpp @@ -57,13 +57,9 @@ DocumentSourcePlanCacheStats::DocumentSourcePlanCacheStats( const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSource(kStageName, expCtx) {} -void DocumentSourcePlanCacheStats::serializeToArray(std::vector<Value>& array, - const SerializationOptions& opts) const { - if (opts.verbosity) { - tassert(7513100, - "$planCacheStats is not equipped to serialize in explain mode with redaction on", - !opts.transformIdentifiers && - opts.literalPolicy == LiteralSerializationPolicy::kUnchanged); +void DocumentSourcePlanCacheStats::serializeToArray( + std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { array.push_back(Value{ Document{{kStageName, Document{{"match"_sd, @@ -71,7 +67,7 @@ void DocumentSourcePlanCacheStats::serializeToArray(std::vector<Value>& array, } else { array.push_back(Value{Document{{kStageName, Document{}}}}); if (_absorbedMatch) { - _absorbedMatch->serializeToArray(array, opts); + _absorbedMatch->serializeToArray(array); } } } diff --git a/src/mongo/db/pipeline/document_source_plan_cache_stats.h b/src/mongo/db/pipeline/document_source_plan_cache_stats.h index e1db4cf732c..d72b0a22ac8 100644 --- a/src/mongo/db/pipeline/document_source_plan_cache_stats.h +++ b/src/mongo/db/pipeline/document_source_plan_cache_stats.h @@ -117,7 +117,7 @@ public: void serializeToArray( std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const final override; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override; private: DocumentSourcePlanCacheStats(const boost::intrusive_ptr<ExpressionContext>& expCtx); @@ -125,8 +125,8 @@ private: GetNextResult doGetNext() final; Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { - MONGO_UNREACHABLE_TASSERT(7484303); // Should call serializeToArray instead. + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const override { + MONGO_UNREACHABLE; // Should call serializeToArray instead. } // If running through mongos in a sharded cluster, stores the shard name so that it can be diff --git a/src/mongo/db/pipeline/document_source_plan_cache_stats_test.cpp b/src/mongo/db/pipeline/document_source_plan_cache_stats_test.cpp index 262806e0780..4f89e47acdf 100644 --- a/src/mongo/db/pipeline/document_source_plan_cache_stats_test.cpp +++ b/src/mongo/db/pipeline/document_source_plan_cache_stats_test.cpp @@ -41,8 +41,6 @@ namespace mongo { using DocumentSourcePlanCacheStatsTest = AggregationContextFixture; -static const SerializationOptions kExplain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; /** * A MongoProcessInterface used for testing which returns artificial plan cache stats. */ @@ -108,9 +106,7 @@ TEST_F(DocumentSourcePlanCacheStatsTest, CanParseAndSerializeAsExplainSuccessful const auto specObj = fromjson("{$planCacheStats: {}}"); auto stage = DocumentSourcePlanCacheStats::createFromBson(specObj.firstElement(), getExpCtx()); std::vector<Value> serialized; - stage->serializeToArray( - serialized, - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}); + stage->serializeToArray(serialized, ExplainOptions::Verbosity::kQueryPlanner); ASSERT_EQ(1u, serialized.size()); ASSERT_BSONOBJ_EQ(specObj, serialized[0].getDocument().toBson()); } @@ -143,31 +139,12 @@ TEST_F(DocumentSourcePlanCacheStatsTest, SerializesSuccessfullyAfterAbsorbingMat pipeline->optimizePipeline(); ASSERT_EQ(1u, pipeline->getSources().size()); - auto serialized = pipeline->writeExplainOps(kExplain); + auto serialized = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); ASSERT_EQ(1u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$planCacheStats: {match: {foo: 'bar'}}}"), serialized[0].getDocument().toBson()); } -TEST_F(DocumentSourcePlanCacheStatsTest, RedactsSuccessfullyAfterAbsorbingMatch) { - const auto specObj = fromjson("{$planCacheStats: {}}"); - auto planCacheStats = - DocumentSourcePlanCacheStats::createFromBson(specObj.firstElement(), getExpCtx()); - auto match = DocumentSourceMatch::create(fromjson("{foo: 'bar'}"), getExpCtx()); - auto pipeline = Pipeline::create({planCacheStats, match}, getExpCtx()); - ASSERT_EQ(2u, pipeline->getSources().size()); - - pipeline->optimizePipeline(); - ASSERT_EQ(1u, pipeline->getSources().size()); - auto serialized = redactToArray(*pipeline->getSources().front()); - ASSERT_EQ(2u, serialized.size()); - - ASSERT_BSONOBJ_EQ(specObj, serialized[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$match":{"HASH<foo>":{"$eq":"?string"}}})", - serialized[1].getDocument().toBson().getOwned()); -} - TEST_F(DocumentSourcePlanCacheStatsTest, ReturnsImmediateEOFWithEmptyPlanCache) { getExpCtx()->mongoProcessInterface = std::make_shared<PlanCacheStatsMongoProcessInterface>(std::vector<BSONObj>{}); diff --git a/src/mongo/db/pipeline/document_source_project_test.cpp b/src/mongo/db/pipeline/document_source_project_test.cpp index bd25639ee3e..ced99ce0200 100644 --- a/src/mongo/db/pipeline/document_source_project_test.cpp +++ b/src/mongo/db/pipeline/document_source_project_test.cpp @@ -338,46 +338,6 @@ TEST_F(ProjectStageTest, CannotAddNestedDocumentExceedingDepthLimit) { ErrorCodes::Overflow); } -/** - * A default redaction strategy that generates easy to check results for testing purposes. - */ -std::string transformIdentifiersForTest(StringData s) { - return str::stream() << "HASH<" << s << ">"; -} - -TEST_F(ProjectStageTest, ShapifyAndRedact) { - auto inclusionProject = DocumentSourceProject::create( - fromjson("{a: true, x: '$b', y: {$and: ['$c','$d']}, z: {$meta: 'textScore'}}"), - getExpCtx(), - "$project"_sd); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$project": { - "HASH<_id>": true, - "HASH<a>": true, - "HASH<x>": "$HASH<b>", - "HASH<y>": { - "$and": [ "$HASH<c>", "$HASH<d>" ] - }, - "HASH<z>": { "$meta": "textScore" } - } - })", - redact(*inclusionProject)); - - auto exclusionProject = DocumentSourceProject::create( - fromjson("{a: false, 'b.c': false}"), getExpCtx(), "$project"_sd); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$project": { - "HASH<a>": false, - "HASH<b>": { - "HASH<c>": false }, - "HASH<_id>": true } - })", - redact(*exclusionProject)); -} - TEST_F(UnsetTest, AcceptsValidUnsetSpecWithArray) { auto spec = BSON("$unset" << BSON_ARRAY("a" << "b" diff --git a/src/mongo/db/pipeline/document_source_query_stats.cpp b/src/mongo/db/pipeline/document_source_query_stats.cpp deleted file mode 100644 index e4b805f376d..00000000000 --- a/src/mongo/db/pipeline/document_source_query_stats.cpp +++ /dev/null @@ -1,301 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/db/pipeline/document_source_query_stats.h" - -#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQueryStats - -#include "mongo/idl/idl_parser.h" -#include "mongo/logv2/log.h" -#include "mongo/util/assert_util.h" -#include "mongo/util/debug_util.h" - -namespace mongo { -namespace { -Counter64 queryStatsHmacApplicationErrors; -ServerStatusMetricField<Counter64> displayWriteErrorsMetric("queryStats.numHmacApplicationErrors", - &queryStatsHmacApplicationErrors); -} // namespace - -REGISTER_DOCUMENT_SOURCE(queryStats, - DocumentSourceQueryStats::LiteParsed::parse, - DocumentSourceQueryStats::createFromBson, - AllowedWithApiStrict::kNeverInVersion1); - -namespace { - -/** - * Parse the spec object calling the `ctor` with the TransformAlgorithm enum algorithm and - * std::string hmacKey arguments. - */ -template <typename Ctor> -auto parseSpec(const BSONElement& spec, const Ctor& ctor) { - uassert(ErrorCodes::FailedToParse, - str::stream() << DocumentSourceQueryStats::kStageName - << " value must be an object. Found: " << typeName(spec.type()), - spec.type() == BSONType::Object); - BSONObj obj = spec.embeddedObject(); - TransformAlgorithmEnum algorithm = TransformAlgorithmEnum::kNone; - std::string hmacKey; - auto parsed = DocumentSourceQueryStatsSpec::parse(DocumentSourceQueryStats::kStageName, obj); - boost::optional<TransformIdentifiersSpec> transformIdentifiers = - parsed.getTransformIdentifiers(); - - if (transformIdentifiers) { - algorithm = transformIdentifiers->getAlgorithm(); - boost::optional<ConstDataRange> hmacKeyContainer = transformIdentifiers->getHmacKey(); - uassert(ErrorCodes::FailedToParse, - str::stream() << "The 'hmacKey' parameter of the $queryStats stage must be " - "specified when applying the hmac-sha-256 algorithm", - algorithm != TransformAlgorithmEnum::kHmacSha256 || - hmacKeyContainer != boost::none); - hmacKey = std::string(hmacKeyContainer->data(), (size_t)hmacKeyContainer->length()); - } - return ctor(algorithm, hmacKey); -} - -} // namespace - -BSONObj DocumentSourceQueryStats::computeQueryStatsKey(std::shared_ptr<const Key> key) const { - static const auto sha256HmacStringDataHasher = [](std::string key, const StringData& sd) { - auto hashed = SHA256Block::computeHmac( - (const uint8_t*)key.data(), key.size(), (const uint8_t*)sd.rawData(), sd.size()); - return hashed.toString(); - }; - - auto opts = SerializationOptions{}; - opts.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString; - if (_algorithm == TransformAlgorithmEnum::kHmacSha256) { - opts.transformIdentifiers = true; - opts.transformIdentifiersCallback = [&](StringData sd) { - return sha256HmacStringDataHasher(_hmacKey, sd); - }; - } - return key->toBson(pExpCtx->opCtx, opts); -} - -std::unique_ptr<DocumentSourceQueryStats::LiteParsed> DocumentSourceQueryStats::LiteParsed::parse( - const NamespaceString& nss, const BSONElement& spec) { - return parseSpec(spec, [&](TransformAlgorithmEnum algorithm, std::string hmacKey) { - return std::make_unique<DocumentSourceQueryStats::LiteParsed>( - spec.fieldName(), algorithm, hmacKey); - }); -} - -boost::intrusive_ptr<DocumentSource> DocumentSourceQueryStats::createFromBson( - BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& pExpCtx) { - const NamespaceString& nss = pExpCtx->ns; - - uassert(ErrorCodes::InvalidNamespace, - "$queryStats must be run against the 'admin' database with {aggregate: 1}", - nss.db() == NamespaceString::kAdminDb && nss.isCollectionlessAggregateNS()); - - LOGV2_DEBUG_OPTIONS(7808300, - 1, - {logv2::LogTruncation::Disabled}, - "Logging invocation $queryStats", - "commandSpec"_attr = - spec.Obj().redact(BSONObj::RedactLevel::sensitiveOnly)); - return parseSpec(spec, [&](TransformAlgorithmEnum algorithm, std::string hmacKey) { - return new DocumentSourceQueryStats(pExpCtx, algorithm, hmacKey); - }); -} - -Value DocumentSourceQueryStats::serialize(const SerializationOptions& opts) const { - auto hmacKey = opts.serializeLiteral( - BSONBinData(_hmacKey.c_str(), _hmacKey.size(), BinDataType::Sensitive)); - if (opts.literalPolicy == LiteralSerializationPolicy::kToRepresentativeParseableValue) { - // The default shape for a BinData under this policy is empty and has sub-type 0 (general). - // This doesn't quite work for us since we assert when we parse that it is at least 32 bytes - // and also is sub-type 8 (sensitive). - hmacKey = - Value(BSONBinData("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 32, BinDataType::Sensitive)); - } - return Value{ - Document{{kStageName, - _transformIdentifiers - ? Document{{"transformIdentifiers", - Document{{"algorithm", TransformAlgorithm_serializer(_algorithm)}, - {"hmacKey", hmacKey}}}} - : Document{}}}}; -} - -DocumentSource::GetNextResult DocumentSourceQueryStats::doGetNext() { - const auto shouldLog = _algorithm != TransformAlgorithmEnum::kNone; - /** - * When a CopiedPartition is present (loaded) and contains more elements (QueryStatsEntry), we - * can process and return the next element in the _currentCopiedPartition. - * - * When the current CopiedPartition is exhausted (emptied), we move on to the next - * partition. Once we have iterated to the end of the valid partitions, we are done iteratiing - * over all the queryStatsStore entries. - * - * We iterate over a copied container (CopiedParitition) containing the entries in - * the partition to reduce the time under which the partition lock is held. - */ - auto& queryStatsStore = getQueryStatsStore(getContext()->opCtx); - - while (_currentCopiedPartition.isValidPartitionId(queryStatsStore.numPartitions())) { - if (!_currentCopiedPartition.isLoaded()) { - _currentCopiedPartition.load(queryStatsStore); - } - // CopiedPartition::load() will throw if any errors occur. - // Safe to assume _currentCopiedPartition is now loaded. - - // Exhaust all elements in the current copied partition. - // Use a while loop here to handle cases where toDocument() may fail for a specific - // QueryStatsEntry, in which case we suppress the thrown exception and continue - // iterating to the next available entry. - while (!_currentCopiedPartition.empty()) { - auto& statsEntries = _currentCopiedPartition.statsEntries; - const auto& queryStatsEntry = statsEntries.front(); - ON_BLOCK_EXIT([&statsEntries]() { statsEntries.pop_front(); }); - if (auto doc = - toDocument(_currentCopiedPartition.getReadTimestamp(), queryStatsEntry)) { - if (shouldLog) { - LOGV2_DEBUG_OPTIONS(7808301, - 3, - {logv2::LogTruncation::Disabled}, - "Logging all outputs of $queryStats", - "thisOutput"_attr = *doc); - } - return std::move(*doc); - } - } - // Once we have exhausted entries in this partition, move on to the next partition. - _currentCopiedPartition.incrementPartitionId(); - } - - if (shouldLog) { - LOGV2_DEBUG_OPTIONS( - 7808302, 3, {logv2::LogTruncation::Disabled}, "Finished logging output of $queryStats"); - } - return DocumentSource::GetNextResult::makeEOF(); -} - -boost::optional<Document> DocumentSourceQueryStats::toDocument( - const Date_t& partitionReadTime, const QueryStatsEntry& queryStatsEntry) const { - const auto& key = queryStatsEntry.key; - try { - auto queryStatsKey = computeQueryStatsKey(key); - // We use the representative shape to generate the key hash. This avoids returning duplicate - // hashes if we have bugs that cause two different representative shapes to re-parse into - // the same debug shape. - auto representativeShapeKey = key->toBson( - pExpCtx->opCtx, SerializationOptions::kRepresentativeQueryShapeSerializeOptions); - - // This SHA256 version of the hash is output to aid in data analytics use cases. In these - // cases, we often care about comparing hashes from different hosts, potentially on - // different versions and platforms. The thinking here is that the SHA256 algorithm is more - // stable across these different environments than the quicker 'absl::HashOf' - // implementation. - auto hash = SHA256Block::computeHash((const uint8_t*)representativeShapeKey.objdata(), - representativeShapeKey.objsize()) - .toString(); - return Document{{"key", std::move(queryStatsKey)}, - {"keyHash", hash}, - {"metrics", queryStatsEntry.toBSON()}, - {"asOf", partitionReadTime}}; - } catch (const DBException& ex) { - queryStatsHmacApplicationErrors.increment(); - const auto& hash = absl::Hash<query_stats::Key>{}(*key); - const auto queryShape = key->universalComponents()._queryShape->toBson( - pExpCtx->opCtx, SerializationOptions::kRepresentativeQueryShapeSerializeOptions); - LOGV2_DEBUG(7349403, - 3, - "Error encountered when applying hmac to query shape, will not publish " - "queryStats for this entry.", - "status"_attr = ex.toStatus(), - "hash"_attr = hash, - "debugQueryShape"_attr = queryShape); - - if (kDebugBuild || internalQueryStatsErrorsAreCommandFatal.load()) { - auto keyString = std::to_string(hash); - tasserted(7349401, - str::stream() << "Was not able to re-parse queryStats key when " - "reading queryStats.Status " - << ex.toString() << " Hash: " << keyString - << " Query Shape: " << queryShape.toString()); - } - } - return {}; -} - -/** - * Loads the current CopiedPartition with copies of the QueryStatsEntries located in partition of - * cache corresponding to the partitionId of the current CopiedPartition. This ensures that the - * partition mutex is only held for the duration of copying. - */ -void DocumentSourceQueryStats::CopiedPartition::load(QueryStatsStore& queryStatsStore) { - tassert(7932100, - "Attempted to load invalid partition.", - _partitionId < queryStatsStore.numPartitions()); - tassert(7932101, "Partition was already loaded.", !isLoaded()); - // 'statsEntries' should be empty, clear just in case. - statsEntries.clear(); - - // Capture the time at which reading the partition begins. - _readTimestamp = Date_t::now(); - { - // We only keep the partition (which holds a lock) - // for the time needed to collect the metrics (QueryStatsEntry) - const auto partition = queryStatsStore.getPartition(_partitionId); - - // Note the intentional copy of QueryStatsEntry. - // This will give us a snapshot of all the metrics we want to report. - for (auto&& [hash, metrics] : *partition) { - statsEntries.push_back(metrics); - } - } - _isLoaded = true; -} - -bool DocumentSourceQueryStats::CopiedPartition::isLoaded() const { - return _isLoaded; -} - -void DocumentSourceQueryStats::CopiedPartition::incrementPartitionId() { - // Ensure loaded state is reset when partitionId is incremented. - ++_partitionId; - _isLoaded = false; -} - -bool DocumentSourceQueryStats::CopiedPartition::isValidPartitionId( - QueryStatsStore::PartitionId maxNumPartitions) const { - return _partitionId < maxNumPartitions; -} - -const Date_t& DocumentSourceQueryStats::CopiedPartition::getReadTimestamp() const { - return _readTimestamp; -} - -bool DocumentSourceQueryStats::CopiedPartition::empty() const { - return statsEntries.empty(); -} -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_query_stats.h b/src/mongo/db/pipeline/document_source_query_stats.h deleted file mode 100644 index bc28a443d52..00000000000 --- a/src/mongo/db/pipeline/document_source_query_stats.h +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include "mongo/db/pipeline/document_source.h" -#include "mongo/db/pipeline/document_source_query_stats_gen.h" -#include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/query/query_stats/query_stats.h" -#include "mongo/util/producer_consumer_queue.h" - -namespace mongo { - -using namespace query_stats; - -class DocumentSourceQueryStats final : public DocumentSource { -public: - static constexpr StringData kStageName = "$queryStats"_sd; - - class LiteParsed final : public LiteParsedDocumentSource { - public: - static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, - const BSONElement& spec); - - LiteParsed(std::string parseTimeName, TransformAlgorithmEnum algorithm, std::string hmacKey) - : LiteParsedDocumentSource(std::move(parseTimeName)), - _algorithm(algorithm), - _hmacKey(hmacKey) {} - - stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const override { - return stdx::unordered_set<NamespaceString>(); - } - - PrivilegeVector requiredPrivileges(bool isMongos, - bool bypassDocumentValidation) const override { - return _algorithm == TransformAlgorithmEnum::kNone - - ? PrivilegeVector{Privilege(ResourcePattern::forClusterResource(), - ActionType::queryStatsReadTransformed), - Privilege(ResourcePattern::forClusterResource(), - ActionType::queryStatsRead)} - : PrivilegeVector{Privilege(ResourcePattern::forClusterResource(), - ActionType::queryStatsReadTransformed)}; - } - - bool allowedToPassthroughFromMongos() const final { - // $queryStats must be run locally on a mongod. - return false; - } - - bool isInitialSource() const final { - return true; - } - - void assertSupportsMultiDocumentTransaction() const { - transactionNotSupported(kStageName); - } - - bool _transformIdentifiers; - - const TransformAlgorithmEnum _algorithm; - - std::string _hmacKey; - }; - - static boost::intrusive_ptr<DocumentSource> createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - - virtual ~DocumentSourceQueryStats() = default; - - StageConstraints constraints( - Pipeline::SplitState = Pipeline::SplitState::kUnsplit) const override { - StageConstraints constraints{StreamType::kStreaming, - PositionRequirement::kFirst, - HostTypeRequirement::kLocalOnly, - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kNotAllowed, - TransactionRequirement::kNotAllowed, - LookupRequirement::kNotAllowed, - UnionRequirement::kNotAllowed}; - - constraints.requiresInputDocSource = false; - constraints.isIndependentOfAnyCollection = true; - return constraints; - } - - boost::optional<DistributedPlanLogic> distributedPlanLogic() final { - return boost::none; - } - - const char* getSourceName() const override { - return kStageName.rawData(); - } - - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; - -private: - /* - * CopiedPartition: This struct is representative of a copied ("materialized") partition - * which should be loaded from the QueryStatsStore. It is used to hold a copy of the - * QueryStatsEntries corresponding to the provided partitionId. - * Once a CopiedPartition has been loaded from QueryStatsStore, it provides access to the - * QueryStatsEntries of the partition without requiring holding the lock over the partition in - * the partitioned cache. - */ - struct CopiedPartition { - CopiedPartition(QueryStatsStore::PartitionId partitionId) - : statsEntries(), _readTimestamp(), _partitionId(partitionId) {} - - ~CopiedPartition() = default; - - bool isLoaded() const; - - void incrementPartitionId(); - - bool isValidPartitionId(QueryStatsStore::PartitionId maxNumPartitions) const; - - const Date_t& getReadTimestamp() const; - - bool empty() const; - - void load(QueryStatsStore& queryStatsStore); - - std::deque<QueryStatsEntry> statsEntries; - - private: - Date_t _readTimestamp; - QueryStatsStore::PartitionId _partitionId; - bool _isLoaded{false}; - }; - - DocumentSourceQueryStats(const boost::intrusive_ptr<ExpressionContext>& expCtx, - TransformAlgorithmEnum algorithm = TransformAlgorithmEnum::kNone, - std::string hmacKey = {}) - : DocumentSource(kStageName, expCtx), - _currentCopiedPartition(0), - _transformIdentifiers(algorithm != TransformAlgorithmEnum::kNone), - _algorithm(algorithm), - _hmacKey(hmacKey) {} - - BSONObj computeQueryStatsKey(std::shared_ptr<const Key> key) const; - - GetNextResult doGetNext() final; - - boost::optional<Document> toDocument(const Date_t& partitionReadTime, - const QueryStatsEntry& queryStatsEntry) const; - - // The current partition copied from query stats store to avoid holding lock during reads. - CopiedPartition _currentCopiedPartition; - - // When true, apply hmac to field names from returned query shapes. - bool _transformIdentifiers; - - // The type of algorithm to use for transform identifiers as an enum, currently only - // kHmacSha256 - // ("hmac-sha-256") is supported. - const TransformAlgorithmEnum _algorithm; - - /** - * Key used for SHA-256 HMAC application on field names. - */ - std::string _hmacKey; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_query_stats.idl b/src/mongo/db/pipeline/document_source_query_stats.idl deleted file mode 100644 index 5a4fdb3cda0..00000000000 --- a/src/mongo/db/pipeline/document_source_query_stats.idl +++ /dev/null @@ -1,63 +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. -# -global: - cpp_namespace: "mongo" - cpp_includes: - - "mongo/db/pipeline/document_source_query_stats_validators.h" -imports: - - "mongo/idl/basic_types.idl" - - "mongo/db/query/query_stats/transform_algorithm.idl" - -structs: - TransformIdentifiersSpec: - description: "Specification for the transformIdentifiers field of $queryStats." - strict: true - fields: - algorithm: - description: "The algorithm to use to transform the identifiers." - type: TransformAlgorithm - optional: false - validator: - {callback: "validateAlgo"} - hmacKey: - description: > - The hmac key to be used to transform the identifiers. This is considered sensitive - data and should be redacted from data dumps, output I/O, logs, and other persistent - storages. - type: bindata_sensitive - optional: true - validator: - {callback: "validateHmac"} - DocumentSourceQueryStatsSpec: - description: "Specification for a $queryStats stage." - strict: true - fields: - transformIdentifiers: - description: "The algorithm and hmac key to use when transforming identifiers." - type: TransformIdentifiersSpec - optional: true diff --git a/src/mongo/db/pipeline/document_source_query_stats_test.cpp b/src/mongo/db/pipeline/document_source_query_stats_test.cpp deleted file mode 100644 index 8d08c0f2bc2..00000000000 --- a/src/mongo/db/pipeline/document_source_query_stats_test.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/platform/basic.h" - -#include "mongo/bson/bsontypes.h" -#include "mongo/db/exec/document_value/document.h" -#include "mongo/db/exec/document_value/document_value_test_util.h" -#include "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/pipeline/document_source_query_stats.h" -#include "mongo/idl/server_parameter_test_util.h" -#include "mongo/unittest/unittest.h" -#include "mongo/util/str.h" - -namespace mongo { -namespace { - -/** - * Subclass AggregationContextFixture to set the ExpressionContext's namespace to 'admin' with - * {aggregate: 1} by default, so that parsing tests other than those which validate the namespace do - * not need to explicitly set it. - */ -class DocumentSourceQueryStatsTest : public AggregationContextFixture { -public: - DocumentSourceQueryStatsTest() - : AggregationContextFixture(NamespaceString::makeCollectionlessAggregateNSS("admin")) {} -}; - -TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfSpecIsNotObject) { - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson( - fromjson("{$queryStats: 1}").firstElement(), getExpCtx()), - AssertionException, - ErrorCodes::FailedToParse); -} - -TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfNotRunOnAdmin) { - getExpCtx()->ns = NamespaceString::makeCollectionlessAggregateNSS("foo"); - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson( - fromjson("{$queryStats: {}}").firstElement(), getExpCtx()), - AssertionException, - ErrorCodes::InvalidNamespace); -} - -TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfNotRunWithAggregateOne) { - getExpCtx()->ns = NamespaceString("admin.foo"); - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson( - fromjson("{$queryStats: {}}").firstElement(), getExpCtx()), - AssertionException, - ErrorCodes::InvalidNamespace); -} - -TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfUnrecognisedParameterSpecified) { - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson( - fromjson("{$queryStats: {foo: true}}").firstElement(), getExpCtx()), - AssertionException, - 40415); -} - -TEST_F(DocumentSourceQueryStatsTest, ParseAndSerialize) { - const auto obj = fromjson("{$queryStats: {}}"); - const auto doc = DocumentSourceQueryStats::createFromBson(obj.firstElement(), getExpCtx()); - const auto queryStatsOp = static_cast<DocumentSourceQueryStats*>(doc.get()); - const auto expected = Document{{"$queryStats", Document{}}}; - const auto serialized = queryStatsOp->serialize().getDocument(); - ASSERT_DOCUMENT_EQ(expected, serialized); - - // Also make sure that we can parse out own serialization output. - - ASSERT_DOES_NOT_THROW( - DocumentSourceQueryStats::createFromBson(serialized.toBson().firstElement(), getExpCtx())); -} - -TEST_F(DocumentSourceQueryStatsTest, ParseAndSerializeShouldIncludeHmacKey) { - const auto obj = fromjson(R"({ - $queryStats: { - transformIdentifiers: { - algorithm: "hmac-sha-256", - hmacKey: { - $binary: "YW4gYXJiaXRyYXJ5IEhNQUNrZXkgZm9yIHRlc3Rpbmc=", - $type: "08" - } - } - } - })"); - const auto doc = DocumentSourceQueryStats::createFromBson(obj.firstElement(), getExpCtx()); - const auto queryStatsOp = static_cast<DocumentSourceQueryStats*>(doc.get()); - const auto expected = - Document{{"$queryStats", - Document{{"transformIdentifiers", - Document{{"algorithm", "hmac-sha-256"_sd}, - {"hmacKey", - BSONBinData("an arbitrary HMACkey for testing", - 32, - BinDataType::Sensitive)}}}}}}; - const auto serialized = queryStatsOp->serialize().getDocument(); - ASSERT_DOCUMENT_EQ(serialized, expected); - - // Also make sure that we can parse out own serialization output. - - ASSERT_DOES_NOT_THROW( - DocumentSourceQueryStats::createFromBson(serialized.toBson().firstElement(), getExpCtx())); -} - -TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfAlgorithmIsNotSupported) { - auto obj = fromjson(R"({ - $queryStats: { - transformIdentifiers: { - algorithm: "randomalgo" - } - } - })"); - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(obj.firstElement(), getExpCtx()), - AssertionException, - ErrorCodes::BadValue); -} - -TEST_F(DocumentSourceQueryStatsTest, - ShouldFailToParseIfTransformIdentifiersSpecifiedButEmptyAlgorithm) { - auto obj = fromjson(R"({ - $queryStats: { - transformIdentifiers: { - algorithm: "" - } - } - })"); - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(obj.firstElement(), getExpCtx()), - AssertionException, - ErrorCodes::BadValue); -} - -TEST_F(DocumentSourceQueryStatsTest, - ShouldFailToParseIfTransformIdentifiersSpecifiedButNoAlgorithm) { - auto obj = fromjson(R"({ - $queryStats: { - transformIdentifiers: { - } - } - })"); - ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(obj.firstElement(), getExpCtx()), - AssertionException, - 40414); -} -} // namespace -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_query_stats_validators.cpp b/src/mongo/db/pipeline/document_source_query_stats_validators.cpp deleted file mode 100644 index 8a3f234e325..00000000000 --- a/src/mongo/db/pipeline/document_source_query_stats_validators.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (C) 2021-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 "mongo/platform/basic.h" - -#include "mongo/db/pipeline/document_source_query_stats_validators.h" -#include "mongo/db/query/query_stats/transform_algorithm_gen.h" -#include "mongo/util/str.h" -#include <set> - -namespace mongo { - -Status validateAlgo(TransformAlgorithmEnum algorithm) { - if (algorithm == TransformAlgorithmEnum::kNone) { - return {ErrorCodes::FailedToParse, - str::stream() << "Algorithm specified but found no valid algorithm"}; - } - return Status::OK(); -} - -Status validateHmac(std::vector<uint8_t> hmacKey) { - if (hmacKey.size() < 32) { - return {ErrorCodes::BadValue, - str::stream() << "hmacKey must be greater than or equal to 32 bytes, found length: " - << hmacKey.size()}; - } - // length check - return Status::OK(); -} -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_query_stats_validators.h b/src/mongo/db/pipeline/document_source_query_stats_validators.h deleted file mode 100644 index 077552da0b3..00000000000 --- a/src/mongo/db/pipeline/document_source_query_stats_validators.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include "mongo/base/status.h" -#include "mongo/db/query/query_stats/transform_algorithm_gen.h" - -namespace mongo { -/** - * Validate properties of the algorithm field of $queryStats.transformIdentifiers - */ -Status validateAlgo(TransformAlgorithmEnum algorithm); - -/** - * Validate properties of the hmac key field of $queryStats.transformIdentifiers - */ -Status validateHmac(std::vector<uint8_t> hmacKey); -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_queue.cpp b/src/mongo/db/pipeline/document_source_queue.cpp index a8cf1af797d..c724db5ae10 100644 --- a/src/mongo/db/pipeline/document_source_queue.cpp +++ b/src/mongo/db/pipeline/document_source_queue.cpp @@ -80,13 +80,12 @@ DocumentSource::GetNextResult DocumentSourceQueue::doGetNext() { return next; } -Value DocumentSourceQueue::serialize(const SerializationOptions& opts) const { +Value DocumentSourceQueue::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { ValueArrayStream vals; for (auto elem : _queue) { vals << elem.getDocument().getOwned(); } - // We treat the queue's documents as one literal in the context of redaction. - return Value(DOC(kStageName << opts.serializeLiteral(vals.done()))); + return Value(DOC(kStageName << vals.done())); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_queue.h b/src/mongo/db/pipeline/document_source_queue.h index f170cca27d9..31dc128f6cb 100644 --- a/src/mongo/db/pipeline/document_source_queue.h +++ b/src/mongo/db/pipeline/document_source_queue.h @@ -55,7 +55,7 @@ public: const char* getSourceName() const override; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const override; StageConstraints constraints(Pipeline::SplitState pipeState) const override { StageConstraints constraints{StreamType::kStreaming, @@ -75,7 +75,7 @@ public: * This stage does not modify anything. */ GetModPathsReturn getModifiedPaths() const override { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } /** diff --git a/src/mongo/db/pipeline/document_source_queue_test.cpp b/src/mongo/db/pipeline/document_source_queue_test.cpp index 2833826ddbf..b7014bd3166 100644 --- a/src/mongo/db/pipeline/document_source_queue_test.cpp +++ b/src/mongo/db/pipeline/document_source_queue_test.cpp @@ -73,19 +73,10 @@ TEST_F(QueueStageTest, QueueStageSerialize) { ASSERT_TRUE(queueStage); - auto res = queueStage->serialize(); + auto res = queueStage->serialize(boost::none); ASSERT_VALUE_EQ(res, Value{DOC("$queue" << DOC_ARRAY(DOC("a1" << 1) << DOC("a2" << 2)))}); } -TEST_F(QueueStageTest, RedactsCorrectly) { - auto queueDoc = BSON("$queue" << BSON_ARRAY(BSON("a" << 1))); - auto queueStage = DocumentSourceQueue::createFromBson(queueDoc.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$queue":"?array<?object>"})", - redact(*queueStage)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_redact.cpp b/src/mongo/db/pipeline/document_source_redact.cpp index b2d4a64cccb..80dc406c620 100644 --- a/src/mongo/db/pipeline/document_source_redact.cpp +++ b/src/mongo/db/pipeline/document_source_redact.cpp @@ -169,8 +169,8 @@ intrusive_ptr<DocumentSource> DocumentSourceRedact::optimize() { return this; } -Value DocumentSourceRedact::serialize(const SerializationOptions& opts) const { - return Value(DOC(getSourceName() << _expression.get()->serialize(opts))); +Value DocumentSourceRedact::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(getSourceName() << _expression.get()->serialize(static_cast<bool>(explain)))); } intrusive_ptr<DocumentSource> DocumentSourceRedact::createFromBson( diff --git a/src/mongo/db/pipeline/document_source_redact.h b/src/mongo/db/pipeline/document_source_redact.h index 84f1f262f5b..10fcfc14f63 100644 --- a/src/mongo/db/pipeline/document_source_redact.h +++ b/src/mongo/db/pipeline/document_source_redact.h @@ -66,22 +66,12 @@ public: static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; boost::intrusive_ptr<Expression> getExpression() { return _expression; } - DepsTracker::State getDependencies(DepsTracker* deps) const final { - // Add the dependencies of the expression but all we really care about is variable - // references for correlation analysis. The field references may get populated but we'll - // still require the full document since the $redact may descend arbitrary levels of nested - // documents that is only known at runtime. - _expression->addDependencies(deps); - deps->needWholeDocument = true; - return DepsTracker::State::SEE_NEXT; - } - private: DocumentSourceRedact(const boost::intrusive_ptr<ExpressionContext>& expCtx, const boost::intrusive_ptr<Expression>& previsit); diff --git a/src/mongo/db/pipeline/document_source_redact_test.cpp b/src/mongo/db/pipeline/document_source_redact_test.cpp index 6605796138b..95d001e4dac 100644 --- a/src/mongo/db/pipeline/document_source_redact_test.cpp +++ b/src/mongo/db/pipeline/document_source_redact_test.cpp @@ -81,26 +81,5 @@ TEST_F(DocumentSourceRedactTest, ShouldPropagatePauses) { ASSERT_TRUE(redact->getNext().isEOF()); ASSERT_TRUE(redact->getNext().isEOF()); } - -TEST_F(DocumentSourceRedactTest, ReportsVariableDependencies) { - auto varId = getExpCtx()->variablesParseState.defineVariable("var"); - auto redactSpec = fromjson(R"({ - "$redact" : { - "$cond" : { - "if" : "$$var", - "then" : "$$PRUNE", - "else" : "$$DESCEND" - } - } - })"); - auto redact = DocumentSourceRedact::createFromBson(redactSpec.firstElement(), getExpCtx()); - - DepsTracker deps; - ASSERT_EQ(redact->getDependencies(&deps), DepsTracker::State::SEE_NEXT); - ASSERT_EQ(deps.needWholeDocument, true); - ASSERT_TRUE(deps.fields.empty()); - ASSERT_EQ(deps.vars.count(varId), 1); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_replace_root.h b/src/mongo/db/pipeline/document_source_replace_root.h index a796cb069f4..e302ce5d88c 100644 --- a/src/mongo/db/pipeline/document_source_replace_root.h +++ b/src/mongo/db/pipeline/document_source_replace_root.h @@ -60,9 +60,9 @@ public: _newRoot->optimize(); } - Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options = {}) const final { - return Document{{"newRoot", _newRoot->serialize(options)}}; + Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const final { + return Document{{"newRoot", _newRoot->serialize(static_cast<bool>(explain))}}; } DepsTracker::State addDependencies(DepsTracker* deps) const final { @@ -74,7 +74,7 @@ public: DocumentSource::GetModPathsReturn getModifiedPaths() const final { // Replaces the entire root, so all paths are modified. - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; } const boost::intrusive_ptr<Expression>& getExpression() const { diff --git a/src/mongo/db/pipeline/document_source_sample.cpp b/src/mongo/db/pipeline/document_source_sample.cpp index 40d5f47e3ad..5ec204d1fa9 100644 --- a/src/mongo/db/pipeline/document_source_sample.cpp +++ b/src/mongo/db/pipeline/document_source_sample.cpp @@ -52,10 +52,8 @@ REGISTER_DOCUMENT_SOURCE(sample, AllowedWithApiStrict::kAlways); DocumentSource::GetNextResult DocumentSourceSample::doGetNext() { - if (_size == 0) { - pSource->dispose(); + if (_size == 0) return GetNextResult::makeEOF(); - } if (!_sortStage->isPopulated()) { // Exhaust source stage, add random metadata, and push all into sorter. @@ -83,8 +81,8 @@ DocumentSource::GetNextResult DocumentSourceSample::doGetNext() { return _sortStage->getNext(); } -Value DocumentSourceSample::serialize(const SerializationOptions& opts) const { - return Value(DOC(kStageName << DOC("size" << opts.serializeLiteral(_size)))); +Value DocumentSourceSample::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(kStageName << DOC("size" << _size))); } namespace { diff --git a/src/mongo/db/pipeline/document_source_sample.h b/src/mongo/db/pipeline/document_source_sample.h index 577043c3ec0..7dd31db3c41 100644 --- a/src/mongo/db/pipeline/document_source_sample.h +++ b/src/mongo/db/pipeline/document_source_sample.h @@ -41,7 +41,7 @@ public: const char* getSourceName() const final { return kStageName.rawData(); } - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { return {StreamType::kBlocking, diff --git a/src/mongo/db/pipeline/document_source_sample_from_random_cursor.cpp b/src/mongo/db/pipeline/document_source_sample_from_random_cursor.cpp index 08716b4fac5..1105966e3ac 100644 --- a/src/mongo/db/pipeline/document_source_sample_from_random_cursor.cpp +++ b/src/mongo/db/pipeline/document_source_sample_from_random_cursor.cpp @@ -143,8 +143,9 @@ DocumentSource::GetNextResult DocumentSourceSampleFromRandomCursor::getNextNonDu "sporadic failure, please try again."); } -Value DocumentSourceSampleFromRandomCursor::serialize(const SerializationOptions& opts) const { - return Value(DOC(getSourceName() << DOC("size" << opts.serializeLiteral(_size)))); +Value DocumentSourceSampleFromRandomCursor::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(getSourceName() << DOC("size" << _size))); } DepsTracker::State DocumentSourceSampleFromRandomCursor::getDependencies(DepsTracker* deps) const { diff --git a/src/mongo/db/pipeline/document_source_sample_from_random_cursor.h b/src/mongo/db/pipeline/document_source_sample_from_random_cursor.h index 4e7a73f0445..65521488158 100644 --- a/src/mongo/db/pipeline/document_source_sample_from_random_cursor.h +++ b/src/mongo/db/pipeline/document_source_sample_from_random_cursor.h @@ -42,7 +42,7 @@ class DocumentSourceSampleFromRandomCursor final : public DocumentSource { public: static constexpr StringData kStageName = "$sampleFromRandomCursor"_sd; const char* getSourceName() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; DepsTracker::State getDependencies(DepsTracker* deps) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { diff --git a/src/mongo/db/pipeline/document_source_sample_test.cpp b/src/mongo/db/pipeline/document_source_sample_test.cpp index ade80752010..68cd93c4698 100644 --- a/src/mongo/db/pipeline/document_source_sample_test.cpp +++ b/src/mongo/db/pipeline/document_source_sample_test.cpp @@ -188,17 +188,6 @@ TEST_F(SampleBasics, ShouldPropagatePauses) { assertEOF(); } -TEST_F(SampleBasics, RedactsCorrectly) { - createSample(10); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$sample": { - "size": "?number" - } - })", - redact(*sample())); -} - /** * Fixture to test error cases of the $sample stage. */ @@ -413,12 +402,5 @@ DEATH_TEST_REGEX_F(SampleFromRandomCursorBasics, sample()->getNext(); } -TEST_F(SampleFromRandomCursorBasics, RedactsCorrectly) { - createSample(2); - ASSERT_VALUE_EQ_AUTO( // NOLINT - "{ $sampleFromRandomCursor: { size: \"?number\" } }", - redact(*sample())); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp b/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp index 535c3f5d2be..33c32f096b3 100644 --- a/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp +++ b/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp @@ -41,6 +41,7 @@ DocumentSourceSequentialDocumentCache::DocumentSourceSequentialDocumentCache( const boost::intrusive_ptr<ExpressionContext>& expCtx, SequentialDocumentCache* cache) : DocumentSource(kStageName, expCtx), _cache(cache) { invariant(_cache); + invariant(!_cache->isAbandoned()); if (_cache->isServing()) { _cache->restartIteration(); @@ -80,10 +81,9 @@ DocumentSource::GetNextResult DocumentSourceSequentialDocumentCache::doGetNext() Pipeline::SourceContainer::iterator DocumentSourceSequentialDocumentCache::doOptimizeAt( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - // The DocumentSourceSequentialDocumentCache relies on all other stages in the pipeline being at - // the final positions which they would have occupied if no cache stage was present. This should - // be the case when we reach this function. The cache should always be the last stage in the - // pipeline pre-optimizing. + // The DocumentSourceSequentialDocumentCache should always be the last stage in the pipeline + // pre-optimization. By the time optimization reaches this point, all preceding stages are in + // the final positions which they would have occupied if no cache stage was present. invariant(_hasOptimizedPos || std::next(itr) == container->end()); invariant((*itr).get() == this); @@ -96,7 +96,7 @@ Pipeline::SourceContainer::iterator DocumentSourceSequentialDocumentCache::doOpt _hasOptimizedPos = true; // If the cache is the only stage in the pipeline, return immediately. - if (itr == container->begin() && std::next(itr) == container->end()) { + if (itr == container->begin()) { return container->end(); } @@ -153,12 +153,12 @@ Pipeline::SourceContainer::iterator DocumentSourceSequentialDocumentCache::doOpt return container->end(); } -Value DocumentSourceSequentialDocumentCache::serialize(const SerializationOptions& opts) const { - if (opts.verbosity) { +Value DocumentSourceSequentialDocumentCache::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { return Value(Document{ {kStageName, - Document{{"maxSizeBytes"_sd, - opts.serializeLiteral(static_cast<long long>(_cache->maxSizeBytes()))}, + Document{{"maxSizeBytes"_sd, Value(static_cast<long long>(_cache->maxSizeBytes()))}, {"status"_sd, _cache->isBuilding() ? "kBuilding"_sd diff --git a/src/mongo/db/pipeline/document_source_sequential_document_cache.h b/src/mongo/db/pipeline/document_source_sequential_document_cache.h index ea6c99a5f9e..4e81390d4ef 100644 --- a/src/mongo/db/pipeline/document_source_sequential_document_cache.h +++ b/src/mongo/db/pipeline/document_source_sequential_document_cache.h @@ -97,10 +97,6 @@ public: return newStage; } - bool hasOptimizedPos() const { - return _hasOptimizedPos; - } - protected: GetNextResult doGetNext() final; Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, @@ -110,7 +106,7 @@ private: DocumentSourceSequentialDocumentCache(const boost::intrusive_ptr<ExpressionContext>& expCtx, SequentialDocumentCache* cache); - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; SequentialDocumentCache* _cache; diff --git a/src/mongo/db/pipeline/document_source_sequential_document_cache_test.cpp b/src/mongo/db/pipeline/document_source_sequential_document_cache_test.cpp index 36b0f043faf..d4480ca7c57 100644 --- a/src/mongo/db/pipeline/document_source_sequential_document_cache_test.cpp +++ b/src/mongo/db/pipeline/document_source_sequential_document_cache_test.cpp @@ -34,11 +34,9 @@ #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/json.h" -#include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/document_source_mock.h" #include "mongo/db/pipeline/document_source_sequential_document_cache.h" -#include "mongo/unittest/bson_test_util.h" #include "mongo/unittest/unittest.h" namespace mongo { @@ -76,27 +74,5 @@ TEST_F(DocumentSourceSequentialDocumentCacheTest, ReturnsEOFAfterCacheExhausted) ASSERT(documentCache->getNext().isEOF()); ASSERT(documentCache->getNext().isEOF()); } - -TEST_F(DocumentSourceSequentialDocumentCacheTest, Redaction) { - SequentialDocumentCache cache(kDefaultMaxCacheSize); - cache.add(DOC("_id" << 0)); - cache.add(DOC("_id" << 1)); - auto documentCache = DocumentSourceSequentialDocumentCache::create(getExpCtx(), &cache); - std::vector<Value> vals; - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$sequentialCache":{"maxSizeBytes":"?number","status":"kBuilding"}})", - redact(*documentCache, true, ExplainOptions::Verbosity::kQueryPlanner)); - - cache.freeze(); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$sequentialCache":{"maxSizeBytes":"?number","status":"kServing"}})", - redact(*documentCache, true, ExplainOptions::Verbosity::kQueryPlanner)); - - cache.abandon(); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$sequentialCache":{"maxSizeBytes":"?number","status":"kAbandoned"}})", - redact(*documentCache, true, ExplainOptions::Verbosity::kQueryPlanner)); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.cpp b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.cpp index d9c5455f271..a3c3cfaf8ef 100644 --- a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.cpp +++ b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.cpp @@ -54,12 +54,13 @@ REGISTER_INTERNAL_DOCUMENT_SOURCE( // to check the FCV. feature_flags::gFeatureFlagSearchShardedFacets.isEnabledAndIgnoreFCV()); -Value DocumentSourceSetVariableFromSubPipeline::serialize(const SerializationOptions& opts) const { +Value DocumentSourceSetVariableFromSubPipeline::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { const auto var = "$$" + Variables::getBuiltinVariableName(_variableID); SetVariableFromSubPipelineSpec spec; tassert(625298, "SubPipeline cannot be null during serialization", _subPipeline); - spec.setSetVariable(opts.serializeIdentifier(var)); - spec.setPipeline(_subPipeline->serializeToBson(opts)); + spec.setSetVariable(var); + spec.setPipeline(_subPipeline->serializeToBson(explain)); return Value(DOC(getSourceName() << spec.toBSON())); } @@ -132,17 +133,4 @@ void DocumentSourceSetVariableFromSubPipeline::addSubPipelineInitialSource( _subPipeline->addInitialSource(std::move(source)); } -void DocumentSourceSetVariableFromSubPipeline::detachFromOperationContext() { - _subPipeline->detachFromOperationContext(); -} - -void DocumentSourceSetVariableFromSubPipeline::reattachToOperationContext(OperationContext* opCtx) { - _subPipeline->reattachToOperationContext(opCtx); -} - -bool DocumentSourceSetVariableFromSubPipeline::validateOperationContext( - const OperationContext* opCtx) const { - return getContext()->opCtx == opCtx && _subPipeline->validateOperationContext(opCtx); -} - } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h index e06cc9a2333..0fccb527fad 100644 --- a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h +++ b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h @@ -76,7 +76,6 @@ public: } // This stage doesn't modify documents. setVariableConstraints.preservesOrderAndMetadata = true; - setVariableConstraints.canSwapWithSkippingOrLimitingStage = true; return setVariableConstraints; } @@ -96,10 +95,6 @@ public: */ void addSubPipelineInitialSource(boost::intrusive_ptr<DocumentSource> source); - void detachFromOperationContext() final; - void reattachToOperationContext(OperationContext* opCtx) final; - bool validateOperationContext(const OperationContext* opCtx) const final; - protected: DocumentSourceSetVariableFromSubPipeline(const boost::intrusive_ptr<ExpressionContext>& expCtx, std::unique_ptr<Pipeline, PipelineDeleter> subpipeline, @@ -111,7 +106,7 @@ protected: private: GetNextResult doGetNext() final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; std::unique_ptr<Pipeline, PipelineDeleter> _subPipeline; Variables::Id _variableID; // $setVariableFromSubPipeline sets the value of $$SEARCH_META only on the first call to diff --git a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline_test.cpp b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline_test.cpp index 8b30d3c95b7..e7cc8d71a27 100644 --- a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline_test.cpp +++ b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline_test.cpp @@ -158,41 +158,6 @@ TEST_F(DocumentSourceSetVariableFromSubPipelineTest, testDoGetNext) { Value((BSON("d" << 1))), nullptr) == 0); } -TEST_F(DocumentSourceSetVariableFromSubPipelineTest, QueryShape) { - const auto inputDocs = - std::vector{Document{{"a", 1}}, Document{{"b", 1}}, Document{{"c", 1}}, Document{{"d", 1}}}; - auto expCtx = getExpCtx(); - const auto mockSourceForSetVarStage = DocumentSourceMock::createForTest(inputDocs[1], expCtx); - auto ctxForSubPipeline = expCtx->copyForSubPipeline(expCtx->ns); - const auto mockSourceForSubPipeline = - DocumentSourceMock::createForTest(inputDocs, ctxForSubPipeline); - auto setVariableFromSubPipeline = DocumentSourceSetVariableFromSubPipeline::create( - expCtx, - Pipeline::create({DocumentSourceMatch::create(BSON("d" << 1), ctxForSubPipeline)}, - ctxForSubPipeline), - Variables::kSearchMetaId); - setVariableFromSubPipeline->addSubPipelineInitialSource(mockSourceForSubPipeline); - setVariableFromSubPipeline->setSource(mockSourceForSetVarStage.get()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$setVariableFromSubPipeline": { - "setVariable": "HASH<$$SEARCH_META>", - "pipeline": [ - { - "mock": {} - }, - { - "$match": { - "HASH<d>": { - "$eq": "?number" - } - } - } - ] - } - })", - redact(*setVariableFromSubPipeline)); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_set_window_fields.cpp b/src/mongo/db/pipeline/document_source_set_window_fields.cpp index e32fc78c04b..45223bb1cf4 100644 --- a/src/mongo/db/pipeline/document_source_set_window_fields.cpp +++ b/src/mongo/db/pipeline/document_source_set_window_fields.cpp @@ -144,8 +144,8 @@ WindowFunctionStatement WindowFunctionStatement::parse(BSONElement elem, window_function::Expression::parse(elem.embeddedObject(), sortBy, expCtx)); } void WindowFunctionStatement::serialize(MutableDocument& outputFields, - const SerializationOptions& opts) const { - outputFields[opts.serializeFieldPathFromString(fieldName)] = expr->serialize(opts); + boost::optional<ExplainOptions::Verbosity> explain) const { + outputFields[fieldName] = expr->serialize(explain); } list<intrusive_ptr<DocumentSource>> document_source_set_window_fields::create( @@ -218,10 +218,12 @@ list<intrusive_ptr<DocumentSource>> document_source_set_window_fields::create( } else { // In DocumentSource we don't have a mechanism for generating non-colliding field names, // so we have to choose the tmp name carefully to make a collision unlikely in practice. - auto tmp = "__internal_setWindowFields_partition_key"; - simplePartitionBy = FieldPath{tmp}; + std::array<unsigned char, 16> nonce = UUID::gen().data(); + // We encode as a base64 string for a shorter, more performant field name (length 22). + std::string tmpField = base64::encode(nonce.data(), sizeof(nonce)); + simplePartitionBy = FieldPath{tmpField}; simplePartitionByExpr = ExpressionFieldPath::createPathFromString( - expCtx.get(), tmp, expCtx->variablesParseState); + expCtx.get(), tmpField, expCtx->variablesParseState); complexPartitionBy = partitionBy; } } @@ -286,38 +288,39 @@ intrusive_ptr<DocumentSource> DocumentSourceInternalSetWindowFields::optimize() return this; } -Value DocumentSourceInternalSetWindowFields::serialize(const SerializationOptions& opts) const { +Value DocumentSourceInternalSetWindowFields::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument spec; spec[SetWindowFieldsSpec::kPartitionByFieldName] = - _partitionBy ? (*_partitionBy)->serialize(opts) : Value(); + _partitionBy ? (*_partitionBy)->serialize(false) : Value(); - auto sortKeySerialization = opts.verbosity + auto sortKeySerialization = explain ? SortPattern::SortKeySerialization::kForExplain : SortPattern::SortKeySerialization::kForPipelineSerialization; spec[SetWindowFieldsSpec::kSortByFieldName] = - _sortBy ? Value(_sortBy->serialize(sortKeySerialization, opts)) : Value(); + _sortBy ? Value(_sortBy->serialize(sortKeySerialization)) : Value(); MutableDocument output; for (auto&& stmt : _outputFields) { - stmt.serialize(output, opts); + stmt.serialize(output, explain); } spec[SetWindowFieldsSpec::kOutputFieldName] = output.freezeToValue(); MutableDocument out; out[getSourceName()] = Value(spec.freeze()); - if (opts.verbosity && *opts.verbosity >= ExplainOptions::Verbosity::kExecStats) { + if (explain && *explain >= ExplainOptions::Verbosity::kExecStats) { MutableDocument md; for (auto&& [fieldName, function] : _executableOutputs) { - md[opts.serializeFieldPathFromString(fieldName)] = opts.serializeLiteral( - static_cast<long long>(_memoryTracker[fieldName].maxMemoryBytes())); + md[fieldName] = + Value(static_cast<long long>(_memoryTracker[fieldName].maxMemoryBytes())); } out["maxFunctionMemoryUsageBytes"] = Value(md.freezeToValue()); out["maxTotalMemoryUsageBytes"] = - opts.serializeLiteral(static_cast<long long>(_memoryTracker.maxMemoryBytes())); - out["usedDisk"] = opts.serializeLiteral(_iterator.usedDisk()); + Value(static_cast<long long>(_memoryTracker.maxMemoryBytes())); + out["usedDisk"] = Value(_iterator.usedDisk()); } return Value(out.freezeToValue()); @@ -461,12 +464,8 @@ DocumentSource::GetNextResult DocumentSourceInternalSetWindowFields::doGetNext() return DocumentSource::GetNextResult::makeEOF(); auto curDoc = _iterator.current(); + // The only way we hit this case is if there are no documents, since otherwise _eof will be set. if (!curDoc) { - if (_iterator.isPaused()) { - return DocumentSource::GetNextResult::makePauseExecution(); - } - // The only way we hit this case is if there are no documents, since otherwise _eof will be - // set. _eof = true; return DocumentSource::GetNextResult::makeEOF(); } @@ -474,15 +473,13 @@ DocumentSource::GetNextResult DocumentSourceInternalSetWindowFields::doGetNext() // Populate the output document with the result from each window function. auto projSpec = std::make_unique<projection_executor::InclusionNode>( ProjectionPolicies{ProjectionPolicies::DefaultIdPolicy::kIncludeId}); - for (auto&& outputField : _outputFields) { + for (auto&& [fieldName, function] : _executableOutputs) { try { // If we hit a uassert while evaluating expressions on user data, delete the temporary // table before aborting the operation. - auto& fieldName = outputField.fieldName; projSpec->addExpressionForPath( FieldPath(fieldName), - ExpressionConstant::create(pExpCtx.get(), - _executableOutputs[fieldName]->getNext())); + ExpressionConstant::create(pExpCtx.get(), function->getNext())); } catch (const DBException&) { _iterator.finalize(); throw; diff --git a/src/mongo/db/pipeline/document_source_set_window_fields.h b/src/mongo/db/pipeline/document_source_set_window_fields.h index 43af58e2dad..652d1206ec9 100644 --- a/src/mongo/db/pipeline/document_source_set_window_fields.h +++ b/src/mongo/db/pipeline/document_source_set_window_fields.h @@ -70,7 +70,8 @@ struct WindowFunctionStatement { } } - void serialize(MutableDocument& outputFields, const SerializationOptions& opts) const; + void serialize(MutableDocument& outputFields, + boost::optional<ExplainOptions::Verbosity> explain) const; }; /** @@ -116,7 +117,7 @@ public: _iterator(expCtx.get(), pSource, &_memoryTracker, std::move(partitionBy), _sortBy){}; GetModPathsReturn getModifiedPaths() const final { - OrderedPathSet outputPaths; + std::set<std::string> outputPaths; for (auto&& outputField : _outputFields) { outputPaths.insert(outputField.fieldName); } @@ -166,7 +167,7 @@ public: boost::intrusive_ptr<DocumentSource> optimize() final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const; DocumentSource::GetNextResult doGetNext(); diff --git a/src/mongo/db/pipeline/document_source_set_window_fields_test.cpp b/src/mongo/db/pipeline/document_source_set_window_fields_test.cpp index 2f35d4a6b94..287497db445 100644 --- a/src/mongo/db/pipeline/document_source_set_window_fields_test.cpp +++ b/src/mongo/db/pipeline/document_source_set_window_fields_test.cpp @@ -212,342 +212,5 @@ TEST_F(DocumentSourceSetWindowFieldsTest, ReportsModifiedFields) { ASSERT_EQUALS(modified.paths.count("b"), 1U); ASSERT_TRUE(modified.renames.empty()); } - -TEST_F(DocumentSourceSetWindowFieldsTest, RedactionOnShiftOperator) { - auto spec = fromjson( - R"({ - $setWindowFields: { - partitionBy: '$foo', - sortBy: { - bar: 1 - }, - output: { - x: { - $shift: { - output: '$y', - by: 1, - default: 'BAZ' - } - } - } - } - })"); - auto docSource = - DocumentSourceInternalSetWindowFields::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalSetWindowFields": { - "partitionBy": "$HASH<foo>", - "sortBy": { - "HASH<bar>": 1 - }, - "output": { - "HASH<x>": { - "$shift": { - "by": "?number", - "output": "$HASH<y>", - "default": "?string" - } - } - } - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RedactionOnExpMovingAvgOperator) { - auto spec = fromjson( - R"({ - $setWindowFields: { - partitionBy: '$foo.bar', - sortBy: { - bar: 1 - }, - output: { - x: { - $expMovingAvg: { - alpha: 0.5, - input: '$y' - } - } - } - } - })"); - auto docSource = - DocumentSourceInternalSetWindowFields::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalSetWindowFields": { - "partitionBy": "$HASH<foo>.HASH<bar>", - "sortBy": { - "HASH<bar>": 1 - }, - "output": { - "HASH<x>": { - "$expMovingAvg": { - "alpha": "?number", - "input": "$HASH<y>" - } - } - } - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RedactionOnExpressionNOperator) { - auto spec = fromjson( - R"({ - $setWindowFields: { - partitionBy: '$a', - output: { - b: { - $minN: { - n: 3, - input: '$y' - } - } - } - } - })"); - auto docSource = - DocumentSourceInternalSetWindowFields::createFromBson(spec.firstElement(), getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalSetWindowFields": { - "partitionBy": "$HASH<a>", - "output": { - "HASH<b>": { - "$minN": { - "n": "?number", - "input": "$HASH<y>" - }, - "window": { - "documents": [ - "unbounded", - "unbounded" - ] - } - } - } - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RedactionOnSumWithDocumentsWindow) { - auto spec = fromjson(R"({ - $setWindowFields: { - partitionBy: { - $year: '$x' - }, - sortBy: { - a: 1, - b: -1 - }, - output: { - cumulative: { - $sum: '$baz', - window: { - documents: [ - 'unbounded', - 'current' - ] - } - }, - maximum: { - $max: '$baz', - window: { - documents: [ - 'unbounded', - 'unbounded' - ] - } - } - } - } - })"); - auto docSource = - DocumentSourceInternalSetWindowFields::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalSetWindowFields": { - "partitionBy": { - "$year": { - "date": "$HASH<x>" - } - }, - "sortBy": { - "HASH<a>": 1, - "HASH<b>": -1 - }, - "output": { - "HASH<cumulative>": { - "$sum": "$HASH<baz>", - "window": { - "documents": [ - "unbounded", - "current" - ] - } - }, - "HASH<maximum>": { - "$max": "$HASH<baz>", - "window": { - "documents": [ - "unbounded", - "unbounded" - ] - } - } - } - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RedactionOnPushWithRangeWindowWithUnit) { - auto spec = fromjson(R"({ - $setWindowFields: { - partitionBy: '$foo', - sortBy: { - bar: 1 - }, - output: { - a: { - $push: '$b', - window: { - range: [ - 'unbounded', - -10 - ], - unit: 'month' - } - } - } - } - })"); - auto docSource = - DocumentSourceInternalSetWindowFields::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalSetWindowFields": { - "partitionBy": "$HASH<foo>", - "sortBy": { - "HASH<bar>": 1 - }, - "output": { - "HASH<a>": { - "$push": "$HASH<b>", - "window": { - "range": [ - "unbounded", - "?number" - ], - "unit": "month" - } - } - } - } - })", - redact(*docSource)); -} - -/** - * Helper function that parses the $setWindowFields aggregation stage from the input, serializes it - * to its representative shape, re-parses the representative shape, and compares to the original. - */ -void assertRepresentativeShapeIsStable(const boost::intrusive_ptr<ExpressionContext>& expCtx, - BSONObj inputStage, - BSONObj expectedRepresentativeStage) { - auto parsedStage = - DocumentSourceInternalSetWindowFields::createFromBson(inputStage.firstElement(), expCtx); - std::vector<Value> serialization; - auto opts = SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}; - parsedStage->serializeToArray(serialization, opts); - - auto serializedStage = serialization[0].getDocument().toBson(); - ASSERT_BSONOBJ_EQ(serializedStage, expectedRepresentativeStage); - - auto roundTripped = DocumentSourceInternalSetWindowFields::createFromBson( - serializedStage.firstElement(), expCtx); - - std::vector<Value> newSerialization; - roundTripped->serializeToArray(newSerialization, opts); - ASSERT_EQ(newSerialization.size(), 1UL); - ASSERT_VALUE_EQ(newSerialization[0], serialization[0]); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RoundTripSerializationDocumentWindowBounds) { - assertRepresentativeShapeIsStable(getExpCtx(), - fromjson(R"( - {$_internalSetWindowFields: {partitionBy: '$state', sortBy: {city: 1}, output: {mySum: - {$sum: '$pop', window: {documents: [-10, 10]}}}}})"), - fromjson(R"( - {$_internalSetWindowFields: {partitionBy: '$state', sortBy: {city: 1}, output: {mySum: - {$sum: '$pop', window: {documents: [0, 1]}}}}})")); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RoundTripSerializationRangeWindowBounds) { - assertRepresentativeShapeIsStable(getExpCtx(), - fromjson(R"( - {$_internalSetWindowFields: {partitionBy: '$state', sortBy: {city: 1}, output: {mySum: - {$sum: '$pop', window: {range: [-10, 10]}}}}})"), - fromjson(R"( - {$_internalSetWindowFields: {partitionBy: '$state', sortBy: {city: 1}, output: {mySum: - {$sum: '$pop', window: {range: [0, 1]}}}}})")); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RoundTripSerializationRangeWindowBoundsWithUnit) { - assertRepresentativeShapeIsStable(getExpCtx(), - fromjson(R"( - {$_internalSetWindowFields: {partitionBy: '$state', sortBy: {city: 1}, output: {mySum: - {$sum: '$pop', window: {range: [-10, 10], unit: 'second'}}}}})"), - fromjson(R"( - {$_internalSetWindowFields: {partitionBy: '$state', sortBy: {city: 1}, output: {mySum: - {$sum: '$pop', window: {range: [0, 1], unit: 'second'}}}}})")); -} - -TEST_F(DocumentSourceSetWindowFieldsTest, RoundTripSerializationExpMovingAvg) { - assertRepresentativeShapeIsStable(getExpCtx(), - fromjson( - R"({ - $setWindowFields: { - partitionBy: '$foo.bar', - sortBy: { - bar: 1 - }, - output: { - x: { - $expMovingAvg: { - alpha: 0.5, - input: '$y' - } - } - } - } - })"), - fromjson( - R"({ - $_internalSetWindowFields: { - partitionBy: '$foo.bar', - sortBy: { - bar: 1 - }, - output: { - x: { - $expMovingAvg: { - alpha: 0.1, - input: '$y' - } - } - } - } - })")); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp deleted file mode 100644 index ff9326ceae4..00000000000 --- a/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/db/pipeline/document_source_sharded_data_distribution.h" - -#include "mongo/db/pipeline/document_source_group.h" -#include "mongo/db/pipeline/document_source_internal_all_collection_stats.h" -#include "mongo/db/pipeline/document_source_lookup.h" -#include "mongo/db/pipeline/document_source_match.h" -#include "mongo/db/pipeline/document_source_project.h" -#include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/s/catalog/type_collection.h" - -namespace mongo { - -using boost::intrusive_ptr; -using std::list; - -REGISTER_DOCUMENT_SOURCE(shardedDataDistribution, - DocumentSourceShardedDataDistribution::LiteParsed::parse, - DocumentSourceShardedDataDistribution::createFromBson, - AllowedWithApiStrict::kAlways); - -list<intrusive_ptr<DocumentSource>> DocumentSourceShardedDataDistribution::createFromBson( - BSONElement elem, const intrusive_ptr<ExpressionContext>& expCtx) { - uassert(6789100, - "The $shardedDataDistribution stage specification must be an empty object", - elem.type() == Object && elem.Obj().isEmpty()); - - uassert( - 6789101, "The $shardedDataDistribution stage can only be run on mongoS", expCtx->inMongos); - - uassert(6789102, - "The $shardedDataDistribution stage must be run on the admin database", - expCtx->ns.isAdminDB() && expCtx->ns.isCollectionlessAggregateNS()); - - static const BSONObj kAllCollStatsObj = - fromjson("{$_internalAllCollectionStats: {stats: {storageStats: {}}}}}"); - static const BSONObj kGroupObj = fromjson(R"({ - $group: { - _id: "$ns", - shards: { - $push: { - $let: { - vars: { - nOwnedDocs: { - $subtract: [ - "$storageStats.count", - "$storageStats.numOrphanDocs" - ] - } - }, - in: { - shardName: "$shard", - numOrphanedDocs: "$storageStats.numOrphanDocs", - numOwnedDocuments: "$$nOwnedDocs", - ownedSizeBytes: { - $multiply: [ - "$storageStats.avgObjSize", - "$$nOwnedDocs" - ] - }, - orphanedSizeBytes: { - $multiply: [ - "$storageStats.avgObjSize", - "$storageStats.numOrphanDocs" - ] - } - } - } - } - } - } - })"); - static const BSONObj kLookupObj = fromjson(R"({ - $lookup: { - from: { - db: "config", - coll: "collections" - }, - localField: "_id", - foreignField: "_id", - as: "matchingShardedCollection" - } - })"); - static const BSONObj kMatchObj = fromjson("{$match: {matchingShardedCollection: {$ne: []}}}"); - static const BSONObj kProjectObj = fromjson(R"({ - $project: { - _id: 0, - ns: "$_id", - shards: "$shards" - } - })"); - - return {DocumentSourceInternalAllCollectionStats::createFromBsonInternal( - kAllCollStatsObj.firstElement(), expCtx), - DocumentSourceGroup::createFromBson(kGroupObj.firstElement(), expCtx), - DocumentSourceLookUp::createFromBson(kLookupObj.firstElement(), expCtx), - DocumentSourceMatch::createFromBson(kMatchObj.firstElement(), expCtx), - DocumentSourceProject::createFromBson(kProjectObj.firstElement(), expCtx)}; -} -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.h b/src/mongo/db/pipeline/document_source_sharded_data_distribution.h deleted file mode 100644 index 23705cf6aeb..00000000000 --- a/src/mongo/db/pipeline/document_source_sharded_data_distribution.h +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include "mongo/db/pipeline/document_source.h" - -namespace mongo { - -/** - * This aggregation stage is an alias for ‘$shardedDataDistribution’. It takes no arguments. Its - * response will be a cursor, each document of which represents the data-distribution information - * for a particular collection. - */ -namespace DocumentSourceShardedDataDistribution { - -static constexpr StringData kStageName = "$shardedDataDistribution"_sd; - -class LiteParsed final : public LiteParsedDocumentSource { -public: - static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, const BSONElement& spec) { - return std::make_unique<LiteParsed>(spec.fieldName()); - } - - explicit LiteParsed(std::string parseTimeName) - : LiteParsedDocumentSource(std::move(parseTimeName)) {} - - stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { - return {NamespaceString::kConfigsvrCollectionsNamespace}; - } - - PrivilegeVector requiredPrivileges(bool isMongos, bool bypassDocumentValidation) const final { - return { - Privilege(ResourcePattern::forClusterResource(), ActionType::shardedDataDistribution)}; - } - - bool isInitialSource() const final { - return true; - } -}; - -static std::list<boost::intrusive_ptr<DocumentSource>> createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); - -}; // namespace DocumentSourceShardedDataDistribution - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_single_document_transformation.cpp b/src/mongo/db/pipeline/document_source_single_document_transformation.cpp index 33820b0ceaf..c72c338ab91 100644 --- a/src/mongo/db/pipeline/document_source_single_document_transformation.cpp +++ b/src/mongo/db/pipeline/document_source_single_document_transformation.cpp @@ -64,7 +64,6 @@ DocumentSource::GetNextResult DocumentSourceSingleDocumentTransformation::doGetN // Get the next input document. auto input = pSource->getNext(); - if (!input.isAdvanced()) { return input; } @@ -89,11 +88,10 @@ void DocumentSourceSingleDocumentTransformation::doDispose() { } Value DocumentSourceSingleDocumentTransformation::serialize( - const SerializationOptions& opts) const { - return Value( - Document{{getSourceName(), - _parsedTransform ? _parsedTransform->serializeTransformation(opts.verbosity, opts) - : _cachedStageOptions}}); + boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(Document{{getSourceName(), + _parsedTransform ? _parsedTransform->serializeTransformation(explain) + : _cachedStageOptions}}); } Pipeline::SourceContainer::iterator DocumentSourceSingleDocumentTransformation::doOptimizeAt( diff --git a/src/mongo/db/pipeline/document_source_single_document_transformation.h b/src/mongo/db/pipeline/document_source_single_document_transformation.h index b3cc1538bfe..db76f149213 100644 --- a/src/mongo/db/pipeline/document_source_single_document_transformation.h +++ b/src/mongo/db/pipeline/document_source_single_document_transformation.h @@ -45,7 +45,7 @@ namespace mongo { class DocumentSourceSingleDocumentTransformation final : public DocumentSource { public: virtual boost::intrusive_ptr<DocumentSource> clone( - const boost::intrusive_ptr<ExpressionContext>& newExpCtx) const { + const boost::intrusive_ptr<ExpressionContext>& newExpCtx = nullptr) const { auto list = DocumentSource::parse(newExpCtx ? newExpCtx : pExpCtx, serialize().getDocument().toBson()); invariant(list.size() == 1); @@ -62,7 +62,7 @@ public: const char* getSourceName() const final; boost::intrusive_ptr<DocumentSource> optimize() final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; DepsTracker::State getDependencies(DepsTracker* deps) const final; GetModPathsReturn getModifiedPaths() const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { diff --git a/src/mongo/db/pipeline/document_source_skip.cpp b/src/mongo/db/pipeline/document_source_skip.cpp index 1a9ea0cd6c9..df7537fd1dd 100644 --- a/src/mongo/db/pipeline/document_source_skip.cpp +++ b/src/mongo/db/pipeline/document_source_skip.cpp @@ -72,8 +72,8 @@ DocumentSource::GetNextResult DocumentSourceSkip::doGetNext() { return pSource->getNext(); } -Value DocumentSourceSkip::serialize(const SerializationOptions& opts) const { - return Value(DOC(getSourceName() << opts.serializeLiteral(_nToSkip))); +Value DocumentSourceSkip::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(getSourceName() << _nToSkip)); } intrusive_ptr<DocumentSource> DocumentSourceSkip::optimize() { diff --git a/src/mongo/db/pipeline/document_source_skip.h b/src/mongo/db/pipeline/document_source_skip.h index 667ab7cd85b..89dbeb95b11 100644 --- a/src/mongo/db/pipeline/document_source_skip.h +++ b/src/mongo/db/pipeline/document_source_skip.h @@ -73,7 +73,7 @@ public: Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; boost::intrusive_ptr<DocumentSource> optimize() final; diff --git a/src/mongo/db/pipeline/document_source_skip_test.cpp b/src/mongo/db/pipeline/document_source_skip_test.cpp index 7e54cbdde7c..1de0df02324 100644 --- a/src/mongo/db/pipeline/document_source_skip_test.cpp +++ b/src/mongo/db/pipeline/document_source_skip_test.cpp @@ -103,12 +103,5 @@ TEST_F(DocumentSourceSkipTest, SkipsChainedTogetherShouldNotOverFlowWhenOptimizi ASSERT_EQUALS(containerOptimized.size(), 1U); ASSERT_EQUALS(skipFirst->getSkip(), 2); } - -TEST_F(DocumentSourceSkipTest, Redaction) { - auto stage = DocumentSourceSkip::create(getExpCtx(), 1337); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"$skip":"?number"})", - redact(*stage)); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sort.cpp b/src/mongo/db/pipeline/document_source_sort.cpp index 235f98039df..2c07c43c501 100644 --- a/src/mongo/db/pipeline/document_source_sort.cpp +++ b/src/mongo/db/pipeline/document_source_sort.cpp @@ -44,7 +44,6 @@ #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/pipeline/skip_and_limit.h" #include "mongo/db/query/collation/collation_index_key.h" -#include "mongo/db/query/query_shape/serialization_options.h" #include "mongo/db/stats/resource_consumption_metrics.h" #include "mongo/logv2/log.h" #include "mongo/platform/overflow_arithmetic.h" @@ -69,10 +68,10 @@ struct BoundMakerMin { doc.metadata().getTimeseriesBucketMinTime().toMillisSinceEpoch() + offset)}; } - Document serialize(const SerializationOptions& opts) const { + Document serialize() const { // Convert from millis to seconds. return Document{{{"base"_sd, DocumentSourceSort::kMin}, - {DocumentSourceSort::kOffset, opts.serializeLiteral(offset / 1000)}}}; + {DocumentSourceSort::kOffset, (offset / 1000)}}}; } }; @@ -85,10 +84,10 @@ struct BoundMakerMax { doc.metadata().getTimeseriesBucketMaxTime().toMillisSinceEpoch() + offset)}; } - Document serialize(const SerializationOptions& opts) const { + Document serialize() const { // Convert from millis to seconds. return Document{{{"base"_sd, DocumentSourceSort::kMax}, - {DocumentSourceSort::kOffset, opts.serializeLiteral(offset / 1000)}}}; + {DocumentSourceSort::kOffset, (offset / 1000)}}}; } }; struct CompAsc { @@ -143,19 +142,13 @@ REGISTER_DOCUMENT_SOURCE(sort, LiteParsedDocumentSourceDefault::parse, DocumentSourceSort::createFromBson, AllowedWithApiStrict::kAlways); - REGISTER_DOCUMENT_SOURCE_CONDITIONALLY( _internalBoundedSort, LiteParsedDocumentSourceDefault::parse, DocumentSourceSort::parseBoundedSort, - ::mongo::getTestCommandsEnabled() ? AllowedWithApiStrict::kNeverInVersion1 - : AllowedWithApiStrict::kInternal, - ::mongo::getTestCommandsEnabled() ? AllowedWithClientType::kAny - : AllowedWithClientType::kInternal, - // We don't expect mongos or clients to produce this stage: - // We only generate it after multiplanning, which means only within one mongod process. - // So, we should be allowed to parse this stage regardless of FCV. - boost::none /*minVersion*/, + AllowedWithApiStrict::kNeverInVersion1, + AllowedWithClientType::kAny, + feature_flags::gFeatureFlagBucketUnpackWithSort.getVersion(), feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV()); DocumentSource::GetNextResult::ReturnStatus DocumentSourceSort::timeSorterPeek() { @@ -287,10 +280,8 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceSort::clone( _sortExecutor->getMaxMemoryBytes()); } -void DocumentSourceSort::serializeToArray(std::vector<Value>& array, - const SerializationOptions& opts) const { - auto explain = opts.verbosity; - +void DocumentSourceSort::serializeToArray( + std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { if (_timeSorter) { tassert(6369900, "$_internalBoundedSort should not absorb a $limit", @@ -298,22 +289,20 @@ void DocumentSourceSort::serializeToArray(std::vector<Value>& array, // {$_internalBoundedSort: {sortKey, bound}} auto sortKey = _sortExecutor->sortPattern().serialize( - SortPattern::SortKeySerialization::kForPipelineSerialization, opts); + SortPattern::SortKeySerialization::kForPipelineSerialization); MutableDocument mutDoc{Document{{ {"$_internalBoundedSort"_sd, Document{{{"sortKey"_sd, std::move(sortKey)}, - {"bound"_sd, _timeSorter->serializeBound(opts)}, - {"limit"_sd, - opts.serializeLiteral(static_cast<long long>(_timeSorter->limit()))}}}}, + {"bound"_sd, _timeSorter->serializeBound()}, + {"limit"_sd, static_cast<long long>(_timeSorter->limit())}}}}, }}}; if (explain >= ExplainOptions::Verbosity::kExecStats) { mutDoc["totalDataSizeSortedBytesEstimate"] = - opts.serializeLiteral(static_cast<long long>(_timeSorter->totalDataSizeBytes())); - mutDoc["usedDisk"] = opts.serializeLiteral(_timeSorter->stats().spilledRanges() > 0); - mutDoc["spills"] = - opts.serializeLiteral(static_cast<long long>(_timeSorter->stats().spilledRanges())); + Value(static_cast<long long>(_timeSorter->totalDataSizeBytes())); + mutDoc["usedDisk"] = Value(_timeSorter->numSpills() > 0); + mutDoc["spills"] = Value(static_cast<long long>(_timeSorter->numSpills())); } array.push_back(Value{mutDoc.freeze()}); @@ -324,31 +313,31 @@ void DocumentSourceSort::serializeToArray(std::vector<Value>& array, if (!explain) { // one Value for $sort and maybe a Value for $limit MutableDocument inner(_sortExecutor->sortPattern().serialize( - SortPattern::SortKeySerialization::kForPipelineSerialization, opts)); + SortPattern::SortKeySerialization::kForPipelineSerialization)); array.push_back(Value(DOC(kStageName << inner.freeze()))); if (_sortExecutor->hasLimit()) { auto limitSrc = DocumentSourceLimit::create(pExpCtx, limit); - limitSrc->serializeToArray(array, opts); + limitSrc->serializeToArray(array); } return; } - MutableDocument mutDoc(DOC( - kStageName << DOC("sortKey" << _sortExecutor->sortPattern().serialize( - SortPattern::SortKeySerialization::kForExplain, opts) - << "limit" - << (_sortExecutor->hasLimit() - ? opts.serializeLiteral(static_cast<long long>(limit)) - : Value())))); + MutableDocument mutDoc( + DOC(kStageName << DOC("sortKey" + << _sortExecutor->sortPattern().serialize( + SortPattern::SortKeySerialization::kForExplain) + << "limit" + << (_sortExecutor->hasLimit() ? Value(static_cast<long long>(limit)) + : Value())))); if (explain >= ExplainOptions::Verbosity::kExecStats) { auto& stats = _sortExecutor->stats(); mutDoc["totalDataSizeSortedBytesEstimate"] = - opts.serializeLiteral(static_cast<long long>(stats.totalDataSizeBytes)); - mutDoc["usedDisk"] = opts.serializeLiteral(stats.spills > 0); - mutDoc["spills"] = opts.serializeLiteral(static_cast<long long>(stats.spills)); + Value(static_cast<long long>(stats.totalDataSizeBytes)); + mutDoc["usedDisk"] = Value(stats.spills > 0); + mutDoc["spills"] = Value(static_cast<long long>(stats.spills)); } array.push_back(Value(mutDoc.freeze())); @@ -493,10 +482,6 @@ intrusive_ptr<DocumentSourceSort> DocumentSourceSort::parseBoundedSort( BSONElement key = args["sortKey"]; uassert(6369904, "$_internalBoundedSort sortKey must be an object", key.type() == Object); - // Empty sort pattern is not allowed for the bounded sort. - uassert(6900501, - "$_internalBoundedSort stage must have at least one sort key", - !key.embeddedObject().isEmpty()); SortPattern pat{key.embeddedObject(), expCtx}; { @@ -664,7 +649,7 @@ boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceSort::distri } bool DocumentSourceSort::canRunInParallelBeforeWriteStage( - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const { + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const { // This is an interesting special case. If there are no further stages which require merging the // streams into one, a $sort should not require it. This is only the case because the sort order // doesn't matter for a pipeline ending with a write stage. We may encounter it here as an diff --git a/src/mongo/db/pipeline/document_source_sort.h b/src/mongo/db/pipeline/document_source_sort.h index d28e29a77ea..f0ab0c28a3c 100644 --- a/src/mongo/db/pipeline/document_source_sort.h +++ b/src/mongo/db/pipeline/document_source_sort.h @@ -72,14 +72,14 @@ public: void serializeToArray( std::vector<Value>& array, - const SerializationOptions& opts = SerializationOptions{}) const final override; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; boost::intrusive_ptr<DocumentSource> clone( const boost::intrusive_ptr<ExpressionContext>& newExpCtx) const final; GetModPathsReturn getModifiedPaths() const final { // A $sort does not modify any paths. - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } StageConstraints constraints(Pipeline::SplitState) const final { @@ -102,7 +102,7 @@ public: boost::optional<DistributedPlanLogic> distributedPlanLogic() final; bool canRunInParallelBeforeWriteStage( - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const final; + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const final; /** * Returns the sort key pattern. @@ -201,8 +201,8 @@ private: uint64_t limit, uint64_t maxMemoryUsageBytes); - Value serialize(const SerializationOptions& opts) const final override { - MONGO_UNREACHABLE_TASSERT(7484302); // Should call serializeToArray instead. + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final { + MONGO_UNREACHABLE; // Should call serializeToArray instead. } /** diff --git a/src/mongo/db/pipeline/document_source_sort_by_count_test.cpp b/src/mongo/db/pipeline/document_source_sort_by_count_test.cpp index df92e9b5e07..d4ab7f15c55 100644 --- a/src/mongo/db/pipeline/document_source_sort_by_count_test.cpp +++ b/src/mongo/db/pipeline/document_source_sort_by_count_test.cpp @@ -70,8 +70,7 @@ public: // Serialize the DocumentSourceGroup and DocumentSourceSort from $sortByCount so that we can // check the explain output to make sure $group and $sort have the correct fields. - const auto explain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; + const auto explain = ExplainOptions::Verbosity::kQueryPlanner; vector<Value> explainedStages; groupStage->serializeToArray(explainedStages, explain); sortStage->serializeToArray(explainedStages, explain); diff --git a/src/mongo/db/pipeline/document_source_sort_test.cpp b/src/mongo/db/pipeline/document_source_sort_test.cpp index 31c6c9323a5..013ef9b6a53 100644 --- a/src/mongo/db/pipeline/document_source_sort_test.cpp +++ b/src/mongo/db/pipeline/document_source_sort_test.cpp @@ -27,7 +27,6 @@ * it in the license file. */ -#include "mongo/db/query/explain_options.h" #include "mongo/platform/basic.h" #include <boost/intrusive_ptr.hpp> @@ -539,79 +538,5 @@ TEST_F(DocumentSourceSortExecutionTest, ShouldCorrectlyTrackMemoryUsageBetweenPa sort->getNext(), AssertionException, ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed); } -TEST_F(DocumentSourceSortTest, Redaction) { - createSort(BSON("a" << 1)); - auto boundedSort = DocumentSourceSort::createBoundedSort( - sort()->getSortKeyPattern(), DocumentSourceSort::kMin, 1337, 10, getExpCtx()); - - ASSERT_BSONOBJ_EQ_AUTO( // - R"({"$sort":{"HASH<a>":1}})", - redact(*sort(), true)); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalBoundedSort": { - "sortKey": { - "HASH<a>": 1 - }, - "bound": { - "base": "min", - "offsetSeconds": "?number" - }, - "limit": "?number" - } - })", - redact(*boundedSort, true)); - - ASSERT_BSONOBJ_EQ_AUTO( // - R"({"$sort":{"sortKey":{"HASH<a>":1}}})", - redact(*sort(), true, ExplainOptions::Verbosity::kQueryPlanner)); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalBoundedSort": { - "sortKey": { - "HASH<a>": 1 - }, - "bound": { - "base": "min", - "offsetSeconds": "?number" - }, - "limit": "?number" - } - })", - redact(*boundedSort, true, ExplainOptions::Verbosity::kQueryPlanner)); - - ASSERT_BSONOBJ_EQ_AUTO( // - R"({ - "$sort": { - "sortKey": { - "HASH<a>": 1 - } - }, - "totalDataSizeSortedBytesEstimate": "?number", - "usedDisk": "?bool", - "spills": "?number" - })", - redact(*sort(), true, ExplainOptions::Verbosity::kExecStats)); - - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$_internalBoundedSort": { - "sortKey": { - "HASH<a>": 1 - }, - "bound": { - "base": "min", - "offsetSeconds": "?number" - }, - "limit": "?number" - }, - "totalDataSizeSortedBytesEstimate": "?number", - "usedDisk": "?bool", - "spills": "?number" - })", - redact(*boundedSort, true, ExplainOptions::Verbosity::kExecStats)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_streaming_group.cpp b/src/mongo/db/pipeline/document_source_streaming_group.cpp deleted file mode 100644 index 0aeae62201a..00000000000 --- a/src/mongo/db/pipeline/document_source_streaming_group.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/platform/basic.h" - -#include <memory> - -#include "mongo/db/exec/document_value/document.h" -#include "mongo/db/exec/document_value/value.h" -#include "mongo/db/exec/document_value/value_comparator.h" -#include "mongo/db/pipeline/accumulation_statement.h" -#include "mongo/db/pipeline/accumulator.h" -#include "mongo/db/pipeline/document_source_group.h" -#include "mongo/db/pipeline/document_source_streaming_group.h" -#include "mongo/db/pipeline/expression.h" -#include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/stats/resource_consumption_metrics.h" -#include "mongo/util/destructor_guard.h" - -namespace mongo { - -/* - * $_internalStreamingGroup is an internal stage that is only used in certain cases by the - * pipeline optimizer. For now it should not be used anywhere outside the MongoDB server. - */ -REGISTER_DOCUMENT_SOURCE(_internalStreamingGroup, - LiteParsedDocumentSourceDefault::parse, - DocumentSourceStreamingGroup::createFromBson, - AllowedWithApiStrict::kAlways); - -constexpr StringData DocumentSourceStreamingGroup::kStageName; - -const char* DocumentSourceStreamingGroup::getSourceName() const { - return kStageName.rawData(); -} - -DocumentSource::GetNextResult DocumentSourceStreamingGroup::doGetNext() { - auto getReadyResult = getNextReadyGroup(); - if (!getReadyResult.isEOF()) { - return getReadyResult; - } else if (_sourceDepleted) { - dispose(); - return getReadyResult; - } - - auto prepareResult = readyNextBatch(); - if (prepareResult.isPaused()) { - return prepareResult; - } - return getNextReadyGroup(); -} - -DocumentSourceStreamingGroup::DocumentSourceStreamingGroup( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes) - : DocumentSourceGroupBase(kStageName, expCtx, maxMemoryUsageBytes), _sourceDepleted(false) {} - -boost::intrusive_ptr<DocumentSourceStreamingGroup> DocumentSourceStreamingGroup::create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const boost::intrusive_ptr<Expression>& groupByExpression, - std::vector<size_t> monotonicExpressionIndexes, - std::vector<AccumulationStatement> accumulationStatements, - boost::optional<size_t> maxMemoryUsageBytes) { - boost::intrusive_ptr<DocumentSourceStreamingGroup> groupStage = - new DocumentSourceStreamingGroup(expCtx, maxMemoryUsageBytes); - groupStage->setIdExpression(groupByExpression); - for (auto&& statement : accumulationStatements) { - groupStage->addAccumulator(statement); - } - uassert(7026709, - "streaming group must have at least one monotonic id expression", - !monotonicExpressionIndexes.empty()); - uassert(7026710, - "streaming group monotonic expression indexes must correspond to id expressions", - std::all_of(monotonicExpressionIndexes.begin(), - monotonicExpressionIndexes.end(), - [&](size_t i) { return i < groupStage->_idExpressions.size(); })); - groupStage->_monotonicExpressionIndexes = std::move(monotonicExpressionIndexes); - return groupStage; -} - -boost::intrusive_ptr<DocumentSource> DocumentSourceStreamingGroup::createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { - return createFromBsonWithMaxMemoryUsage(std::move(elem), expCtx, boost::none); -} - -boost::intrusive_ptr<DocumentSource> DocumentSourceStreamingGroup::createFromBsonWithMaxMemoryUsage( - BSONElement elem, - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes) { - boost::intrusive_ptr<DocumentSourceStreamingGroup> groupStage = - new DocumentSourceStreamingGroup(expCtx, maxMemoryUsageBytes); - groupStage->initializeFromBson(elem); - - const auto& monotonicIdFieldsElem = elem.Obj().getField(kMonotonicIdFieldsSpecField); - uassert(7026702, - "streaming group must specify an array of monotonic id fields " + - kMonotonicIdFieldsSpecField, - monotonicIdFieldsElem.type() == Array); - const auto& monotonicIdFields = monotonicIdFieldsElem.Array(); - if (groupStage->_idFieldNames.empty()) { - uassert(7026703, - "if there is no explicit id fields, " + kMonotonicIdFieldsSpecField + - " must contain a single \"_id\" string", - monotonicIdFields.size() == 1 && - monotonicIdFields[0].valueStringDataSafe() == "_id"_sd); - groupStage->_monotonicExpressionIndexes.push_back(0); - } else { - groupStage->_monotonicExpressionIndexes.reserve(monotonicIdFields.size()); - for (const auto& fieldNameElem : monotonicIdFields) { - uassert(7026704, - kMonotonicIdFieldsSpecField + " elements must be strings", - fieldNameElem.type() == String); - StringData fieldName = fieldNameElem.valueStringData(); - auto it = std::find( - groupStage->_idFieldNames.begin(), groupStage->_idFieldNames.end(), fieldName); - uassert(7026705, "id field not found", it != groupStage->_idFieldNames.end()); - groupStage->_monotonicExpressionIndexes.push_back( - std::distance(groupStage->_idFieldNames.begin(), it)); - } - std::sort(groupStage->_monotonicExpressionIndexes.begin(), - groupStage->_monotonicExpressionIndexes.end()); - } - - return groupStage; -} - -void DocumentSourceStreamingGroup::serializeAdditionalFields( - MutableDocument& out, const SerializationOptions& opts) const { - std::vector<Value> monotonicIdFields; - if (_idFieldNames.empty()) { - monotonicIdFields.emplace_back(opts.serializeFieldPath("_id")); - } else { - for (size_t i : _monotonicExpressionIndexes) { - monotonicIdFields.emplace_back(opts.serializeFieldPathFromString(_idFieldNames[i])); - } - } - out[kMonotonicIdFieldsSpecField] = Value(std::move(monotonicIdFields)); -} - -bool DocumentSourceStreamingGroup::isSpecFieldReserved(StringData fieldName) { - return fieldName == kMonotonicIdFieldsSpecField; -} - -DocumentSource::GetNextResult DocumentSourceStreamingGroup::getNextDocument() { - if (_firstDocumentOfNextBatch) { - GetNextResult result = std::move(_firstDocumentOfNextBatch.value()); - _firstDocumentOfNextBatch.reset(); - return result; - } - return pSource->getNext(); -} - -DocumentSource::GetNextResult DocumentSourceStreamingGroup::readyNextBatch() { - resetReadyGroups(); - GetNextResult input = getNextDocument(); - return readyNextBatchInner(input); -} - -// This separate NOINLINE function is used here to decrease stack utilization of readyNextBatch() -// and prevent stack overflows. -MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult -DocumentSourceStreamingGroup::readyNextBatchInner(GetNextResult input) { - setExecutionStarted(); - // Calculate groups until we either exaust pSource or encounter change in monotonic id - // expression, which means all current groups are finalized. - for (; input.isAdvanced(); input = pSource->getNext()) { - if (shouldSpillWithAttemptToSaveMemory()) { - spill(); - } - auto root = input.releaseDocument(); - Value id = computeId(root); - - if (isBatchFinished(id)) { - _firstDocumentOfNextBatch = std::move(root); - readyGroups(); - return input; - } - - processDocument(id, root); - } - - switch (input.getStatus()) { - case DocumentSource::GetNextResult::ReturnStatus::kAdvanced: { - MONGO_UNREACHABLE; // We consumed all advances above. - } - case DocumentSource::GetNextResult::ReturnStatus::kPauseExecution: { - return input; // Propagate pause. - } - case DocumentSource::GetNextResult::ReturnStatus::kEOF: { - readyGroups(); - _sourceDepleted = true; - return input; - } - } - MONGO_UNREACHABLE; -} - -bool DocumentSourceStreamingGroup::isBatchFinished(const Value& id) { - if (_idExpressions.size() == 1) { - tassert(7026706, - "if there are no explicit id fields, it is only one monotonic expression with id 0", - _monotonicExpressionIndexes.size() == 1 && _monotonicExpressionIndexes[0] == 0); - return checkForBatchEndAndUpdateLastIdValues([&](size_t) { return id; }); - } else { - tassert(7026707, - "if there are explicit id fields, internal representation of id is an array", - id.isArray()); - const std::vector<Value>& idValues = id.getArray(); - return checkForBatchEndAndUpdateLastIdValues([&](size_t i) { return idValues[i]; }); - } -} - -template <typename IdValueGetter> -bool DocumentSourceStreamingGroup::checkForBatchEndAndUpdateLastIdValues( - const IdValueGetter& idValueGetter) { - auto assertStreamable = [&](Value value) { - // Nullish and array values will mess us up because they sort differently than they group. - // A null and a missing value will compare equal in sorting, but could result in different - // groups, e.g. {_id: {x: null, y: null}} vs {_id: {}}. An array value will sort by the min - // or max element, with no tie breaking, but group by the whole array. This means that two - // of the exact same array could appear in the input sequence, but with a different array in - // the middle of them, and that would still be considered sorted. That would break our - // batching group logic. - uassert(7026708, - "Monotonic value should not be missing, null or an array", - !value.nullish() && !value.isArray()); - return value; - }; - - // If _lastMonotonicIdFieldValues is empty, it is the first document, so the only thing we need - // to do is initialize it. - if (_lastMonotonicIdFieldValues.empty()) { - for (size_t i : _monotonicExpressionIndexes) { - _lastMonotonicIdFieldValues.push_back(assertStreamable(idValueGetter(i))); - } - return false; - } else { - bool batchFinished = false; - for (size_t index = 0; index < _monotonicExpressionIndexes.size(); ++index) { - Value& oldId = _lastMonotonicIdFieldValues[index]; - const Value& id = assertStreamable(idValueGetter(_monotonicExpressionIndexes[index])); - if (pExpCtx->getValueComparator().compare(oldId, id) != 0) { - oldId = id; - batchFinished = true; - } - } - return batchFinished; - } -} - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_streaming_group.h b/src/mongo/db/pipeline/document_source_streaming_group.h deleted file mode 100644 index e69db4ef026..00000000000 --- a/src/mongo/db/pipeline/document_source_streaming_group.h +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include <memory> -#include <utility> - -#include "mongo/db/pipeline/document_source_group_base.h" - -namespace mongo { - -/** - * This class represents streaming group implementation that can only be used when at least one of - * _id fields is monotonic. It stores and output groups in batches. All groups in the batch has - * the same value of monotonic id fields. - * - * For example, if the inputs are sorted by "x", we could use a batched streaming algorithm to - * perform the grouping for {$group: {_id: {x: "$x", y: "$y"}}}. - * - * Groups are processes in batches. One batch corresponds to a set of groups when each monotonic - * id field have the same value. Non-monotonic fields can have different values, so we still may - * have multiple groups and even spill to disk, but we still consume significanty less memory - * than general hash based group. - * When a document with a different value in at least one group id field is encountered, it is - * cached in '_firstDocumentOfNextBatch', current groups are finalized and returned in - * subsequent getNext() called and when the current batch is depleted, memory is freeed and the - * process starts again. - * - * TODO SERVER-71437 Implement an optimization for a special case where all group fields are - * monotonic - * - we don't need any hashing in this case. - */ -class DocumentSourceStreamingGroup final : public DocumentSourceGroupBase { -public: - static constexpr StringData kStageName = "$_internalStreamingGroup"_sd; - - const char* getSourceName() const final; - - /** - * Convenience method for creating a new $_internalStreamingGroup stage. If maxMemoryUsageBytes - * is boost::none, then it will actually use the value of - * internalDocumentSourceGroupMaxMemoryBytes. - */ - static boost::intrusive_ptr<DocumentSourceStreamingGroup> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const boost::intrusive_ptr<Expression>& groupByExpression, - std::vector<size_t> monotonicExpressionIndexes, - std::vector<AccumulationStatement> accumulationStatements, - boost::optional<size_t> maxMemoryUsageBytes = boost::none); - - /** - * Parses 'elem' into a $_internalStreamingGroup stage, or throws a AssertionException if 'elem' - * was an invalid specification. - */ - static boost::intrusive_ptr<DocumentSource> createFromBson( - BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); - static boost::intrusive_ptr<DocumentSource> createFromBsonWithMaxMemoryUsage( - BSONElement elem, - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes); - -protected: - GetNextResult doGetNext() final; - - bool isSpecFieldReserved(StringData fieldName) final; - void serializeAdditionalFields( - MutableDocument& out, - const SerializationOptions& opts = SerializationOptions{}) const final; - -private: - static constexpr StringData kMonotonicIdFieldsSpecField = "$monotonicIdFields"_sd; - - explicit DocumentSourceStreamingGroup( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - boost::optional<size_t> maxMemoryUsageBytes = boost::none); - - - GetNextResult getNextDocument(); - - GetNextResult readyNextBatch(); - /** - * Readies next batch after all children are initialized. See readyNextBatch() for - * more details. - */ - GetNextResult readyNextBatchInner(GetNextResult input); - - bool isBatchFinished(const Value& id); - - template <typename IdValueGetter> - bool checkForBatchEndAndUpdateLastIdValues(const IdValueGetter& idValueGetter); - - std::vector<size_t> _monotonicExpressionIndexes; - std::vector<Value> _lastMonotonicIdFieldValues; - - boost::optional<Document> _firstDocumentOfNextBatch; - - bool _sourceDepleted; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_tee_consumer.cpp b/src/mongo/db/pipeline/document_source_tee_consumer.cpp index 47539fb491c..739cec3498c 100644 --- a/src/mongo/db/pipeline/document_source_tee_consumer.cpp +++ b/src/mongo/db/pipeline/document_source_tee_consumer.cpp @@ -71,8 +71,9 @@ void DocumentSourceTeeConsumer::doDispose() { _bufferSource->dispose(_facetId); } -Value DocumentSourceTeeConsumer::serialize(const SerializationOptions& opts) const { +Value DocumentSourceTeeConsumer::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { // We only serialize this stage in the context of explain. - return opts.verbosity ? Value(DOC(_stageName << Document())) : Value(); + return explain ? Value(DOC(_stageName << Document())) : Value(); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_tee_consumer.h b/src/mongo/db/pipeline/document_source_tee_consumer.h index 26a8ee02a2f..da56c76a37b 100644 --- a/src/mongo/db/pipeline/document_source_tee_consumer.h +++ b/src/mongo/db/pipeline/document_source_tee_consumer.h @@ -79,7 +79,7 @@ public: const char* getSourceName() const override; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; protected: GetNextResult doGetNext() final; diff --git a/src/mongo/db/pipeline/document_source_test_optimizations.h b/src/mongo/db/pipeline/document_source_test_optimizations.h index d7f8f4bec13..2f6e484be10 100644 --- a/src/mongo/db/pipeline/document_source_test_optimizations.h +++ b/src/mongo/db/pipeline/document_source_test_optimizations.h @@ -71,9 +71,8 @@ public: } private: - virtual Value serialize( - const SerializationOptions& opts = SerializationOptions{}) const final override { - MONGO_UNREACHABLE_TASSERT(7484301); + virtual Value serialize(boost::optional<ExplainOptions::Verbosity>) const override { + MONGO_UNREACHABLE; } }; diff --git a/src/mongo/db/pipeline/document_source_union_with.cpp b/src/mongo/db/pipeline/document_source_union_with.cpp index 3b541c57f4c..62904bfb270 100644 --- a/src/mongo/db/pipeline/document_source_union_with.cpp +++ b/src/mongo/db/pipeline/document_source_union_with.cpp @@ -78,22 +78,6 @@ std::unique_ptr<Pipeline, PipelineDeleter> buildPipelineFromViewDefinition( } // namespace -DocumentSourceUnionWith::DocumentSourceUnionWith( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - std::unique_ptr<Pipeline, PipelineDeleter> pipeline) - : DocumentSource(kStageName, expCtx), _pipeline(std::move(pipeline)) {} - -DocumentSourceUnionWith::DocumentSourceUnionWith( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - NamespaceString unionNss, - std::vector<BSONObj> pipeline) - : DocumentSourceUnionWith(expCtx, - buildPipelineFromViewDefinition( - expCtx, expCtx->getResolvedNamespace(unionNss), pipeline)) { - _userNss = std::move(unionNss); - _userPipeline = std::move(pipeline); -} - DocumentSourceUnionWith::~DocumentSourceUnionWith() { if (_pipeline && _pipeline->getContext()->explain) { _pipeline->dispose(pExpCtx->opCtx); @@ -213,7 +197,9 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceUnionWith::createFromBson( pipeline = unionWithSpec.getPipeline().value_or(std::vector<BSONObj>{}); } return make_intrusive<DocumentSourceUnionWith>( - expCtx, std::move(unionNss), std::move(pipeline)); + expCtx, + buildPipelineFromViewDefinition( + expCtx, expCtx->getResolvedNamespace(std::move(unionNss)), std::move(pipeline))); } DocumentSource::GetNextResult DocumentSourceUnionWith::doGetNext() { @@ -289,10 +275,14 @@ MONGO_COMPILER_NOINLINE void DocumentSourceUnionWith::logShardedViewFound( Pipeline::SourceContainer::iterator DocumentSourceUnionWith::doOptimizeAt( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { auto duplicateAcrossUnion = [&](auto&& nextStage) { - _pipeline->addFinalSource(nextStage->clone(_pipeline->getContext())); + _pipeline->addFinalSource(nextStage->clone()); // Apply the same rewrite to the cached pipeline if available. if (pExpCtx->explain >= ExplainOptions::Verbosity::kExecStats) { - _pushedDownStages.push_back(nextStage->serialize().getDocument().toBson()); + auto cloneForExplain = nextStage->clone(); + if (!_cachedPipeline.empty()) { + cloneForExplain->setSource(_cachedPipeline.back().get()); + } + _cachedPipeline.push_back(std::move(cloneForExplain)); } auto newStageItr = container->insert(itr, std::move(nextStage)); container->erase(std::next(itr)); @@ -325,16 +315,14 @@ void DocumentSourceUnionWith::doDispose() { if (!_pipeline->getContext()->explain) { _pipeline->dispose(pExpCtx->opCtx); - _userPipeline.clear(); - _pushedDownStages.clear(); _pipeline.reset(); } } } -Value DocumentSourceUnionWith::serialize(const SerializationOptions& opts) const { +Value DocumentSourceUnionWith::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { auto collectionless = _pipeline->getContext()->ns.isCollectionlessAggregateNS(); - if (opts.verbosity) { + if (explain) { // There are several different possible states depending on the explain verbosity as well as // the other stages in the pipeline: // * If verbosity is queryPlanner, then the sub-pipeline should be untouched and we can @@ -344,63 +332,44 @@ Value DocumentSourceUnionWith::serialize(const SerializationOptions& opts) const // $limit stage after the $unionWith which results in only reading from the base collection // branch and not the sub-pipeline. Pipeline* pipeCopy = nullptr; - if (*opts.verbosity == ExplainOptions::Verbosity::kQueryPlanner) { + if (*explain == ExplainOptions::Verbosity::kQueryPlanner) { pipeCopy = Pipeline::create(_pipeline->getSources(), _pipeline->getContext()).release(); - } else if (*opts.verbosity >= ExplainOptions::Verbosity::kExecStats && + } else if (*explain >= ExplainOptions::Verbosity::kExecStats && _executionState > ExecutionProgress::kIteratingSource) { - std::vector<BSONObj> recoveredPipeline; // We've either exhausted the sub-pipeline or at least started iterating it. Use the - // cached user pipeline and pushed down stages to get the explain output since the - // '_pipeline' may have been modified for any optimizations or pushdowns into the - // initial $cursor stage. - recoveredPipeline.reserve(_userPipeline.size() + _pushedDownStages.size()); - std::move( - _userPipeline.begin(), _userPipeline.end(), std::back_inserter(recoveredPipeline)); - std::move(_pushedDownStages.begin(), - _pushedDownStages.end(), - std::back_inserter(recoveredPipeline)); - pipeCopy = Pipeline::parse(recoveredPipeline, _pipeline->getContext()).release(); + // cached pipeline to get the explain output since the '_pipeline' may have been + // modified for any optimizations or pushdowns into the initial $cursor stage. + pipeCopy = Pipeline::create(_cachedPipeline, _pipeline->getContext()).release(); } else { // The plan does not require reading from the sub-pipeline, so just include the // serialization in the explain output. BSONArrayBuilder bab; - for (auto&& stage : _pipeline->serialize(opts)) + for (auto&& stage : _pipeline->serialize(explain)) bab << stage; auto spec = collectionless ? DOC("pipeline" << bab.arr()) - : DOC("coll" << opts.serializeIdentifier(_pipeline->getContext()->ns.coll()) - << "pipeline" << bab.arr()); + : DOC("coll" << _pipeline->getContext()->ns.coll() << "pipeline" << bab.arr()); return Value(DOC(getSourceName() << spec)); } invariant(pipeCopy); BSONObj explainLocal = - pExpCtx->mongoProcessInterface->preparePipelineAndExplain(pipeCopy, *opts.verbosity); + pExpCtx->mongoProcessInterface->preparePipelineAndExplain(pipeCopy, *explain); LOGV2_DEBUG(4553501, 3, "$unionWith attached cursor to pipeline for explain"); // We expect this to be an explanation of a pipeline -- there should only be one field. invariant(explainLocal.nFields() == 1); - auto spec = collectionless - ? DOC("pipeline" << explainLocal.firstElement()) - : DOC("coll" << opts.serializeIdentifier(_pipeline->getContext()->ns.coll()) - << "pipeline" << explainLocal.firstElement()); + auto spec = collectionless ? DOC("pipeline" << explainLocal.firstElement()) + : DOC("coll" << _pipeline->getContext()->ns.coll() << "pipeline" + << explainLocal.firstElement()); return Value(DOC(getSourceName() << spec)); } else { - // Query shapes must reflect the original, unresolved and unoptimized pipeline, so we need a - // special case here if we are serializing the stage for that purpose. Otherwise, we should - // return the current (optimized) pipeline for introspection with explain, etc. - auto serializedPipeline = [&]() -> std::vector<BSONObj> { - if (opts.transformIdentifiers || - opts.literalPolicy != LiteralSerializationPolicy::kUnchanged) { - return Pipeline::parse(_userPipeline, _pipeline->getContext()) - ->serializeToBson(opts); - } - return _pipeline->serializeToBson(opts); - }(); - - auto spec = collectionless ? DOC("pipeline" << serializedPipeline) - : DOC("coll" << opts.serializeIdentifier(_userNss.coll()) - << "pipeline" << serializedPipeline); + BSONArrayBuilder bab; + for (auto&& stage : _pipeline->serialize()) + bab << stage; + auto spec = collectionless + ? DOC("pipeline" << bab.arr()) + : DOC("coll" << _pipeline->getContext()->ns.coll() << "pipeline" << bab.arr()); return Value(DOC(getSourceName() << spec)); } } @@ -449,11 +418,6 @@ void DocumentSourceUnionWith::reattachToOperationContext(OperationContext* opCtx } } -bool DocumentSourceUnionWith::validateOperationContext(const OperationContext* opCtx) const { - return getContext()->opCtx == opCtx && - (!_pipeline || _pipeline->validateOperationContext(opCtx)); -} - void DocumentSourceUnionWith::addInvolvedCollections( stdx::unordered_set<NamespaceString>* collectionNames) const { collectionNames->insert(_pipeline->getContext()->ns); diff --git a/src/mongo/db/pipeline/document_source_union_with.h b/src/mongo/db/pipeline/document_source_union_with.h index 9d9f9aae9c8..0e2999a2bfc 100644 --- a/src/mongo/db/pipeline/document_source_union_with.h +++ b/src/mongo/db/pipeline/document_source_union_with.h @@ -61,19 +61,20 @@ public: }; DocumentSourceUnionWith(const boost::intrusive_ptr<ExpressionContext>& expCtx, - NamespaceString unionNss, - std::vector<BSONObj> pipeline); - - // Expose a constructor that skips the parsing step for testing purposes. - DocumentSourceUnionWith(const boost::intrusive_ptr<ExpressionContext>& expCtx, - std::unique_ptr<Pipeline, PipelineDeleter> pipeline); + std::unique_ptr<Pipeline, PipelineDeleter> pipeline) + : DocumentSource(kStageName, expCtx), _pipeline(std::move(pipeline)) { + // If this pipeline is being run as part of explain, then cache a copy to use later during + // serialization. + if (expCtx->explain >= ExplainOptions::Verbosity::kExecStats) { + _cachedPipeline = _pipeline->getSources(); + } + } DocumentSourceUnionWith(const DocumentSourceUnionWith& original, const boost::intrusive_ptr<ExpressionContext>& newExpCtx) - : DocumentSource(kStageName, newExpCtx), - _pipeline(original._pipeline->clone()), - _userNss(original._userNss), - _userPipeline(original._userPipeline) {} + : DocumentSource(kStageName, + newExpCtx ? newExpCtx : original.pExpCtx->copyWith(original.pExpCtx->ns)), + _pipeline(original._pipeline->clone()) {} ~DocumentSourceUnionWith(); @@ -129,8 +130,6 @@ public: void reattachToOperationContext(OperationContext* opCtx) final; - bool validateOperationContext(const OperationContext* opCtx) const final; - bool usedDisk() final; const SpecificStats* getSpecificStats() const final { @@ -181,7 +180,7 @@ private: kFinished }; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; void addViewDefinition(NamespaceString nss, std::vector<BSONObj> viewPipeline); @@ -190,15 +189,7 @@ private: const ExceptionFor<ErrorCodes::CommandOnShardedViewNotSupportedOnMongod>& e); std::unique_ptr<Pipeline, PipelineDeleter> _pipeline; - // The original, unresolved namespace to union. - NamespaceString _userNss; - // The aggregation pipeline defined with the user request, prior to optimization and view - // resolution. - std::vector<BSONObj> _userPipeline; - // Match and/or project stages after a $unionWith can be pushed down into the $unionWith (and - // the head of the pipeline). If we're doing an explain with execution stats, we will need - // copies of these stages as they may be pushed down to the find layer. - std::vector<BSONObj> _pushedDownStages; + Pipeline::SourceContainer _cachedPipeline; ExecutionProgress _executionState = ExecutionProgress::kIteratingSource; UnionWithStats _stats; }; diff --git a/src/mongo/db/pipeline/document_source_union_with.idl b/src/mongo/db/pipeline/document_source_union_with.idl index 11bbaa499dc..77b3ea64a82 100644 --- a/src/mongo/db/pipeline/document_source_union_with.idl +++ b/src/mongo/db/pipeline/document_source_union_with.idl @@ -31,7 +31,6 @@ global: imports: - "mongo/idl/basic_types.idl" - - "mongo/db/pipeline/aggregate_command.idl" structs: UnionWithSpec: @@ -45,4 +44,4 @@ structs: pipeline: description: An optional pipeline to apply to the collection being unioned. optional: true - type: pipeline + type: array<object> diff --git a/src/mongo/db/pipeline/document_source_union_with_test.cpp b/src/mongo/db/pipeline/document_source_union_with_test.cpp index 6dff25c53b1..ec2749edd0f 100644 --- a/src/mongo/db/pipeline/document_source_union_with_test.cpp +++ b/src/mongo/db/pipeline/document_source_union_with_test.cpp @@ -60,95 +60,104 @@ using MockMongoInterface = StubLookupSingleDocumentProcessInterface; // This provides access to getExpCtx(), but we'll use a different name for this test suite. using DocumentSourceUnionWithTest = AggregationContextFixture; -// A custom-deleter which disposes a DocumentSource when it goes out of scope. -struct DocumentSourceDeleter { - void operator()(DocumentSource* docSource) { - docSource->dispose(); - delete docSource; - } -}; - -auto makeUnion(const boost::intrusive_ptr<ExpressionContext>& expCtx, - std::unique_ptr<Pipeline, PipelineDeleter> pipeline) { - return std::unique_ptr<DocumentSourceUnionWith, DocumentSourceDeleter>( - new DocumentSourceUnionWith(expCtx, std::move(pipeline)), DocumentSourceDeleter()); -} - TEST_F(DocumentSourceUnionWithTest, BasicSerialUnions) { - const auto doc = Document{{"a", 1}}; - const auto mock = DocumentSourceMock::createForTest(doc, getExpCtx()); - const auto mockDeque = std::deque<DocumentSource::GetNextResult>{Document{doc}}; - getExpCtx()->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDeque); - auto unionWithOne = - makeUnion(getExpCtx(), - Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); - auto unionWithTwo = - makeUnion(getExpCtx(), - Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); - unionWithOne->setSource(mock.get()); - unionWithTwo->setSource(unionWithOne.get()); + const auto docs = std::array{Document{{"a", 1}}, Document{{"b", 1}}, Document{{"c", 1}}}; + const auto mock = DocumentSourceMock::createForTest(docs[0], getExpCtx()); + const auto mockDequeOne = std::deque<DocumentSource::GetNextResult>{Document{docs[1]}}; + const auto mockDequeTwo = std::deque<DocumentSource::GetNextResult>{Document{docs[2]}}; + const auto mockCtxOne = getExpCtx()->copyWith({}); + mockCtxOne->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeOne); + const auto mockCtxTwo = getExpCtx()->copyWith({}); + mockCtxTwo->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeTwo); + auto unionWithOne = DocumentSourceUnionWith( + mockCtxOne, + Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); + auto unionWithTwo = DocumentSourceUnionWith( + mockCtxTwo, + Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); + unionWithOne.setSource(mock.get()); + unionWithTwo.setSource(&unionWithOne); auto comparator = DocumentComparator(); - const auto expectedResults = 3; - for (auto i = 0; i < expectedResults; ++i) { - auto next = unionWithTwo->getNext(); + auto results = comparator.makeUnorderedDocumentSet(); + for (auto& doc [[maybe_unused]] : docs) { + auto next = unionWithTwo.getNext(); ASSERT_TRUE(next.isAdvanced()); - ASSERT_EQ(comparator.compare(next.releaseDocument(), doc), 0); + const auto [ignored, inserted] = results.insert(next.releaseDocument()); + ASSERT_TRUE(inserted); } + for (const auto& doc : docs) + ASSERT_TRUE(results.find(doc) != results.end()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + + unionWithOne.dispose(); + unionWithTwo.dispose(); } TEST_F(DocumentSourceUnionWithTest, BasicNestedUnions) { - const auto doc = Document{{"a", 1}}; - const auto mock = DocumentSourceMock::createForTest(doc, getExpCtx()); - const auto mockDeque = std::deque<DocumentSource::GetNextResult>{Document{doc}}; - getExpCtx()->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDeque); + const auto docs = std::array{Document{{"a", 1}}, Document{{"b", 1}}, Document{{"c", 1}}}; + const auto mock = DocumentSourceMock::createForTest(docs[0], getExpCtx()); + const auto mockDequeOne = std::deque<DocumentSource::GetNextResult>{Document{docs[1]}}; + const auto mockDequeTwo = std::deque<DocumentSource::GetNextResult>{Document{docs[2]}}; + const auto mockCtxOne = getExpCtx()->copyWith({}); + mockCtxOne->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeOne); + const auto mockCtxTwo = getExpCtx()->copyWith({}); + mockCtxTwo->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeTwo); auto unionWithOne = make_intrusive<DocumentSourceUnionWith>( - getExpCtx(), + mockCtxOne, Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); - auto unionWithTwo = - makeUnion(getExpCtx(), - Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{unionWithOne}, - getExpCtx())); - unionWithTwo->setSource(mock.get()); + auto unionWithTwo = DocumentSourceUnionWith( + mockCtxTwo, + Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{unionWithOne}, + getExpCtx())); + unionWithTwo.setSource(mock.get()); auto comparator = DocumentComparator(); - const auto expectedResults = 3; - for (auto i = 0; i < expectedResults; ++i) { - auto next = unionWithTwo->getNext(); + auto results = comparator.makeUnorderedDocumentSet(); + for (auto& doc [[maybe_unused]] : docs) { + auto next = unionWithTwo.getNext(); ASSERT_TRUE(next.isAdvanced()); - ASSERT_EQ(comparator.compare(next.releaseDocument(), doc), 0); + const auto [ignored, inserted] = results.insert(next.releaseDocument()); + ASSERT_TRUE(inserted); } + for (const auto& doc : docs) + ASSERT_TRUE(results.find(doc) != results.end()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + + unionWithTwo.dispose(); } TEST_F(DocumentSourceUnionWithTest, UnionsWithNonEmptySubPipelines) { - const auto inputDoc = Document{{"a", 1}}; - const auto outputDocs = std::array{Document{{"a", 1}}, Document{{"a", 1}, {"d", 1}}}; - const auto mock = DocumentSourceMock::createForTest(inputDoc, getExpCtx()); - const auto mockDeque = std::deque<DocumentSource::GetNextResult>{Document{inputDoc}}; - getExpCtx()->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDeque); - const auto filter = DocumentSourceMatch::create(BSON("d" << 1), getExpCtx()); - const auto proj = DocumentSourceAddFields::create(BSON("d" << 1), getExpCtx()); - auto unionWithOne = makeUnion( - getExpCtx(), + const auto inputDocs = std::array{Document{{"a", 1}}, Document{{"b", 1}}, Document{{"c", 1}}}; + const auto outputDocs = std::array{Document{{"a", 1}}, Document{{"c", 1}, {"d", 1}}}; + const auto mock = DocumentSourceMock::createForTest(inputDocs[0], getExpCtx()); + const auto mockDequeOne = std::deque<DocumentSource::GetNextResult>{Document{inputDocs[1]}}; + const auto mockDequeTwo = std::deque<DocumentSource::GetNextResult>{Document{inputDocs[2]}}; + const auto mockCtxOne = getExpCtx()->copyWith({}); + mockCtxOne->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeOne); + const auto mockCtxTwo = getExpCtx()->copyWith({}); + mockCtxTwo->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeTwo); + const auto filter = DocumentSourceMatch::create(BSON("d" << 1), mockCtxOne); + const auto proj = DocumentSourceAddFields::create(BSON("d" << 1), mockCtxTwo); + auto unionWithOne = DocumentSourceUnionWith( + mockCtxOne, Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{filter}, getExpCtx())); - auto unionWithTwo = makeUnion( - getExpCtx(), + auto unionWithTwo = DocumentSourceUnionWith( + mockCtxTwo, Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{proj}, getExpCtx())); - unionWithOne->setSource(mock.get()); - unionWithTwo->setSource(unionWithOne.get()); + unionWithOne.setSource(mock.get()); + unionWithTwo.setSource(&unionWithOne); auto comparator = DocumentComparator(); auto results = comparator.makeUnorderedDocumentSet(); for (auto& doc [[maybe_unused]] : outputDocs) { - auto next = unionWithTwo->getNext(); + auto next = unionWithTwo.getNext(); ASSERT_TRUE(next.isAdvanced()); const auto [ignored, inserted] = results.insert(next.releaseDocument()); ASSERT_TRUE(inserted); @@ -156,9 +165,12 @@ TEST_F(DocumentSourceUnionWithTest, UnionsWithNonEmptySubPipelines) { for (const auto& doc : outputDocs) ASSERT_TRUE(results.find(doc) != results.end()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + + unionWithOne.dispose(); + unionWithTwo.dispose(); } TEST_F(DocumentSourceUnionWithTest, SerializeAndParseWithPipeline) { @@ -297,25 +309,32 @@ TEST_F(DocumentSourceUnionWithTest, PropagatePauses) { Document(), DocumentSource::GetNextResult::makePauseExecution()}, getExpCtx()); - const auto mockDeque = std::deque<DocumentSource::GetNextResult>{}; - getExpCtx()->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDeque); - auto unionWithOne = - makeUnion(getExpCtx(), - Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); - auto unionWithTwo = - makeUnion(getExpCtx(), - Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); - unionWithOne->setSource(mock.get()); - unionWithTwo->setSource(unionWithOne.get()); - - ASSERT_TRUE(unionWithTwo->getNext().isAdvanced()); - ASSERT_TRUE(unionWithTwo->getNext().isPaused()); - ASSERT_TRUE(unionWithTwo->getNext().isAdvanced()); - ASSERT_TRUE(unionWithTwo->getNext().isPaused()); - - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); - ASSERT_TRUE(unionWithTwo->getNext().isEOF()); + const auto mockDequeOne = std::deque<DocumentSource::GetNextResult>{}; + const auto mockDequeTwo = std::deque<DocumentSource::GetNextResult>{}; + const auto mockCtxOne = getExpCtx()->copyWith({}); + mockCtxOne->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeOne); + const auto mockCtxTwo = getExpCtx()->copyWith({}); + mockCtxTwo->mongoProcessInterface = std::make_unique<MockMongoInterface>(mockDequeTwo); + auto unionWithOne = DocumentSourceUnionWith( + mockCtxOne, + Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); + auto unionWithTwo = DocumentSourceUnionWith( + mockCtxTwo, + Pipeline::create(std::list<boost::intrusive_ptr<DocumentSource>>{}, getExpCtx())); + unionWithOne.setSource(mock.get()); + unionWithTwo.setSource(&unionWithOne); + + ASSERT_TRUE(unionWithTwo.getNext().isAdvanced()); + ASSERT_TRUE(unionWithTwo.getNext().isPaused()); + ASSERT_TRUE(unionWithTwo.getNext().isAdvanced()); + ASSERT_TRUE(unionWithTwo.getNext().isPaused()); + + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + ASSERT_TRUE(unionWithTwo.getNext().isEOF()); + + unionWithOne.dispose(); + unionWithTwo.dispose(); } TEST_F(DocumentSourceUnionWithTest, ReturnEOFAfterBeingDisposed) { @@ -572,62 +591,5 @@ TEST_F(DocumentSourceUnionWithTest, StricterConstraintsFromSubSubPipelineAreInhe StageConstraints::UnionRequirement::kAllowed); ASSERT_TRUE(unionStage.constraints(Pipeline::SplitState::kUnsplit) == expectedConstraints); } - -TEST_F(DocumentSourceUnionWithTest, RedactsCorrectlyBasic) { - auto expCtx = getExpCtx(); - NamespaceString nsToUnionWith(expCtx->ns.db(), "coll"); - expCtx->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {nsToUnionWith.coll().toString(), {nsToUnionWith, std::vector<BSONObj>()}}}); - - auto docSource = DocumentSourceUnionWith::createFromBson( - BSON("$unionWith" << nsToUnionWith.coll()).firstElement(), expCtx); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$unionWith": { - "coll": "HASH<coll>", - "pipeline": [] - } - })", - redact(*docSource)); -} - -TEST_F(DocumentSourceUnionWithTest, RedactsCorrectlyWithPipeline) { - auto expCtx = getExpCtx(); - NamespaceString nsToUnionWith(expCtx->ns.db(), "coll"); - expCtx->setResolvedNamespaces(StringMap<ExpressionContext::ResolvedNamespace>{ - {nsToUnionWith.coll().toString(), {nsToUnionWith, std::vector<BSONObj>()}}}); - - BSONArrayBuilder pipeline; - pipeline << BSON("$match" << BSON("a" << 15)); - pipeline << BSON("$project" << BSON("a" << 1 << "b" << 1)); - auto docSource = DocumentSourceUnionWith::createFromBson( - BSON("$unionWith" << BSON("coll" << nsToUnionWith.coll() << "pipeline" << pipeline.arr())) - .firstElement(), - expCtx); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$unionWith": { - "coll": "HASH<coll>", - "pipeline": [ - { - "$match": { - "HASH<a>": { - "$eq": "?number" - } - } - }, - { - "$project": { - "HASH<_id>": true, - "HASH<a>": true, - "HASH<b>": true - } - } - ] - } - })", - redact(*docSource)); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_unwind.cpp b/src/mongo/db/pipeline/document_source_unwind.cpp index f664c0e62ff..daf7adcd5ed 100644 --- a/src/mongo/db/pipeline/document_source_unwind.cpp +++ b/src/mongo/db/pipeline/document_source_unwind.cpp @@ -220,7 +220,7 @@ DocumentSource::GetNextResult DocumentSourceUnwind::doGetNext() { } DocumentSource::GetModPathsReturn DocumentSourceUnwind::getModifiedPaths() const { - OrderedPathSet modifiedFields{_unwindPath.fullPath()}; + std::set<std::string> modifiedFields{_unwindPath.fullPath()}; if (_indexPath) { modifiedFields.insert(_indexPath->fullPath()); } @@ -232,16 +232,19 @@ bool DocumentSourceUnwind::canPushSortBack(const DocumentSourceSort* sort) const // otherwise when we swap the limit and unwind, we could end up providing fewer results to the // user than expected. if (!sort->hasLimit() || _preserveNullAndEmptyArrays) { - auto modifiedPaths = getModifiedPaths(); + auto unwindPath = _unwindPath.fullPath(); // Checks if any of the $sort's paths depend on the unwind path (or vice versa). SortPattern sortKeyPattern = sort->getSortKeyPattern(); - bool sortDependsOnUnwind = + bool sortPathMatchesUnwindPath = std::any_of(sortKeyPattern.begin(), sortKeyPattern.end(), [&](auto& sortKey) { // If 'sortKey' is a $meta expression, we can do the swap. - return sortKey.fieldPath && modifiedPaths.canModify(*sortKey.fieldPath); + if (!sortKey.fieldPath) + return false; + return expression::bidirectionalPathPrefixOf(unwindPath, + sortKey.fieldPath->fullPath()); }); - return !sortDependsOnUnwind; + return !sortPathMatchesUnwindPath; } return false; } @@ -290,13 +293,12 @@ Pipeline::SourceContainer::iterator DocumentSourceUnwind::doOptimizeAt( return std::next(itr); } -Value DocumentSourceUnwind::serialize(const SerializationOptions& opts) const { - return Value(DOC( - getSourceName() << DOC( - "path" << opts.serializeFieldPathWithPrefix(_unwindPath) << "preserveNullAndEmptyArrays" - << (_preserveNullAndEmptyArrays ? opts.serializeLiteral(true) : Value()) - << "includeArrayIndex" - << (_indexPath ? Value(opts.serializeFieldPath(*_indexPath)) : Value())))); +Value DocumentSourceUnwind::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(DOC(getSourceName() << DOC( + "path" << _unwindPath.fullPathWithPrefix() << "preserveNullAndEmptyArrays" + << (_preserveNullAndEmptyArrays ? Value(true) : Value()) + << "includeArrayIndex" + << (_indexPath ? Value((*_indexPath).fullPath()) : Value())))); } DepsTracker::State DocumentSourceUnwind::getDependencies(DepsTracker* deps) const { diff --git a/src/mongo/db/pipeline/document_source_unwind.h b/src/mongo/db/pipeline/document_source_unwind.h index 81a2fb58882..d7f401dd99c 100644 --- a/src/mongo/db/pipeline/document_source_unwind.h +++ b/src/mongo/db/pipeline/document_source_unwind.h @@ -43,7 +43,7 @@ public: // virtuals from DocumentSource const char* getSourceName() const final; - Value serialize(const SerializationOptions& opts = SerializationOptions{}) const final override; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; /** * Returns the unwound path, and the 'includeArrayIndex' path, if specified. diff --git a/src/mongo/db/pipeline/document_source_unwind_test.cpp b/src/mongo/db/pipeline/document_source_unwind_test.cpp index 3c4b4fcfd68..aa8a550ec06 100644 --- a/src/mongo/db/pipeline/document_source_unwind_test.cpp +++ b/src/mongo/db/pipeline/document_source_unwind_test.cpp @@ -816,26 +816,6 @@ TEST_F(UnwindStageTest, ShouldRejectUnrecognizedOption) { 28811); } -TEST_F(UnwindStageTest, Redaction) { - auto spec = fromjson(R"({ - $unwind: { - path: "$foo.bar", - includeArrayIndex: "foo.baz", - preserveNullAndEmptyArrays: true - } - })"); - auto docSource = DocumentSourceUnwind::createFromBson(spec.firstElement(), getExpCtx()); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "$unwind": { - "path": "$HASH<foo>.HASH<bar>", - "preserveNullAndEmptyArrays": "?bool", - "includeArrayIndex": "HASH<foo>.HASH<baz>" - } - })", - redact(*docSource)); -} - class All : public OldStyleSuiteSpecification { public: All() : OldStyleSuiteSpecification("DocumentSourceUnwindTests") {} diff --git a/src/mongo/db/pipeline/document_source_writer.h b/src/mongo/db/pipeline/document_source_writer.h index 25fd08aac5c..a94b4efca6b 100644 --- a/src/mongo/db/pipeline/document_source_writer.h +++ b/src/mongo/db/pipeline/document_source_writer.h @@ -36,11 +36,8 @@ #include "mongo/db/db_raii.h" #include "mongo/db/operation_context.h" #include "mongo/db/pipeline/document_source.h" -#include "mongo/db/query/query_knobs_gen.h" #include "mongo/db/read_concern.h" #include "mongo/db/storage/recovery_unit.h" -#include "mongo/rpc/metadata/impersonated_user_metadata.h" -#include "mongo/s/write_ops/batched_command_request.h" namespace mongo { using namespace fmt::literals; @@ -86,14 +83,11 @@ public: /** * This is a base abstract class for all stages performing a write operation into an output * collection. The writes are organized in batches in which elements are objects of the templated - * type 'B'. A subclass must override the following methods to be able to write into the output - * collection: + * type 'B'. A subclass must override two methods to be able to write into the output collection: * - * - 'makeBatchObject()' - creates an object of type 'B' from the given 'Document', which is, + * 1. 'makeBatchObject()' - to create an object of type 'B' from the given 'Document', which is, * essentially, a result of the input source's 'getNext()' . - * - 'spill()' - writes the batch into the output collection. - * - 'initializeBatchedWriteRequest()' - initializes the request object for writing a batch to - * the output collection. + * 2. 'spill()' - to write the batch into the output collection. * * Two other virtual methods exist which a subclass may override: 'initialize()' and 'finalize()', * which are called before the first element is read from the input source, and after the last one @@ -105,26 +99,12 @@ public: using BatchObject = B; using BatchedObjects = std::vector<BatchObject>; - static BatchedCommandRequest makeInsertCommand(const NamespaceString& outputNs, - bool bypassDocumentValidation) { - write_ops::InsertCommandRequest insertOp(outputNs); - insertOp.setWriteCommandRequestBase([&] { - write_ops::WriteCommandRequestBase wcb; - wcb.setOrdered(false); - wcb.setBypassDocumentValidation(bypassDocumentValidation); - return wcb; - }()); - return BatchedCommandRequest(std::move(insertOp)); - } - DocumentSourceWriter(const char* stageName, NamespaceString outputNs, const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSource(stageName, expCtx), _outputNs(std::move(outputNs)), - _writeConcern(expCtx->opCtx->getWriteConcern()), - _writeSizeEstimator( - expCtx->mongoProcessInterface->getWriteSizeEstimator(expCtx->opCtx, outputNs)) {} + _writeConcern(expCtx->opCtx->getWriteConcern()) {} DepsTracker::State getDependencies(DepsTracker* deps) const override { deps->needWholeDocument = true; @@ -134,7 +114,7 @@ public: GetModPathsReturn getModifiedPaths() const override { // For purposes of tracking which fields come from where, the writer stage does not modify // any fields by default. - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() override { @@ -142,7 +122,7 @@ public: } bool canRunInParallelBeforeWriteStage( - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const override { + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const override { return true; } @@ -163,31 +143,9 @@ protected: virtual void finalize() {} /** - * Writes the documents in 'batch' to the output namespace via 'bcr'. - */ - virtual void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) = 0; - - /** - * Estimates the size of the header of a batch write (that is, the size of the write command - * minus the size of write statements themselves). - */ - int estimateWriteHeaderSize(const BatchedCommandRequest& bcr) const { - using BatchType = BatchedCommandRequest::BatchType; - switch (bcr.getBatchType()) { - case BatchType::BatchType_Insert: - return _writeSizeEstimator->estimateInsertHeaderSize(bcr.getInsertRequest()); - case BatchType::BatchType_Update: - return _writeSizeEstimator->estimateUpdateHeaderSize(bcr.getUpdateRequest()); - case BatchType::BatchType_Delete: - break; - } - MONGO_UNREACHABLE; - } - - /** - * Constructs and configures a BatchedCommandRequest for performing a batch write. + * Writes the documents in 'batch' to the output namespace. */ - virtual BatchedCommandRequest initializeBatchedWriteRequest() const = 0; + virtual void spill(BatchedObjects&& batch) = 0; /** * Creates a batch object from the given document and returns it to the caller along with the @@ -211,9 +169,6 @@ protected: // respect the writeConcern of the original command. WriteConcernOptions _writeConcern; - // An interface that is used to estimate the size of each write operation. - const std::unique_ptr<MongoProcessInterface::WriteSizeEstimator> _writeSizeEstimator; - private: bool _initialized{false}; bool _done{false}; @@ -243,30 +198,9 @@ DocumentSource::GetNextResult DocumentSourceWriter<B>::doGetNext() { _initialized = true; } - // While most metadata attached to a command is limited to less than a KB, Impersonation - // metadata may grow to an arbitrary size. - // - // Ask the active Client how much impersonation metadata we'll use for it, add in our own - // estimate of write header size, and assume that the rest can fit in the space reserved by - // BSONObjMaxUserSize's overhead plus the value from the server parameter: - // internalQueryDocumentSourceWriterBatchExtraReservedBytes. - const auto estimatedMetadataSizeBytes = - rpc::estimateImpersonatedUserMetadataSize(pExpCtx->opCtx); - - BatchedCommandRequest batchWrite = initializeBatchedWriteRequest(); - const auto writeHeaderSize = estimateWriteHeaderSize(batchWrite); - const auto initialRequestSize = estimatedMetadataSizeBytes + writeHeaderSize + - internalQueryDocumentSourceWriterBatchExtraReservedBytes.load(); - - uassert(7637800, - "Unable to proceed with write while metadata size ({}KB) exceeds {}KB"_format( - initialRequestSize / 1024, BSONObjMaxUserSize / 1024), - initialRequestSize <= BSONObjMaxUserSize); - - const auto maxBatchSizeBytes = BSONObjMaxUserSize - initialRequestSize; - BatchedObjects batch; - size_t bufferedBytes = 0; + int bufferedBytes = 0; + auto nextInput = pSource->getNext(); for (; nextInput.isAdvanced(); nextInput = pSource->getNext()) { waitWhileFailPointEnabled(); @@ -276,17 +210,16 @@ DocumentSource::GetNextResult DocumentSourceWriter<B>::doGetNext() { bufferedBytes += objSize; if (!batch.empty() && - (bufferedBytes > maxBatchSizeBytes || + (bufferedBytes > BSONObjMaxUserSize || batch.size() >= write_ops::kMaxWriteBatchSize)) { - spill(std::move(batchWrite), std::move(batch)); + spill(std::move(batch)); batch.clear(); - batchWrite = initializeBatchedWriteRequest(); bufferedBytes = objSize; } batch.push_back(obj); } if (!batch.empty()) { - spill(std::move(batchWrite), std::move(batch)); + spill(std::move(batch)); batch.clear(); } diff --git a/src/mongo/db/pipeline/exchange_spec.idl b/src/mongo/db/pipeline/exchange_spec.idl index 0bc67b96d20..5f07f822693 100644 --- a/src/mongo/db/pipeline/exchange_spec.idl +++ b/src/mongo/db/pipeline/exchange_spec.idl @@ -46,30 +46,25 @@ enums: structs: ExchangeSpec: description: "exchange aggregation request specification" - query_shape_component: true fields: policy: type: ExchangePolicy description: A string indicating a policy of how documents are distributed to consumers. unstable: false - query_shape: parameter consumers: type: int description: Number of consumers. unstable: false - query_shape: literal orderPreserving: type: bool default: false description: A flag indicating documents are merged while preserving the order. unstable: false - query_shape: parameter bufferSize: type: int default: 16777216 description: The size of exchange buffers. unstable: false - query_shape: literal key: type: object default: "BSONObj()" @@ -79,17 +74,14 @@ structs: field listed here, or if any prefix of any path is multikey (i.e. an array is encountered while traversing a path listed here), then it is by definition sent to consumer 0. - query_shape: literal boundaries: type: array<object> optional: true description: Range/hash split points. unstable: false - query_shape: literal consumerIds: type: array<int> optional: true description: Mapping from a range index to a consumer id. unstable: false - query_shape: literal diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 658fd9051b4..bf0915deddd 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -72,25 +72,13 @@ using std::pair; using std::string; using std::vector; -Value ExpressionConstant::serializeConstant(const SerializationOptions& opts, - Value val, - bool wrapRepresentativeValue) { +/// Helper function to easily wrap constants with $const. +static Value serializeConstant(Value val) { if (val.missing()) { return Value("$$REMOVE"_sd); } - // It's safer to wrap constants in $const when generating representative shapes to avoid - // ambiguity when re-parsing (SERVER-88296, SERVER-85376). However, we allow certain expressions - // to override this behavior in order to reduce shape verbosity if the expression takes many - // constant arguments (e.g. variadic expressions - SERVER-84159). - // Debug shapes never wrap constants in $const to reduce shape size (and because re-parsing - // support is not a consideration there). - if ((opts.literalPolicy == LiteralSerializationPolicy::kUnchanged) || - (wrapRepresentativeValue && - opts.literalPolicy == LiteralSerializationPolicy::kToRepresentativeParseableValue)) { - return Value(DOC("$const" << opts.serializeLiteral(val))); - } - return opts.serializeLiteral(val); + return Value(DOC("$const" << val)); } /* --------------------------- Expression ------------------------------ */ @@ -200,7 +188,7 @@ void Expression::registerExpression( parserMap[key] = ParserRegistration{parser, allowedWithApiStrict, allowedWithClientType, requiredMinVersion}; // Add this expression to the global map of operator counters for expressions. - operatorCountersAggExpressions.addCounter(key); + operatorCountersAggExpressions.addAggExpressionCounter(key); } intrusive_ptr<Expression> Expression::parseExpression(ExpressionContext* const expCtx, @@ -320,204 +308,111 @@ const char* ExpressionAbs::getOpName() const { /* ------------------------- ExpressionAdd ----------------------------- */ -namespace { +StatusWith<Value> ExpressionAdd::apply(Value lhs, Value rhs) { + BSONType diffType = Value::getWidestNumeric(rhs.getType(), lhs.getType()); -/** - * We'll try to return the narrowest possible result value while avoiding overflow or implicit use - * of decimal types. To do that, compute separate sums for long, double and decimal values, and - * track the current widest type. The long sum will be converted to double when the first double - * value is seen or when long arithmetic would overflow. - */ -class AddState { -public: - /** - * Update the internal state with another operand. It is up to the caller to validate that the - * operand is of a proper type. - */ - void operator+=(const Value& operand) { - auto oldWidestType = widestType; - // Dates are represented by the long number of milliseconds since the unix epoch, so we can - // treat them as regular numeric values for the purposes of addition after making sure that - // only one date is present in the operand list. - Value valToAdd; - if (operand.getType() == Date) { - uassert(16612, "only one date allowed in an $add expression", !isDate); - Value oldValue = getValue(); - longTotal = 0; - addToDateValue(oldValue); - isDate = true; - valToAdd = Value(operand.getDate().toMillisSinceEpoch()); - } else { - widestType = Value::getWidestNumeric(widestType, operand.getType()); - valToAdd = operand; - } + if (diffType == NumberDecimal) { + Decimal128 left = lhs.coerceToDecimal(); + Decimal128 right = rhs.coerceToDecimal(); + return Value(left.add(right)); + } else if (diffType == NumberDouble) { + double right = rhs.coerceToDouble(); + double left = lhs.coerceToDouble(); + return Value(left + right); + } else if (diffType == NumberLong) { + long long result; - if (isDate) { - addToDateValue(valToAdd); - return; + // If there is an overflow, convert the values to doubles. + if (overflow::add(lhs.coerceToLong(), rhs.coerceToLong(), &result)) { + return Value(lhs.coerceToDouble() + rhs.coerceToDouble()); } + return Value(result); + } else if (diffType == NumberInt) { + long long right = rhs.coerceToLong(); + long long left = lhs.coerceToLong(); + return Value::createIntOrLong(left + right); + } else if (lhs.nullish() || rhs.nullish()) { + return Value(BSONNULL); + } else { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "cannot $add a" << typeName(rhs.getType()) << " from a " + << typeName(lhs.getType())); + } +} - // If this operation widens the return type, perform any necessary type conversions. - if (oldWidestType != widestType) { - switch (widestType) { - case NumberLong: - // Int -> Long is handled by the same sum. - break; - case NumberDouble: - // Int/Long -> Double converts the existing longTotal to a doubleTotal. - doubleTotal = longTotal; - break; - case NumberDecimal: - // Convert the right total to NumberDecimal by looking at the old widest type. - switch (oldWidestType) { - case NumberInt: - case NumberLong: - decimalTotal = Decimal128(longTotal); - break; - case NumberDouble: - decimalTotal = Decimal128(doubleTotal); - break; - default: - MONGO_UNREACHABLE; - } - break; - default: - MONGO_UNREACHABLE; - } - } +Value ExpressionAdd::evaluate(const Document& root, Variables* variables) const { + // We'll try to return the narrowest possible result value while avoiding overflow, loss + // of precision due to intermediate rounding or implicit use of decimal types. To do that, + // compute a compensated sum for non-decimal values and a separate decimal sum for decimal + // values, and track the current narrowest type. + DoubleDoubleSummation nonDecimalTotal; + Decimal128 decimalTotal; + BSONType totalType = NumberInt; + bool haveDate = false; - // Perform the add operation. - switch (widestType) { - case NumberInt: - case NumberLong: - // If the long long arithmetic overflows, promote the result to a NumberDouble and - // start incrementing the doubleTotal. - long long newLongTotal; - if (overflow::add(longTotal, valToAdd.coerceToLong(), &newLongTotal)) { - widestType = NumberDouble; - doubleTotal = longTotal + valToAdd.coerceToDouble(); - } else { - longTotal = newLongTotal; - } + const size_t n = _children.size(); + for (size_t i = 0; i < n; ++i) { + Value val = _children[i]->evaluate(root, variables); + + switch (val.getType()) { + case NumberDecimal: + decimalTotal = decimalTotal.add(val.getDecimal()); + totalType = NumberDecimal; break; case NumberDouble: - doubleTotal += valToAdd.coerceToDouble(); - break; - case NumberDecimal: - decimalTotal = decimalTotal.add(valToAdd.coerceToDecimal()); + nonDecimalTotal.addDouble(val.getDouble()); + if (totalType != NumberDecimal) + totalType = NumberDouble; break; - default: - uasserted(ErrorCodes::TypeMismatch, - str::stream() << "$add only supports numeric or date types, not " - << typeName(valToAdd.getType())); - } - } - - Value getValue() const { - // If one of the operands was a date, then return long value as Date. - if (isDate) { - return Value(Date_t::fromMillisSinceEpoch(longTotal)); - } else { - switch (widestType) { - case NumberInt: - return Value::createIntOrLong(longTotal); - case NumberLong: - return Value(longTotal); - case NumberDouble: - return Value(doubleTotal); - case NumberDecimal: - return Value(decimalTotal); - default: - MONGO_UNREACHABLE; - } - } - } - -private: - // Convert 'valToAdd' into the data type used for dates (long long) and add it to 'longTotal'. - void addToDateValue(Value valToAdd) { - switch (valToAdd.getType()) { - case NumberInt: case NumberLong: - if (overflow::add(longTotal, valToAdd.coerceToLong(), &longTotal)) { - uasserted(ErrorCodes::Overflow, "date overflow"); - } + nonDecimalTotal.addLong(val.getLong()); + if (totalType == NumberInt) + totalType = NumberLong; break; - case NumberDouble: { - using limits = std::numeric_limits<long long>; - double doubleToAdd = valToAdd.coerceToDouble(); - uassert(ErrorCodes::Overflow, - "date overflow", - // The upper bound is exclusive because it rounds up when it is cast to - // a double. - doubleToAdd >= static_cast<double>(limits::min()) && - doubleToAdd < static_cast<double>(limits::max())); - - if (overflow::add(longTotal, llround(doubleToAdd), &longTotal)) { - uasserted(ErrorCodes::Overflow, "date overflow"); - } + case NumberInt: + nonDecimalTotal.addDouble(val.getInt()); break; - } - case NumberDecimal: { - Decimal128 decimalToAdd = valToAdd.coerceToDecimal(); - - std::uint32_t signalingFlags = Decimal128::SignalingFlag::kNoFlag; - std::int64_t longToAdd = decimalToAdd.toLong(&signalingFlags); - if (signalingFlags != Decimal128::SignalingFlag::kNoFlag || - overflow::add(longTotal, longToAdd, &longTotal)) { - uasserted(ErrorCodes::Overflow, "date overflow"); - } + case Date: + uassert(16612, "only one date allowed in an $add expression", !haveDate); + haveDate = true; + nonDecimalTotal.addLong(val.getDate().toMillisSinceEpoch()); break; - } default: - MONGO_UNREACHABLE; + uassert(16554, + str::stream() << "$add only supports numeric or date types, not " + << typeName(val.getType()), + val.nullish()); + return Value(BSONNULL); } } - long long longTotal = 0; - double doubleTotal = 0; - Decimal128 decimalTotal; - BSONType widestType = NumberInt; - bool isDate = false; -}; - -Status checkAddOperandType(Value val) { - if (!val.numeric() && val.getType() != Date) { - return Status(ErrorCodes::TypeMismatch, - str::stream() << "$add only supports numeric or date types, not " - << typeName(val.getType())); + if (haveDate) { + int64_t longTotal; + if (totalType == NumberDecimal) { + longTotal = decimalTotal.add(nonDecimalTotal.getDecimal()).toLong(); + } else { + uassert(ErrorCodes::Overflow, "date overflow in $add", nonDecimalTotal.fitsLong()); + longTotal = nonDecimalTotal.getLong(); + } + return Value(Date_t::fromMillisSinceEpoch(longTotal)); } - - return Status::OK(); -} -} // namespace - -StatusWith<Value> ExpressionAdd::apply(Value lhs, Value rhs) { - if (lhs.nullish()) - return Value(BSONNULL); - if (Status s = checkAddOperandType(lhs); !s.isOK()) - return s; - if (rhs.nullish()) - return Value(BSONNULL); - if (Status s = checkAddOperandType(rhs); !s.isOK()) - return s; - - AddState state; - state += lhs; - state += rhs; - return state.getValue(); -} - -Value ExpressionAdd::evaluate(const Document& root, Variables* variables) const { - AddState state; - for (auto&& child : _children) { - Value val = child->evaluate(root, variables); - if (val.nullish()) - return Value(BSONNULL); - uassertStatusOK(checkAddOperandType(val)); - state += val; + switch (totalType) { + case NumberDecimal: + return Value(decimalTotal.add(nonDecimalTotal.getDecimal())); + case NumberLong: + dassert(nonDecimalTotal.isInteger()); + if (nonDecimalTotal.fitsLong()) + return Value(nonDecimalTotal.getLong()); + // Fallthrough. + case NumberInt: + if (nonDecimalTotal.fitsLong()) + return Value::createIntOrLong(nonDecimalTotal.getLong()); + // Fallthrough. + case NumberDouble: + return Value(nonDecimalTotal.getDouble()); + default: + massert(16417, "$add resulted in a non-numeric type", false); } - return state.getValue(); } REGISTER_STABLE_EXPRESSION(add, ExpressionAdd::parse); @@ -655,16 +550,11 @@ Value ExpressionArray::evaluate(const Document& root, Variables* variables) cons return Value(std::move(values)); } -Value ExpressionArray::serialize(const SerializationOptions& options) const { - if (options.literalPolicy != LiteralSerializationPolicy::kUnchanged && - selfAndChildrenAreConstant()) { - return ExpressionConstant::serializeConstant( - options, evaluate(Document{}, &(getExpressionContext()->variables))); - } +Value ExpressionArray::serialize(bool explain) const { vector<Value> expressions; expressions.reserve(_children.size()); for (auto&& expr : _children) { - expressions.push_back(expr->serialize(options)); + expressions.push_back(expr->serialize(explain)); } return Value(std::move(expressions)); } @@ -687,15 +577,6 @@ intrusive_ptr<Expression> ExpressionArray::optimize() { return this; } -bool ExpressionArray::selfAndChildrenAreConstant() const { - for (auto&& exprPointer : _children) { - if (!exprPointer->selfAndChildrenAreConstant()) { - return false; - } - } - return true; -} - const char* ExpressionArray::getOpName() const { // This should never be called, but is needed to inherit from ExpressionNary. return "$array"; @@ -996,11 +877,11 @@ Value ExpressionCoerceToBool::evaluate(const Document& root, Variables* variable return Value(false); } -Value ExpressionCoerceToBool::serialize(const SerializationOptions& options) const { +Value ExpressionCoerceToBool::serialize(bool explain) const { // When not explaining, serialize to an $and expression. When parsed, the $and expression // will be optimized back into a ExpressionCoerceToBool. - const char* name = options.verbosity ? "$coerceToBool" : "$and"; - return Value(DOC(name << DOC_ARRAY(pExpression->serialize(options)))); + const char* name = explain ? "$coerceToBool" : "$and"; + return Value(DOC(name << DOC_ARRAY(pExpression->serialize(explain)))); } /* ----------------------- ExpressionCompare --------------------------- */ @@ -1249,8 +1130,8 @@ Value ExpressionConstant::evaluate(const Document& root, Variables* variables) c return _value; } -Value ExpressionConstant::serialize(const SerializationOptions& options) const { - return ExpressionConstant::serializeConstant(options, _value); +Value ExpressionConstant::serialize(bool explain) const { + return serializeConstant(_value); } REGISTER_STABLE_EXPRESSION(const, ExpressionConstant::parse); @@ -1464,20 +1345,20 @@ intrusive_ptr<Expression> ExpressionDateFromParts::optimize() { return this; } -Value ExpressionDateFromParts::serialize(const SerializationOptions& options) const { +Value ExpressionDateFromParts::serialize(bool explain) const { return Value(Document{ {"$dateFromParts", - Document{{"year", _year ? _year->serialize(options) : Value()}, - {"month", _month ? _month->serialize(options) : Value()}, - {"day", _day ? _day->serialize(options) : Value()}, - {"hour", _hour ? _hour->serialize(options) : Value()}, - {"minute", _minute ? _minute->serialize(options) : Value()}, - {"second", _second ? _second->serialize(options) : Value()}, - {"millisecond", _millisecond ? _millisecond->serialize(options) : Value()}, - {"isoWeekYear", _isoWeekYear ? _isoWeekYear->serialize(options) : Value()}, - {"isoWeek", _isoWeek ? _isoWeek->serialize(options) : Value()}, - {"isoDayOfWeek", _isoDayOfWeek ? _isoDayOfWeek->serialize(options) : Value()}, - {"timezone", _timeZone ? _timeZone->serialize(options) : Value()}}}}); + Document{{"year", _year ? _year->serialize(explain) : Value()}, + {"month", _month ? _month->serialize(explain) : Value()}, + {"day", _day ? _day->serialize(explain) : Value()}, + {"hour", _hour ? _hour->serialize(explain) : Value()}, + {"minute", _minute ? _minute->serialize(explain) : Value()}, + {"second", _second ? _second->serialize(explain) : Value()}, + {"millisecond", _millisecond ? _millisecond->serialize(explain) : Value()}, + {"isoWeekYear", _isoWeekYear ? _isoWeekYear->serialize(explain) : Value()}, + {"isoWeek", _isoWeek ? _isoWeek->serialize(explain) : Value()}, + {"isoDayOfWeek", _isoDayOfWeek ? _isoDayOfWeek->serialize(explain) : Value()}, + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}}}}); } bool ExpressionDateFromParts::evaluateNumberWithDefault(const Document& root, @@ -1721,14 +1602,14 @@ intrusive_ptr<Expression> ExpressionDateFromString::optimize() { return this; } -Value ExpressionDateFromString::serialize(const SerializationOptions& options) const { +Value ExpressionDateFromString::serialize(bool explain) const { return Value( Document{{"$dateFromString", - Document{{"dateString", _dateString->serialize(options)}, - {"timezone", _timeZone ? _timeZone->serialize(options) : Value()}, - {"format", _format ? _format->serialize(options) : Value()}, - {"onNull", _onNull ? _onNull->serialize(options) : Value()}, - {"onError", _onError ? _onError->serialize(options) : Value()}}}}); + Document{{"dateString", _dateString->serialize(explain)}, + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}, + {"format", _format ? _format->serialize(explain) : Value()}, + {"onNull", _onNull ? _onNull->serialize(explain) : Value()}, + {"onError", _onError ? _onError->serialize(explain) : Value()}}}}); } Value ExpressionDateFromString::evaluate(const Document& root, Variables* variables) const { @@ -1879,12 +1760,12 @@ intrusive_ptr<Expression> ExpressionDateToParts::optimize() { return this; } -Value ExpressionDateToParts::serialize(const SerializationOptions& options) const { +Value ExpressionDateToParts::serialize(bool explain) const { return Value( Document{{"$dateToParts", - Document{{"date", _date->serialize(options)}, - {"timezone", _timeZone ? _timeZone->serialize(options) : Value()}, - {"iso8601", _iso8601 ? _iso8601->serialize(options) : Value()}}}}); + Document{{"date", _date->serialize(explain)}, + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}, + {"iso8601", _iso8601 ? _iso8601->serialize(explain) : Value()}}}}); } boost::optional<int> ExpressionDateToParts::evaluateIso8601Flag(const Document& root, @@ -2037,13 +1918,13 @@ intrusive_ptr<Expression> ExpressionDateToString::optimize() { return this; } -Value ExpressionDateToString::serialize(const SerializationOptions& options) const { +Value ExpressionDateToString::serialize(bool explain) const { return Value( Document{{"$dateToString", - Document{{"date", _date->serialize(options)}, - {"format", _format ? _format->serialize(options) : Value()}, - {"timezone", _timeZone ? _timeZone->serialize(options) : Value()}, - {"onNull", _onNull ? _onNull->serialize(options) : Value()}}}}); + Document{{"date", _date->serialize(explain)}, + {"format", _format ? _format->serialize(explain) : Value()}, + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}, + {"onNull", _onNull ? _onNull->serialize(explain) : Value()}}}}); } Value ExpressionDateToString::evaluate(const Document& root, Variables* variables) const { @@ -2184,14 +2065,14 @@ boost::intrusive_ptr<Expression> ExpressionDateDiff::optimize() { return this; }; -Value ExpressionDateDiff::serialize(const SerializationOptions& options) const { +Value ExpressionDateDiff::serialize(bool explain) const { return Value{Document{ {"$dateDiff"_sd, - Document{{"startDate"_sd, _startDate->serialize(options)}, - {"endDate"_sd, _endDate->serialize(options)}, - {"unit"_sd, _unit->serialize(options)}, - {"timezone"_sd, _timeZone ? _timeZone->serialize(options) : Value{}}, - {"startOfWeek"_sd, _startOfWeek ? _startOfWeek->serialize(options) : Value{}}}}}}; + Document{{"startDate"_sd, _startDate->serialize(explain)}, + {"endDate"_sd, _endDate->serialize(explain)}, + {"unit"_sd, _unit->serialize(explain)}, + {"timezone"_sd, _timeZone ? _timeZone->serialize(explain) : Value{}}, + {"startOfWeek"_sd, _startOfWeek ? _startOfWeek->serialize(explain) : Value{}}}}}}; }; Date_t ExpressionDateDiff::convertToDate(const Value& value, StringData parameterName) { @@ -2253,17 +2134,6 @@ void ExpressionDateDiff::_doAddDependencies(DepsTracker* deps) const { } } -monotonic::State ExpressionDateDiff::getMonotonicState(const FieldPath& sortedFieldPath) const { - if (!ExpressionConstant::allNullOrConstant({_unit, _timeZone, _startOfWeek})) { - return monotonic::State::NonMonotonic; - } - // Because the result of this expression can be negative, this works the same way as - // ExpressionSubtract. Edge cases with DST and other timezone changes are handled correctly - // according to dateDiff. - return monotonic::combine(_endDate->getMonotonicState(sortedFieldPath), - monotonic::opposite(_startDate->getMonotonicState(sortedFieldPath))); -} - /* ----------------------- ExpressionDivide ---------------------------- */ Value ExpressionDivide::evaluate(const Document& root, Variables* variables) const { @@ -2405,24 +2275,10 @@ Value ExpressionObject::evaluate(const Document& root, Variables* variables) con return outputDoc.freezeToValue(); } -bool ExpressionObject::selfAndChildrenAreConstant() const { - for (auto&& [_, exprPointer] : _expressions) { - if (!exprPointer->selfAndChildrenAreConstant()) { - return false; - } - } - return true; -} - -Value ExpressionObject::serialize(const SerializationOptions& options) const { - if (options.literalPolicy != LiteralSerializationPolicy::kUnchanged && - selfAndChildrenAreConstant()) { - return ExpressionConstant::serializeConstant(options, Value(Document{})); - } +Value ExpressionObject::serialize(bool explain) const { MutableDocument outputDoc; for (auto&& pair : _expressions) { - outputDoc.addField(options.serializeFieldPathFromString(pair.first), - pair.second->serialize(options)); + outputDoc.addField(pair.first, pair.second->serialize(explain)); } return outputDoc.freezeToValue(); } @@ -2597,34 +2453,14 @@ Value ExpressionFieldPath::evaluate(const Document& root, Variables* variables) } } -namespace { -// Shared among expressions that need to serialize dotted paths and redact the path components. -auto getPrefixAndPath(FieldPath path) { - if (path.getFieldName(0) == "CURRENT" && path.getPathLength() > 1) { +Value ExpressionFieldPath::serialize(bool explain) const { + if (_fieldPath.getFieldName(0) == "CURRENT" && _fieldPath.getPathLength() > 1) { // use short form for "$$CURRENT.foo" but not just "$$CURRENT" - return std::make_pair(std::string("$"), path.tail()); + return Value("$" + _fieldPath.tail().fullPath()); } else { - return std::make_pair(std::string("$$"), path); + return Value("$$" + _fieldPath.fullPath()); } } -} // namespace - -Value ExpressionFieldPath::serialize(const SerializationOptions& options) const { - auto [prefix, path] = getPrefixAndPath(_fieldPath); - // First handles special cases for redaction of system variables. User variables will fall - // through to the default full redaction case. - if (options.transformIdentifiers && prefix.length() == 2) { - if (path.getPathLength() == 1 && Variables::isBuiltin(_variable)) { - // Nothing to redact for builtin variables. - return Value(prefix + path.fullPath()); - } else if (path.getPathLength() > 1 && Variables::isBuiltin(_variable)) { - // The first component of this path is a system variable, so keep that and redact - // the rest. - return Value(prefix + path.front() + "." + options.serializeFieldPath(path.tail())); - } - } - return Value(prefix + options.serializeFieldPath(path)); -} Expression::ComputedPaths ExpressionFieldPath::getComputedPaths(const std::string& exprFieldPath, Variables::Id renamingVar) const { @@ -2671,11 +2507,6 @@ std::unique_ptr<Expression> ExpressionFieldPath::copyWithSubstitution( return nullptr; } -monotonic::State ExpressionFieldPath::getMonotonicState(const FieldPath& sortedFieldPath) const { - return getFieldPathWithoutCurrentPrefix() == sortedFieldPath ? monotonic::State::Increasing - : monotonic::State::NonMonotonic; -} - /* ------------------------- ExpressionFilter ----------------------------- */ REGISTER_STABLE_EXPRESSION(filter, ExpressionFilter::parse); @@ -2765,14 +2596,14 @@ intrusive_ptr<Expression> ExpressionFilter::optimize() { return this; } -Value ExpressionFilter::serialize(const SerializationOptions& options) const { +Value ExpressionFilter::serialize(bool explain) const { if (_limit) { - return Value(DOC("$filter" << DOC("input" << _input->serialize(options) << "as" << _varName - << "cond" << _cond->serialize(options) << "limit" - << (*_limit)->serialize(options)))); + return Value(DOC("$filter" << DOC("input" << _input->serialize(explain) << "as" << _varName + << "cond" << _cond->serialize(explain) << "limit" + << (*_limit)->serialize(explain)))); } - return Value(DOC("$filter" << DOC("input" << _input->serialize(options) << "as" << _varName - << "cond" << _cond->serialize(options)))); + return Value(DOC("$filter" << DOC("input" << _input->serialize(explain) << "as" << _varName + << "cond" << _cond->serialize(explain)))); } Value ExpressionFilter::evaluate(const Document& root, Variables* variables) const { @@ -2964,19 +2795,15 @@ intrusive_ptr<Expression> ExpressionLet::optimize() { return this; } -Value ExpressionLet::serialize(const SerializationOptions& options) const { +Value ExpressionLet::serialize(bool explain) const { MutableDocument vars; for (VariableMap::const_iterator it = _variables.begin(), end = _variables.end(); it != end; ++it) { - auto key = it->second.name; - if (options.transformIdentifiers) { - key = options.transformIdentifiersCallback(key); - } - vars[key] = it->second.expression->serialize(options); + vars[it->second.name] = it->second.expression->serialize(explain); } return Value( - DOC("$let" << DOC("vars" << vars.freeze() << "in" << _subExpression->serialize(options)))); + DOC("$let" << DOC("vars" << vars.freeze() << "in" << _subExpression->serialize(explain)))); } Value ExpressionLet::evaluate(const Document& root, Variables* variables) const { @@ -3070,9 +2897,9 @@ intrusive_ptr<Expression> ExpressionMap::optimize() { return this; } -Value ExpressionMap::serialize(const SerializationOptions& options) const { - return Value(DOC("$map" << DOC("input" << _input->serialize(options) << "as" << _varName << "in" - << _each->serialize(options)))); +Value ExpressionMap::serialize(bool explain) const { + return Value(DOC("$map" << DOC("input" << _input->serialize(explain) << "as" << _varName << "in" + << _each->serialize(explain)))); } Value ExpressionMap::evaluate(const Document& root, Variables* variables) const { @@ -3154,14 +2981,11 @@ const std::string recordIdName = "recordId"; const std::string indexKeyName = "indexKey"; const std::string sortKeyName = "sortKey"; const std::string searchScoreDetailsName = "searchScoreDetails"; -const std::string searchSequenceTokenName = "searchSequenceToken"; const std::string timeseriesBucketMinTimeName = "timeseriesBucketMinTime"; const std::string timeseriesBucketMaxTimeName = "timeseriesBucketMaxTime"; -const std::string vectorSearchScoreName = "vectorSearchScore"; using MetaType = DocumentMetadataFields::MetaType; const StringMap<DocumentMetadataFields::MetaType> kMetaNameToMetaType = { - {vectorSearchScoreName, MetaType::kVectorSearchScore}, {geoNearDistanceName, MetaType::kGeoNearDist}, {geoNearPointName, MetaType::kGeoNearPoint}, {indexKeyName, MetaType::kIndexKey}, @@ -3170,7 +2994,6 @@ const StringMap<DocumentMetadataFields::MetaType> kMetaNameToMetaType = { {searchHighlightsName, MetaType::kSearchHighlights}, {searchScoreName, MetaType::kSearchScore}, {searchScoreDetailsName, MetaType::kSearchScoreDetails}, - {searchSequenceTokenName, MetaType::kSearchSequenceToken}, {sortKeyName, MetaType::kSortKey}, {textScoreName, MetaType::kTextScore}, {timeseriesBucketMinTimeName, MetaType::kTimeseriesBucketMinTime}, @@ -3178,7 +3001,6 @@ const StringMap<DocumentMetadataFields::MetaType> kMetaNameToMetaType = { }; const stdx::unordered_map<DocumentMetadataFields::MetaType, StringData> kMetaTypeToMetaName = { - {MetaType::kVectorSearchScore, vectorSearchScoreName}, {MetaType::kGeoNearDist, geoNearDistanceName}, {MetaType::kGeoNearPoint, geoNearPointName}, {MetaType::kIndexKey, indexKeyName}, @@ -3187,7 +3009,6 @@ const stdx::unordered_map<DocumentMetadataFields::MetaType, StringData> kMetaTyp {MetaType::kSearchHighlights, searchHighlightsName}, {MetaType::kSearchScore, searchScoreName}, {MetaType::kSearchScoreDetails, searchScoreDetailsName}, - {MetaType::kSearchSequenceToken, searchSequenceTokenName}, {MetaType::kSortKey, sortKeyName}, {MetaType::kTextScore, textScoreName}, {MetaType::kTimeseriesBucketMinTime, timeseriesBucketMinTimeName}, @@ -3214,7 +3035,7 @@ ExpressionMeta::ExpressionMeta(ExpressionContext* const expCtx, MetaType metaTyp expCtx->sbeCompatible = false; } -Value ExpressionMeta::serialize(const SerializationOptions& options) const { +Value ExpressionMeta::serialize(bool explain) const { const auto nameIter = kMetaTypeToMetaName.find(_metaType); invariant(nameIter != kMetaTypeToMetaName.end()); return Value(DOC("$meta" << nameIter->second)); @@ -3223,9 +3044,6 @@ Value ExpressionMeta::serialize(const SerializationOptions& options) const { Value ExpressionMeta::evaluate(const Document& root, Variables* variables) const { const auto& metadata = root.metadata(); switch (_metaType) { - case MetaType::kVectorSearchScore: - return metadata.hasVectorSearchScore() ? Value(metadata.getVectorSearchScore()) - : Value(); case MetaType::kTextScore: return metadata.hasTextScore() ? Value(metadata.getTextScore()) : Value(); case MetaType::kRandVal: @@ -3260,9 +3078,6 @@ Value ExpressionMeta::evaluate(const Document& root, Variables* variables) const case MetaType::kSearchScoreDetails: return metadata.hasSearchScoreDetails() ? Value(metadata.getSearchScoreDetails()) : Value(); - case MetaType::kSearchSequenceToken: - return metadata.hasSearchSequenceToken() ? Value(metadata.getSearchSequenceToken()) - : Value(); case MetaType::kTimeseriesBucketMinTime: return metadata.hasTimeseriesBucketMinTime() ? Value(metadata.getTimeseriesBucketMinTime()) @@ -3850,7 +3665,7 @@ Value ExpressionLn::evaluateNumericArg(const Value& numericArg) const { if (numericArg.getType() == NumberDecimal) { Decimal128 argDecimal = numericArg.getDecimal(); if (argDecimal.isGreater(Decimal128::kNormalizedZero)) - return Value(argDecimal.naturalLogarithm()); + return Value(argDecimal.logarithm()); // Fall through for error case. } double argDouble = numericArg.coerceToDouble(); @@ -4004,9 +3819,9 @@ Value toValue(const std::array<std::uint8_t, 32>& buf) { return Value(BSONBinData(vec.data(), vec.size(), BinDataType::Encrypt)); } -Value ExpressionInternalFLEEqual::serialize(const SerializationOptions& options) const { +Value ExpressionInternalFLEEqual::serialize(bool explain) const { return Value(Document{{kInternalFleEq, - Document{{"field", _children[0]->serialize(options)}, + Document{{"field", _children[0]->serialize(explain)}, {"edc", toValue(_edcToken)}, {"counter", Value(static_cast<long long>(_contentionFactor))}, {"server", toValue(_serverToken)}}}}); @@ -4161,22 +3976,13 @@ void ExpressionNary::addOperand(const intrusive_ptr<Expression>& pExpression) { _children.push_back(pExpression); } -Value ExpressionNary::serialize(const SerializationOptions& options) const { +Value ExpressionNary::serialize(bool explain) const { const size_t nOperand = _children.size(); vector<Value> array; /* build up the array */ - for (size_t i = 0; i < nOperand; i++) { - // If this input is a constant, bypass the standard serialization that wraps the - // representative value in $const. This does not lead to ambiguity for variadic operators - // but avoids bloating the representative shape for operators that have many inputs. - ExpressionConstant const* exprConst = dynamic_cast<ExpressionConstant*>(_children[i].get()); - if (exprConst) { - array.push_back(exprConst->serializeConstant( - options, exprConst->getValue(), false /* wrapRepresentativeValue */)); - } else { - array.push_back(_children[i]->serialize(options)); - } - } + for (size_t i = 0; i < nOperand; i++) + array.push_back(_children[i]->serialize(explain)); + return Value(DOC(getOpName() << array)); } @@ -4641,11 +4447,11 @@ void ExpressionReduce::_doAddDependencies(DepsTracker* deps) const { _in->addDependencies(deps); } -Value ExpressionReduce::serialize(const SerializationOptions& options) const { +Value ExpressionReduce::serialize(bool explain) const { return Value(Document{{"$reduce", - Document{{"input", _input->serialize(options)}, - {"initialValue", _initial->serialize(options)}, - {"in", _in->serialize(options)}}}}); + Document{{"input", _input->serialize(explain)}, + {"initialValue", _initial->serialize(explain)}, + {"in", _in->serialize(explain)}}}}); } /* ------------------------ ExpressionReplaceBase ------------------------ */ @@ -4656,11 +4462,11 @@ void ExpressionReplaceBase::_doAddDependencies(DepsTracker* deps) const { _replacement->addDependencies(deps); } -Value ExpressionReplaceBase::serialize(const SerializationOptions& options) const { +Value ExpressionReplaceBase::serialize(bool explain) const { return Value(Document{{getOpName(), - Document{{"input", _input->serialize(options)}, - {"find", _find->serialize(options)}, - {"replacement", _replacement->serialize(options)}}}}); + Document{{"input", _input->serialize(explain)}, + {"find", _find->serialize(explain)}, + {"replacement", _replacement->serialize(explain)}}}}); } namespace { @@ -4953,9 +4759,9 @@ void ExpressionSortArray::_doAddDependencies(DepsTracker* deps) const { _input->addDependencies(deps); } -Value ExpressionSortArray::serialize(const SerializationOptions& options) const { +Value ExpressionSortArray::serialize(bool explain) const { return Value(Document{{kName, - Document{{"input", _input->serialize(options)}, + Document{{"input", _input->serialize(explain)}, {"sortBy", _sortBy.getOriginalElement()}}}}); } @@ -5729,45 +5535,14 @@ StatusWith<Value> ExpressionSubtract::apply(Value lhs, Value rhs) { } else if (lhs.nullish() || rhs.nullish()) { return Value(BSONNULL); } else if (lhs.getType() == Date) { - BSONType rhsType = rhs.getType(); - switch (rhsType) { - case Date: - return Value(durationCount<Milliseconds>(lhs.getDate() - rhs.getDate())); - case NumberInt: - case NumberLong: { - long long longDiff = lhs.getDate().toMillisSinceEpoch(); - if (overflow::sub(longDiff, rhs.coerceToLong(), &longDiff)) { - return Status(ErrorCodes::Overflow, str::stream() << "date overflow"); - } - return Value(Date_t::fromMillisSinceEpoch(longDiff)); - } - case NumberDouble: { - using limits = std::numeric_limits<long long>; - long long longDiff = lhs.getDate().toMillisSinceEpoch(); - double doubleRhs = rhs.coerceToDouble(); - // check the doubleRhs should not exceed int64 limit and result will not overflow - if (doubleRhs >= static_cast<double>(limits::min()) && - doubleRhs < static_cast<double>(limits::max()) && - !overflow::sub(longDiff, llround(doubleRhs), &longDiff)) { - return Value(Date_t::fromMillisSinceEpoch(longDiff)); - } - return Status(ErrorCodes::Overflow, str::stream() << "date overflow"); - } - case NumberDecimal: { - long long longDiff = lhs.getDate().toMillisSinceEpoch(); - Decimal128 decimalRhs = rhs.coerceToDecimal(); - std::uint32_t signalingFlags = Decimal128::SignalingFlag::kNoFlag; - std::int64_t longRhs = decimalRhs.toLong(&signalingFlags); - if (signalingFlags != Decimal128::SignalingFlag::kNoFlag || - overflow::sub(longDiff, longRhs, &longDiff)) { - return Status(ErrorCodes::Overflow, str::stream() << "date overflow"); - } - return Value(Date_t::fromMillisSinceEpoch(longDiff)); - } - default: - return Status(ErrorCodes::TypeMismatch, - str::stream() - << "can't $subtract " << typeName(rhs.getType()) << " from Date"); + if (rhs.getType() == Date) { + return Value(durationCount<Milliseconds>(lhs.getDate() - rhs.getDate())); + } else if (rhs.numeric()) { + return Value(lhs.getDate() - Milliseconds(rhs.coerceToLong())); + } else { + return Status(ErrorCodes::TypeMismatch, + str::stream() + << "can't $subtract " << typeName(rhs.getType()) << " from Date"); } } else { return Status(ErrorCodes::TypeMismatch, @@ -5781,35 +5556,24 @@ const char* ExpressionSubtract::getOpName() const { return "$subtract"; } -monotonic::State ExpressionSubtract::getMonotonicState(const FieldPath& sortedFieldPath) const { - // 1. Get monotonic states of the both children. - // 2. Apply monotonic::opposite to the state of the second child, because it is negated. - // 3. Combine children. Function monotonic::combine correctly handles all the cases where, for - // example, argumemnts are both monotonic, but in the opposite directions. - return monotonic::combine( - getChildren()[0]->getMonotonicState(sortedFieldPath), - monotonic::opposite(getChildren()[1]->getMonotonicState(sortedFieldPath))); -} - /* ------------------------- ExpressionSwitch ------------------------------ */ REGISTER_STABLE_EXPRESSION(switch, ExpressionSwitch::parse); Value ExpressionSwitch::evaluate(const Document& root, Variables* variables) const { - for (int i = 0; i < numBranches(); ++i) { - auto [caseExpr, thenExpr] = getBranch(i); - Value caseResult = caseExpr->evaluate(root, variables); + for (auto&& branch : _branches) { + Value caseExpression(branch.first->evaluate(root, variables)); - if (caseResult.coerceToBool()) { - return thenExpr->evaluate(root, variables); + if (caseExpression.coerceToBool()) { + return branch.second->evaluate(root, variables); } } uassert(40066, "$switch could not find a matching branch for an input, and no default was specified.", - defaultExpr()); + _default); - return defaultExpr()->evaluate(root, variables); + return _default->evaluate(root, variables); } boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* const expCtx, @@ -5818,7 +5582,7 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* cons uassert(40060, str::stream() << "$switch requires an object as an argument, found: " << typeName(expr.type()), - expr.type() == BSONType::Object); + expr.type() == Object); boost::intrusive_ptr<Expression> expDefault; std::vector<boost::intrusive_ptr<Expression>> children; @@ -5830,13 +5594,13 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* cons uassert(40061, str::stream() << "$switch expected an array for 'branches', found: " << typeName(elem.type()), - elem.type() == BSONType::Array); + elem.type() == Array); for (auto&& branch : elem.Array()) { uassert(40062, str::stream() << "$switch expected each branch to be an object, found: " << typeName(branch.type()), - branch.type() == BSONType::Object); + branch.type() == Object); boost::intrusive_ptr<Expression> switchCase, switchThen; @@ -5868,96 +5632,98 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* cons uasserted(40067, str::stream() << "$switch found an unknown argument: " << field); } } - - // The the 'default' expression is always the final child. If no 'default' expression is - // provided, then the final child is nullptr. children.push_back(std::move(expDefault)); + // Obtain references to the case and branch expressions two-by-two from the children vector, + // ignore the last. + std::vector<ExpressionPair> branches; + boost::optional<boost::intrusive_ptr<Expression>&> first; + for (auto&& child : children) { + if (first) { + branches.emplace_back(*first, child); + first = boost::none; + } else { + first = child; + } + } - return new ExpressionSwitch(expCtx, std::move(children)); -} + uassert(40068, "$switch requires at least one branch.", !branches.empty()); -void ExpressionSwitch::deleteBranch(int i) { - invariant(i >= 0); - invariant(i < numBranches()); - // Delete the two elements corresponding to this branch at positions 2i and 2i + 1. - _children.erase(std::next(_children.begin(), i * 2), std::next(_children.begin(), i * 2 + 2)); + return new ExpressionSwitch(expCtx, std::move(children), std::move(branches)); } void ExpressionSwitch::_doAddDependencies(DepsTracker* deps) const { - for (auto&& child : _children) { - // Check for nullptr, since we leave a nullptr as the final child when the 'default' - // expression is missing. - if (child) { - child->addDependencies(deps); - } + for (auto&& branch : _branches) { + branch.first->addDependencies(deps); + branch.second->addDependencies(deps); + } + + if (_default) { + _default->addDependencies(deps); } } boost::intrusive_ptr<Expression> ExpressionSwitch::optimize() { - if (defaultExpr()) { - _children.back() = _children.back()->optimize(); + if (_default) { + _default = _default->optimize(); } - bool trueConst = false; + std::vector<ExpressionPair>::iterator it = _branches.begin(); + bool true_const = false; - int i = 0; - while (!trueConst && i < numBranches()) { - boost::intrusive_ptr<Expression>& caseExpr = _children[i * 2]; - boost::intrusive_ptr<Expression>& thenExpr = _children[i * 2 + 1]; - caseExpr = caseExpr->optimize(); + while (!true_const && it != _branches.end()) { + (it->first) = (it->first)->optimize(); - if (auto* val = dynamic_cast<ExpressionConstant*>(caseExpr.get())) { - if (!val->getValue().coerceToBool()) { + if (auto* val = dynamic_cast<ExpressionConstant*>((it->first).get())) { + if (!((val->getValue()).coerceToBool())) { // Case is constant and evaluates to false, so it is removed. - deleteBranch(i); + it = _branches.erase(it); } else { - // Case optimized to a constant true value. Set the optimized version of the - // corresponding 'then' expression as the new 'default'. Break out of the loop and - // fall through to the logic to remove this and all subsequent branches. - trueConst = true; - _children.back() = thenExpr->optimize(); - break; + // Case is constant and true so it is set to default and then removed. + true_const = true; + + // Optimizing this case's then, so that default will remain optimized. + (it->second) = (it->second)->optimize(); + _default = it->second; + it = _branches.erase(it); } } else { // Since case is not removed from the switch, its then is now optimized. - thenExpr = thenExpr->optimize(); - ++i; + (it->second) = (it->second)->optimize(); + ++it; } } // Erasing the rest of the cases because found a default true value. - if (trueConst) { - while (i < numBranches()) { - deleteBranch(i); - } + if (true_const) { + _branches.erase(it, _branches.end()); } // If there are no cases, make the switch its default. - if (numBranches() == 0) { + if (_branches.size() == 0 && _default) { + return _default; + } else if (_branches.size() == 0) { uassert(40069, - "Cannot execute a switch statement where all the cases evaluate to false " - "without a default", - defaultExpr()); - return _children.back(); + "One cannot execute a switch statement where all the cases evaluate to false " + "without a default.", + _branches.size()); } return this; } -Value ExpressionSwitch::serialize(const SerializationOptions& options) const { +Value ExpressionSwitch::serialize(bool explain) const { std::vector<Value> serializedBranches; - serializedBranches.reserve(numBranches()); + serializedBranches.reserve(_branches.size()); - for (int i = 0; i < numBranches(); ++i) { - auto [caseExpr, thenExpr] = getBranch(i); - serializedBranches.push_back(Value(Document{{"case", caseExpr->serialize(options)}, - {"then", thenExpr->serialize(options)}})); + for (auto&& branch : _branches) { + serializedBranches.push_back(Value(Document{{"case", branch.first->serialize(explain)}, + {"then", branch.second->serialize(explain)}})); } - if (defaultExpr()) { + if (_default) { return Value(Document{{"$switch", Document{{"branches", Value(serializedBranches)}, - {"default", defaultExpr()->serialize(options)}}}}); + {"default", _default->serialize(explain)}}}}); } return Value(Document{{"$switch", Document{{"branches", Value(serializedBranches)}}}}); @@ -6199,11 +5965,11 @@ boost::intrusive_ptr<Expression> ExpressionTrim::optimize() { return this; } -Value ExpressionTrim::serialize(const SerializationOptions& options) const { +Value ExpressionTrim::serialize(bool explain) const { return Value( Document{{_name, - Document{{"input", _input->serialize(options)}, - {"chars", _characters ? _characters->serialize(options) : Value()}}}}); + Document{{"input", _input->serialize(explain)}, + {"chars", _characters ? _characters->serialize(explain) : Value()}}}}); } void ExpressionTrim::_doAddDependencies(DepsTracker* deps) const { @@ -6501,17 +6267,17 @@ boost::intrusive_ptr<Expression> ExpressionZip::optimize() { return this; } -Value ExpressionZip::serialize(const SerializationOptions& options) const { +Value ExpressionZip::serialize(bool explain) const { vector<Value> serializedInput; vector<Value> serializedDefaults; Value serializedUseLongestLength = Value(_useLongestLength); for (auto&& expr : _inputs) { - serializedInput.push_back(expr.get()->serialize(options)); + serializedInput.push_back(expr.get()->serialize(explain)); } for (auto&& expr : _defaults) { - serializedDefaults.push_back(expr.get()->serialize(options)); + serializedDefaults.push_back(expr.get()->serialize(explain)); } return Value(DOC("$zip" << DOC("inputs" << Value(serializedInput) << "defaults" @@ -6905,7 +6671,7 @@ private: } else if (doubleValue == 0.0 && std::signbit(doubleValue)) { return Value("-0"_sd); } else { - return Value(static_cast<std::string>(str::stream() << fmt::format("{}", doubleValue))); + return Value(static_cast<std::string>(str::stream() << doubleValue)); } } @@ -7092,6 +6858,14 @@ boost::intrusive_ptr<Expression> ExpressionConvert::optimize() { return this; } +Value ExpressionConvert::serialize(bool explain) const { + return Value(Document{{"$convert", + Document{{"input", _input->serialize(explain)}, + {"to", _to->serialize(explain)}, + {"onError", _onError ? _onError->serialize(explain) : Value()}, + {"onNull", _onNull ? _onNull->serialize(explain) : Value()}}}}); +} + void ExpressionConvert::_doAddDependencies(DepsTracker* deps) const { _input->addDependencies(deps); _to->addDependencies(deps); @@ -7103,29 +6877,6 @@ void ExpressionConvert::_doAddDependencies(DepsTracker* deps) const { } } -Value ExpressionConvert::serialize(const SerializationOptions& options) const { - // Since the 'to' field is a parameter from a set of valid values and not free user input, - // we want to avoid boiling it down to the representative value in the query shape. The first - // condition is so that we can keep serializing correctly whenever the 'to' field is an - // expression that gets resolved down to a string of a valid type, or its corresponding - // numerical value. If it's just the constant, we want to wrap it in a $const except when the - // serialization policy is debug. - auto constExpr = dynamic_cast<ExpressionConstant*>(_to.get()); - Value toField = Value(); - if (!constExpr) { - toField = _to->serialize(options); - } else if (options.literalPolicy == LiteralSerializationPolicy::kToDebugTypeString) { - toField = constExpr->getValue(); - } else { - toField = Value(DOC("$const" << constExpr->getValue())); - } - return Value(Document{{"$convert", - Document{{"input", _input->serialize(options)}, - {"to", toField}, - {"onError", _onError ? _onError->serialize(options) : Value()}, - {"onNull", _onNull ? _onNull->serialize(options) : Value()}}}}); -} - BSONType ExpressionConvert::computeTargetType(Value targetTypeName) const { BSONType targetType; if (targetTypeName.getType() == BSONType::String) { @@ -7388,12 +7139,12 @@ void ExpressionRegex::_compile(RegexExecutionState* executionState) const { executionState->capturesBuffer.resize((1 + executionState->numCaptures) * 3); } -Value ExpressionRegex::serialize(const SerializationOptions& options) const { +Value ExpressionRegex::serialize(bool explain) const { return Value( Document{{_opName, - Document{{"input", _input->serialize(options)}, - {"regex", _regex->serialize(options)}, - {"options", _options ? _options->serialize(options) : Value()}}}}); + Document{{"input", _input->serialize(explain)}, + {"regex", _regex->serialize(explain)}, + {"options", _options ? _options->serialize(explain) : Value()}}}}); } void ExpressionRegex::_extractInputField(RegexExecutionState* executionState, @@ -7656,7 +7407,7 @@ void ExpressionRandom::_doAddDependencies(DepsTracker* deps) const { deps->needRandomGenerator = true; } -Value ExpressionRandom::serialize(const SerializationOptions& options) const { +Value ExpressionRandom::serialize(const bool explain) const { return Value(DOC(getOpName() << Document())); } @@ -7679,8 +7430,8 @@ Value ExpressionToHashedIndexKey::evaluate(const Document& root, Variables* vari BSONElementHasher::DEFAULT_HASH_SEED)); } -Value ExpressionToHashedIndexKey::serialize(const SerializationOptions& options) const { - return Value(DOC("$toHashedIndexKey" << _children[0]->serialize(options))); +Value ExpressionToHashedIndexKey::serialize(bool explain) const { + return Value(DOC("$toHashedIndexKey" << _children[0]->serialize(explain))); } void ExpressionToHashedIndexKey::_doAddDependencies(DepsTracker* deps) const { @@ -7756,13 +7507,13 @@ boost::intrusive_ptr<Expression> ExpressionDateArithmetics::optimize() { return intrusive_ptr<Expression>(this); } -Value ExpressionDateArithmetics::serialize(const SerializationOptions& options) const { +Value ExpressionDateArithmetics::serialize(bool explain) const { return Value( Document{{_opName, - Document{{"startDate", _startDate->serialize(options)}, - {"unit", _unit->serialize(options)}, - {"amount", _amount->serialize(options)}, - {"timezone", _timeZone ? _timeZone->serialize(options) : Value()}}}}); + Document{{"startDate", _startDate->serialize(explain)}, + {"unit", _unit->serialize(explain)}, + {"amount", _amount->serialize(explain)}, + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}}}}); } Value ExpressionDateArithmetics::evaluate(const Document& root, Variables* variables) const { @@ -7800,15 +7551,6 @@ Value ExpressionDateArithmetics::evaluate(const Document& root, Variables* varia startDate.coerceToDate(), unit, amount.coerceToLong(), timezone.get()); } -monotonic::State ExpressionDateArithmetics::getMonotonicState( - const FieldPath& sortedFieldPath) const { - if (!ExpressionConstant::allNullOrConstant({_unit, _timeZone})) { - return monotonic::State::NonMonotonic; - } - return combineMonotonicStateOfArguments(_startDate->getMonotonicState(sortedFieldPath), - _amount->getMonotonicState(sortedFieldPath)); -} - /* ----------------------- ExpressionDateAdd ---------------------------- */ REGISTER_STABLE_EXPRESSION(dateAdd, ExpressionDateAdd::parse); @@ -7833,11 +7575,6 @@ Value ExpressionDateAdd::evaluateDateArithmetics(Date_t date, return Value(dateAdd(date, unit, amount, timezone)); } -monotonic::State ExpressionDateAdd::combineMonotonicStateOfArguments( - monotonic::State startDataMonotonicState, monotonic::State amountMonotonicState) const { - return monotonic::combine(startDataMonotonicState, amountMonotonicState); -} - /* ----------------------- ExpressionDateSubtract ---------------------------- */ REGISTER_STABLE_EXPRESSION(dateSubtract, ExpressionDateSubtract::parse); @@ -7867,11 +7604,6 @@ Value ExpressionDateSubtract::evaluateDateArithmetics(Date_t date, return Value(dateAdd(date, unit, -amount, timezone)); } -monotonic::State ExpressionDateSubtract::combineMonotonicStateOfArguments( - monotonic::State startDataMonotonicState, monotonic::State amountMonotonicState) const { - return monotonic::combine(startDataMonotonicState, amountMonotonicState); -} - /* ----------------------- ExpressionDateTrunc ---------------------------- */ REGISTER_STABLE_EXPRESSION(dateTrunc, ExpressionDateTrunc::parse); @@ -7955,14 +7687,14 @@ boost::intrusive_ptr<Expression> ExpressionDateTrunc::optimize() { return this; }; -Value ExpressionDateTrunc::serialize(const SerializationOptions& options) const { +Value ExpressionDateTrunc::serialize(bool explain) const { return Value{Document{ {"$dateTrunc"_sd, - Document{{"date"_sd, _date->serialize(options)}, - {"unit"_sd, _unit->serialize(options)}, - {"binSize"_sd, _binSize ? _binSize->serialize(options) : Value{}}, - {"timezone"_sd, _timeZone ? _timeZone->serialize(options) : Value{}}, - {"startOfWeek"_sd, _startOfWeek ? _startOfWeek->serialize(options) : Value{}}}}}}; + Document{{"date"_sd, _date->serialize(explain)}, + {"unit"_sd, _unit->serialize(explain)}, + {"binSize"_sd, _binSize ? _binSize->serialize(explain) : Value{}}, + {"timezone"_sd, _timeZone ? _timeZone->serialize(explain) : Value{}}, + {"startOfWeek"_sd, _startOfWeek ? _startOfWeek->serialize(explain) : Value{}}}}}}; }; Date_t ExpressionDateTrunc::convertToDate(const Value& value) { @@ -8044,13 +7776,6 @@ void ExpressionDateTrunc::_doAddDependencies(DepsTracker* deps) const { } } -monotonic::State ExpressionDateTrunc::getMonotonicState(const FieldPath& sortedFieldPath) const { - if (!ExpressionConstant::allNullOrConstant({_unit, _binSize, _timeZone, _startOfWeek})) { - return monotonic::State::NonMonotonic; - } - return _date->getMonotonicState(sortedFieldPath); -} - /* -------------------------- ExpressionGetField ------------------------------ */ REGISTER_EXPRESSION_WITH_MIN_VERSION( getField, @@ -8144,6 +7869,7 @@ Value ExpressionGetField::evaluate(const Document& root, Variables* variables) c return Value(); } + return inputValue.getDocument().getField(fieldValue.getString()); } @@ -8156,22 +7882,10 @@ void ExpressionGetField::_doAddDependencies(DepsTracker* deps) const { _field->addDependencies(deps); } -Value ExpressionGetField::serialize(const SerializationOptions& options) const { - // The parser guarantees that the '_field' expression evaluates to a constant string. - auto strPath = static_cast<ExpressionConstant*>(_field.get())->getValue().getString(); - - Value maybeRedactedPath{options.serializeFieldPathFromString(strPath)}; - // This is a pretty unique option to serialize. It is both a constant and a field path, which - // means that it: - // - should be redacted (if that option is set). - // - should *not* be wrapped in $const iff we are serializing for a debug string - if (options.literalPolicy != LiteralSerializationPolicy::kToDebugTypeString) { - maybeRedactedPath = Value(Document{{"$const"_sd, maybeRedactedPath}}); - } - +Value ExpressionGetField::serialize(const bool explain) const { return Value(Document{{"$getField"_sd, - Document{{"field"_sd, std::move(maybeRedactedPath)}, - {"input"_sd, _input->serialize(options)}}}}); + Document{{"field"_sd, _field->serialize(explain)}, + {"input"_sd, _input->serialize(explain)}}}}); } /* -------------------------- ExpressionSetField ------------------------------ */ @@ -8295,23 +8009,11 @@ void ExpressionSetField::_doAddDependencies(DepsTracker* deps) const { _value->addDependencies(deps); } -Value ExpressionSetField::serialize(const SerializationOptions& options) const { - // The parser guarantees that the '_field' expression evaluates to a constant string. - auto strPath = static_cast<ExpressionConstant*>(_field.get())->getValue().getString(); - - Value maybeRedactedPath{options.serializeFieldPathFromString(strPath)}; - // This is a pretty unique option to serialize. It is both a constant and a field path, which - // means that it: - // - should be redacted (if that option is set). - // - should *not* be wrapped in $const iff we are serializing for a debug string - if (options.literalPolicy != LiteralSerializationPolicy::kToDebugTypeString) { - maybeRedactedPath = Value(Document{{"$const"_sd, maybeRedactedPath}}); - } - +Value ExpressionSetField::serialize(const bool explain) const { return Value(Document{{"$setField"_sd, - Document{{"field"_sd, std::move(maybeRedactedPath)}, - {"input"_sd, _input->serialize(options)}, - {"value"_sd, _value->serialize(options)}}}}); + Document{{"field"_sd, _field->serialize(explain)}, + {"input"_sd, _input->serialize(explain)}, + {"value"_sd, _value->serialize(explain)}}}}); } /* ------------------------- ExpressionTsSecond ----------------------------- */ @@ -8364,90 +8066,4 @@ REGISTER_EXPRESSION_WITH_MIN_VERSION( MONGO_INITIALIZER_GROUP(BeginExpressionRegistration, ("default"), ("EndExpressionRegistration")) MONGO_INITIALIZER_GROUP(EndExpressionRegistration, ("BeginExpressionRegistration"), ()) - -/* ----------------------- ExpressionInternalKeyStringValue ---------------------------- */ - -REGISTER_STABLE_EXPRESSION(_internalKeyStringValue, ExpressionInternalKeyStringValue::parse); - -boost::intrusive_ptr<Expression> ExpressionInternalKeyStringValue::parse( - ExpressionContext* expCtx, BSONElement expr, const VariablesParseState& vps) { - - uassert( - 8281500, - str::stream() << "$_internalKeyStringValue only supports an object as its argument, not " - << typeName(expr.type()), - expr.type() == BSONType::Object); - - boost::intrusive_ptr<Expression> inputExpr; - boost::intrusive_ptr<Expression> collationExpr; - - for (auto&& element : expr.embeddedObject()) { - auto field = element.fieldNameStringData(); - if ("input"_sd == field) { - inputExpr = parseOperand(expCtx, element, vps); - } else if ("collation"_sd == field) { - collationExpr = parseOperand(expCtx, element, vps); - } else { - uasserted(8281501, - str::stream() << "Unrecognized argument to $_internalKeyStringValue: " - << element.fieldName()); - } - } - uassert(8281502, - str::stream() << "$_internalKeyStringValue requires 'input' to be specified", - inputExpr); - - return make_intrusive<ExpressionInternalKeyStringValue>(expCtx, inputExpr, collationExpr); -} - -Value ExpressionInternalKeyStringValue::serialize(const SerializationOptions& options) const { - return Value( - Document{{getOpName(), - Document{{"input", _children[_kInput]->serialize(options)}, - {"collation", - _children[_kCollation] ? _children[_kCollation]->serialize(options) - : Value()}}}}); -} - -Value ExpressionInternalKeyStringValue::evaluate(const Document& root, Variables* variables) const { - const Value input = _children[_kInput]->evaluate(root, variables); - auto inputBson = input.wrap(""); - - std::unique_ptr<CollatorInterface> collator = nullptr; - if (_children[_kCollation]) { - const Value collation = _children[_kCollation]->evaluate(root, variables); - uassert(8281503, - str::stream() << "Collation spec must be an object, not " - << typeName(collation.getType()), - collation.isObject()); - auto collationBson = collation.getDocument().toBson(); - - auto collatorFactory = - CollatorFactoryInterface::get(getExpressionContext()->opCtx->getServiceContext()); - collator = uassertStatusOKWithContext(collatorFactory->makeFromBSON(collationBson), - "Invalid collation spec"); - } - - KeyString::HeapBuilder ksBuilder(KeyString::Version::V1); - if (collator) { - ksBuilder.appendBSONElement(inputBson.firstElement(), [&](StringData str) { - return collator->getComparisonString(str); - }); - } else { - ksBuilder.appendBSONElement(inputBson.firstElement()); - } - auto ksValue = ksBuilder.release(); - - // The result omits the typebits so that the numeric value of different types have the same - // binary representation. - return Value( - BSONBinData{ksValue.getBuffer(), static_cast<int>(ksValue.getSize()), BinDataGeneral}); -} - -void ExpressionInternalKeyStringValue::_doAddDependencies(DepsTracker* deps) const { - _children[_kInput]->addDependencies(deps); - if (_children[_kCollation]) { - _children[_kCollation]->addDependencies(deps); - } -} } // namespace mongo diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 3520fb6d7a9..9cce6d0b1e2 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -49,12 +49,10 @@ #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/expression_visitor.h" #include "mongo/db/pipeline/field_path.h" -#include "mongo/db/pipeline/monotonic_expression.h" #include "mongo/db/pipeline/variables.h" #include "mongo/db/query/allowed_contexts.h" #include "mongo/db/query/datetime/date_time_support.h" #include "mongo/db/query/query_feature_flags_gen.h" -#include "mongo/db/query/query_shape/serialization_options.h" #include "mongo/db/query/sort_pattern.h" #include "mongo/db/server_options.h" #include "mongo/db/update/pattern_cmp.h" @@ -172,7 +170,7 @@ public: */ struct ComputedPaths { // Non-rename computed paths. - OrderedPathSet paths; + std::set<std::string> paths; // Mappings from the old name of a path before applying this expression, to the new one // after applying this expression. @@ -224,7 +222,7 @@ public: * If 'explain' is false, the returned Value must result in the same Expression when parsed by * parseOperand(). */ - virtual Value serialize(const SerializationOptions& options = {}) const = 0; + virtual Value serialize(bool explain) const = 0; /** * Evaluate the expression with respect to the Document given by 'root' and the Variables given @@ -341,25 +339,6 @@ public: return _expCtx; } - boost::optional<Variables::Id> getBoundaryVariableId() const { - return _boundaryVariableId; - } - - bool isMonotonic(const FieldPath& sortedFieldPath) const { - return getMonotonicState(sortedFieldPath) != monotonic::State::NonMonotonic; - } - - virtual monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const { - return monotonic::State::NonMonotonic; - } - - /** - * Helper to determine whether this expression always evaluates to the same value. - */ - virtual bool selfAndChildrenAreConstant() const { - return false; - } - protected: using ExpressionVector = std::vector<boost::intrusive_ptr<Expression>>; @@ -392,105 +371,12 @@ private: }; /** - * A constant expression. Repeated calls to evaluate() will always return the same thing. - */ -class ExpressionConstant final : public Expression { -public: - ExpressionConstant(ExpressionContext* expCtx, const Value& value); - - boost::intrusive_ptr<Expression> optimize() final; - Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options = {}) const final; - - const char* getOpName() const; - - /** - * Creates a new ExpressionConstant with value 'value'. - */ - static boost::intrusive_ptr<ExpressionConstant> create(ExpressionContext* expCtx, - const Value& value); - - static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, - BSONElement bsonExpr, - const VariablesParseState& vps); - - /** - * Returns true if 'expression' is nullptr or if 'expression' is an instance of an - * ExpressionConstant. - */ - static bool isNullOrConstant(boost::intrusive_ptr<Expression> expression) { - return !expression || dynamic_cast<ExpressionConstant*>(expression.get()); - } - - /** - * Returns true if 'expression' is an instance of an ExpressionConstant. - */ - static bool isConstant(boost::intrusive_ptr<Expression> expression) { - return dynamic_cast<ExpressionConstant*>(expression.get()); - } - - static Value serializeConstant(const SerializationOptions& opts, - Value val, - bool wrapRepresentativeValue = true); - - bool selfAndChildrenAreConstant() const override final { - return true; - } - - /** - * Returns true if every expression in 'expressions' is either a nullptr or an instance of an - * ExpressionConstant. - */ - static bool allNullOrConstant( - const std::initializer_list<boost::intrusive_ptr<Expression>>& expressions) { - return std::all_of(expressions.begin(), expressions.end(), [](auto exp) { - return ExpressionConstant::isNullOrConstant(exp); - }); - } - template <typename ExpressionContainer> - static bool allConstant(const ExpressionContainer& expressions) { - return std::all_of(expressions.begin(), expressions.end(), [](auto exp) { - return ExpressionConstant::isConstant(exp); - }); - } - - /** - * Returns the constant value represented by this Expression. - */ - Value getValue() const { - return _value; - } - - void setValue(const Value& value) { - _value = value; - }; - - void acceptVisitor(ExpressionMutableVisitor* visitor) final { - return visitor->visit(this); - } - - void acceptVisitor(ExpressionConstVisitor* visitor) const final { - return visitor->visit(this); - } - -protected: - void _doAddDependencies(DepsTracker* deps) const override; - -private: - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { - return monotonic::State::Constant; - } - - Value _value; -}; - -/** * Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class. */ class ExpressionNary : public Expression { public: boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + Value serialize(bool explain) const override; /* Add an operand to the n-ary expression. @@ -555,33 +441,6 @@ public: : ExpressionNaryBase<SubClass>(expCtx) {} ExpressionVariadic(ExpressionContext* const expCtx, Expression::ExpressionVector&& children) : ExpressionNaryBase<SubClass>(expCtx, std::move(children)) {} - - Value serialize(const SerializationOptions& options = {}) const { - // As a special case, we would like to serialize a variadic number of children as - // "?array<?subtype>" if they are all constant. Check for that here, otherwise default to - // the normal one-by-one serialization of the children. - if (options.literalPolicy == LiteralSerializationPolicy::kToDebugTypeString && - ExpressionConstant::allConstant(this->_children)) { - // We could evaluate the expression right here and now and end up with just the one - // constant answer, but this is not an optimization funciton, it is meant to just - // serialize what we have, so let's preserve the array of constants. - auto args = [&]() { - std::vector<Value> values; - const auto& constants = this->_children; - values.reserve(constants.size()); - std::transform(constants.begin(), - constants.end(), - std::back_inserter(values), - [](const auto& exp) { - return static_cast<ExpressionConstant*>(exp.get())->getValue(); - }); - return values; - }(); - return Value(Document{ - {this->getOpName(), ExpressionConstant::serializeConstant(options, Value(args))}}); - } - return ExpressionNary::serialize(options); - } }; /** @@ -697,9 +556,9 @@ public: return AccumulatorN::kName.rawData(); } - Value serialize(const SerializationOptions& options = {}) const { + Value serialize(bool explain) const { MutableDocument md; - AccumulatorN::serializeHelper(_n, _output, options, md); + AccumulatorN::serializeHelper(_n, _output, explain, md); return Value(DOC(getOpName() << md.freeze())); } @@ -814,6 +673,74 @@ public: }; /** + * A constant expression. Repeated calls to evaluate() will always return the same thing. + */ +class ExpressionConstant final : public Expression { +public: + ExpressionConstant(ExpressionContext* expCtx, const Value& value); + + boost::intrusive_ptr<Expression> optimize() final; + Value evaluate(const Document& root, Variables* variables) const final; + Value serialize(bool explain) const final; + + const char* getOpName() const; + + /** + * Creates a new ExpressionConstant with value 'value'. + */ + static boost::intrusive_ptr<ExpressionConstant> create(ExpressionContext* expCtx, + const Value& value); + + static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, + BSONElement bsonExpr, + const VariablesParseState& vps); + + /** + * Returns true if 'expression' is nullptr or if 'expression' is an instance of an + * ExpressionConstant. + */ + static bool isNullOrConstant(boost::intrusive_ptr<Expression> expression) { + return !expression || dynamic_cast<ExpressionConstant*>(expression.get()); + } + + /** + * Returns true if every expression in 'expressions' is either a nullptr or an instance of an + * ExpressionConstant. + */ + static bool allNullOrConstant( + const std::initializer_list<boost::intrusive_ptr<Expression>>& expressions) { + return std::all_of(expressions.begin(), expressions.end(), [](auto exp) { + return ExpressionConstant::isNullOrConstant(exp); + }); + } + + /** + * Returns the constant value represented by this Expression. + */ + Value getValue() const { + return _value; + } + + void setValue(const Value& value) { + _value = value; + }; + + void acceptVisitor(ExpressionMutableVisitor* visitor) final { + return visitor->visit(this); + } + + void acceptVisitor(ExpressionConstVisitor* visitor) const final { + return visitor->visit(this); + } + +protected: + void _doAddDependencies(DepsTracker* deps) const override; + +private: + Value _value; +}; + +/** * Inherit from this class if your expression works with date types, and accepts either a single * argument which is a date, or an object {date: <date>, timezone: <string>}. */ @@ -854,11 +781,11 @@ public: * Always serializes to the full {date: <date arg>, timezone: <timezone arg>} format, leaving * off the timezone if not specified. */ - Value serialize(const SerializationOptions& options = {}) const final { - auto timezone = _timeZone ? _timeZone->serialize(options) : Value(); + Value serialize(bool explain) const final { + auto timezone = _timeZone ? _timeZone->serialize(explain) : Value(); return Value(Document{ {_opName, - Document{{"date", _date->serialize(options)}, {"timezone", std::move(timezone)}}}}); + Document{{"date", _date->serialize(explain)}, {"timezone", std::move(timezone)}}}}); } boost::intrusive_ptr<Expression> optimize() final { @@ -1016,11 +943,6 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } - -private: - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { - return monotonic::combineExpressions(sortedFieldPath, getChildren()); - }; }; @@ -1112,7 +1034,7 @@ public: } Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; static boost::intrusive_ptr<ExpressionArray> create( ExpressionContext* const expCtx, std::vector<boost::intrusive_ptr<Expression>>&& children) { @@ -1129,8 +1051,6 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } - - bool selfAndChildrenAreConstant() const override final; }; @@ -1268,11 +1188,6 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } - -private: - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { - return getChildren()[0]->getMonotonicState(sortedFieldPath); - } }; @@ -1280,7 +1195,7 @@ class ExpressionCoerceToBool final : public Expression { public: boost::intrusive_ptr<Expression> optimize() final; Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; static boost::intrusive_ptr<ExpressionCoerceToBool> create( ExpressionContext* expCtx, boost::intrusive_ptr<Expression> pExpression); @@ -1443,8 +1358,8 @@ public: boost::intrusive_ptr<Expression> onNull, boost::intrusive_ptr<Expression> onError); - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -1485,8 +1400,8 @@ public: boost::intrusive_ptr<Expression> isoDayOfWeek, boost::intrusive_ptr<Expression> timeZone); - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -1565,8 +1480,8 @@ public: boost::intrusive_ptr<Expression> timeZone, boost::intrusive_ptr<Expression> iso8601); - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -1599,8 +1514,8 @@ public: boost::intrusive_ptr<Expression> date, boost::intrusive_ptr<Expression> timeZone, boost::intrusive_ptr<Expression> onNull); - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -1711,8 +1626,8 @@ public: boost::intrusive_ptr<Expression> unit, boost::intrusive_ptr<Expression> timezone, boost::intrusive_ptr<Expression> startOfWeek); - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, BSONElement expr, @@ -1747,8 +1662,6 @@ private: void _doAddDependencies(DepsTracker* deps) const final; - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; - // Starting time instant expression. Accepted types: Date_t, Timestamp, OID. boost::intrusive_ptr<Expression>& _startDate; @@ -1844,7 +1757,7 @@ public: boost::intrusive_ptr<Expression> optimize() final; Value evaluate(const Document& root, Variables* variables) const; - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; /* Create a field path expression using old semantics (rooted off of CURRENT). @@ -1919,8 +1832,6 @@ protected: private: - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; - /* Internal implementation of evaluate(), used recursively. @@ -1945,8 +1856,8 @@ private: class ExpressionFilter final : public Expression { public: - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -2012,11 +1923,6 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } - -private: - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { - return getChildren()[0]->getMonotonicState(sortedFieldPath); - } }; @@ -2182,8 +2088,8 @@ public: class ExpressionLet final : public Expression { public: - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -2299,7 +2205,7 @@ public: ConstDataRange serverToken, int64_t contentionFactor, ConstDataRange edcToken); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; const char* getOpName() const; @@ -2333,8 +2239,8 @@ public: boost::intrusive_ptr<Expression> input, // yields array to iterate boost::intrusive_ptr<Expression> each); // yields results to be added to output array - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -2366,7 +2272,7 @@ class ExpressionMeta final : public Expression { public: ExpressionMeta(ExpressionContext* expCtx, DocumentMetadataFields::MetaType metaType); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, @@ -2560,7 +2466,7 @@ class ExpressionObject final : public Expression { public: boost::intrusive_ptr<Expression> optimize() final; Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; static boost::intrusive_ptr<ExpressionObject> create( ExpressionContext* expCtx, @@ -2592,10 +2498,8 @@ public: return visitor->visit(this); } - protected: void _doAddDependencies(DepsTracker* deps) const final; - bool selfAndChildrenAreConstant() const override final; private: ExpressionObject( @@ -2705,7 +2609,7 @@ public: static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, BSONElement expr, const VariablesParseState& vps); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -2741,8 +2645,8 @@ public: virtual const char* getOpName() const = 0; Value evaluate(const Document& root, Variables* variables) const final; - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; protected: void _doAddDependencies(DepsTracker* deps) const final; @@ -2956,10 +2860,7 @@ public: } bool isCommutative() const final { - // Only commutative when performing binary string comparison. The first value entered when - // multiple collation-equal but binary-unequal values are added will dictate what is stored - // in the set. - return getExpressionContext()->getCollator() == nullptr; + return true; } void acceptVisitor(ExpressionMutableVisitor* visitor) final { @@ -3022,7 +2923,7 @@ public: static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, BSONElement expr, const VariablesParseState& vps); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -3375,9 +3276,6 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } - -private: - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; }; @@ -3387,17 +3285,18 @@ public: std::pair<boost::intrusive_ptr<Expression>&, boost::intrusive_ptr<Expression>&>; ExpressionSwitch(ExpressionContext* const expCtx, - std::vector<boost::intrusive_ptr<Expression>> children) - : Expression(expCtx, std::move(children)) { - uassert(40068, "$switch requires at least one branch", numBranches() >= 1); - } + std::vector<boost::intrusive_ptr<Expression>> children, + std::vector<ExpressionPair> branches) + : Expression(expCtx, std::move(children)), + _default(_children.back()), + _branches(std::move(branches)) {} Value evaluate(const Document& root, Variables* variables) const final; boost::intrusive_ptr<Expression> optimize() final; static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, BSONElement expr, const VariablesParseState& vpsIn); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -3407,38 +3306,12 @@ public: return visitor->visit(this); } - /** - * Returns the number of cases in the switch expression. Each branch is made up of two - * expressions ('case' and 'then'). - */ - int numBranches() const { - return _children.size() / 2; - } - - /** - * Returns a pair of expression pointers representing the 'case' and 'then' expressions for the - * i-th branch of the switch. - */ - std::pair<const Expression*, const Expression*> getBranch(int i) const { - invariant(i >= 0); - invariant(i < numBranches()); - return {_children[i * 2].get(), _children[i * 2 + 1].get()}; - } - - /** - * Returns the 'default' expression, or nullptr if there is no 'default'. - */ - const Expression* defaultExpr() const { - return _children.back().get(); - } - protected: void _doAddDependencies(DepsTracker* deps) const final; private: - // Helper for 'optimize()'. Deletes the 'case' and 'then' children associated with the i-th - // branch of the switch. - void deleteBranch(int i); + boost::intrusive_ptr<Expression>& _default; + std::vector<ExpressionPair> _branches; }; @@ -3512,7 +3385,7 @@ public: static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, BSONElement expr, const VariablesParseState& vpsIn); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -3763,7 +3636,7 @@ public: static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, BSONElement expr, const VariablesParseState& vpsIn); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -3802,8 +3675,8 @@ public: const VariablesParseState& vpsIn); Value evaluate(const Document& root, Variables* variables) const final; - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -3908,7 +3781,7 @@ public: boost::optional<std::pair<boost::optional<std::string>, std::string>> getConstantPatternAndOptions() const; - Value serialize(const SerializationOptions& options = {}) const; + Value serialize(bool explain) const; const std::string& getOpName() const { return _opName; @@ -4025,7 +3898,7 @@ public: BSONElement exprElement, const VariablesParseState& vps); - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; @@ -4071,7 +3944,7 @@ public: } Value evaluate(const Document& root, Variables* variables) const; - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; protected: void _doAddDependencies(DepsTracker* deps) const final; @@ -4094,8 +3967,8 @@ public: _timeZone(_children[3]), _opName(opName) {} - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; protected: @@ -4109,10 +3982,6 @@ protected: long long amount, const TimeZone& timezone) const = 0; - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; - virtual monotonic::State combineMonotonicStateOfArguments( - monotonic::State startDataMonotonicState, monotonic::State amountMonotonicState) const = 0; - private: // The expression representing the startDate argument. boost::intrusive_ptr<Expression>& _startDate; @@ -4147,14 +4016,10 @@ public: } private: - monotonic::State combineMonotonicStateOfArguments( - monotonic::State startDataMonotonicState, - monotonic::State amountMonotonicState) const final; - - Value evaluateDateArithmetics(Date_t date, - TimeUnit unit, - long long amount, - const TimeZone& timezone) const final; + virtual Value evaluateDateArithmetics(Date_t date, + TimeUnit unit, + long long amount, + const TimeZone& timezone) const override; }; class ExpressionDateSubtract final : public ExpressionDateArithmetics { @@ -4174,14 +4039,10 @@ public: } private: - monotonic::State combineMonotonicStateOfArguments( - monotonic::State startDataMonotonicState, - monotonic::State amountMonotonicState) const final; - - Value evaluateDateArithmetics(Date_t date, - TimeUnit unit, - long long amount, - const TimeZone& timezone) const final; + virtual Value evaluateDateArithmetics(Date_t date, + TimeUnit unit, + long long amount, + const TimeZone& timezone) const override; }; struct SubstituteFieldPathWalker { @@ -4222,8 +4083,8 @@ public: boost::intrusive_ptr<Expression> binSize, boost::intrusive_ptr<Expression> timezone, boost::intrusive_ptr<Expression> startOfWeek); - boost::intrusive_ptr<Expression> optimize() override; - Value serialize(const SerializationOptions& options = {}) const override; + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); @@ -4267,8 +4128,6 @@ private: void _doAddDependencies(DepsTracker* deps) const final; - monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; - // Expression that evaluates to a date to truncate. Accepted BSON types: Date, bsonTimestamp, // jstOID. boost::intrusive_ptr<Expression>& _date; @@ -4313,7 +4172,7 @@ public: expCtx->sbeCompatible = false; } - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; @@ -4358,7 +4217,7 @@ public: expCtx->sbeCompatible = false; } - Value serialize(const SerializationOptions& options = {}) const final; + Value serialize(bool explain) const final; Value evaluate(const Document& root, Variables* variables) const final; @@ -4433,83 +4292,4 @@ public: } }; -/** - * The expression '$_internalKeyStringValue' is used to generate the key string binary of any - * document value ('input' field) under an optionally different non-default collation ('collation' - * field). The generated key string binary purposefully doesn't contain the type bits information, - * so that the generated binary has the same ordering as the index. - * - * The expression specification is a follows: - * { - * $_internalKeyStringValue: { - * input: <expression>, - * collation: <collation spec> - * } - * } - * - * Examples: - * Case 1: The 'input' field is an integer. - * Input1: - * { - * $_internalKeyStringValue: { - * input: 1 - * } - * } - * Output1: BinData(0, "KwIE") - * - * Case 2: The 'input' field is an integer of the same numeric value as above but different type. - * Input2: - * { - * $_internalKeyStringValue: { - * input: 1.0 - * } - * } - * Output2: BinData(0, "KwIE") - * - * Case 3: The 'input' field is a string. The 'collation' field is a non-default collation spec. - * Input3: - * { - * $_internalIndexKey: { - * input: "aAa", - * collation: {locale: "en", strength: 1} - * } - * } - * Output3: BinData(0, "PCkpKQAE") - */ -class ExpressionInternalKeyStringValue final : public Expression { -public: - ExpressionInternalKeyStringValue(ExpressionContext* expCtx, - boost::intrusive_ptr<Expression> input, - boost::intrusive_ptr<Expression> collation) - : Expression(expCtx, {input, collation}) { - expCtx->sbeCompatible = false; - } - - static boost::intrusive_ptr<Expression> parse(ExpressionContext* expCtx, - BSONElement expr, - const VariablesParseState& vps); - - Value serialize(const SerializationOptions& options = {}) const final; - - Value evaluate(const Document& root, Variables* variables) const final; - - const char* getOpName() const { - return "$_internalKeyStringValue"; - } - - void acceptVisitor(ExpressionMutableVisitor* visitor) final { - return visitor->visit(this); - } - - void acceptVisitor(ExpressionConstVisitor* visitor) const final { - return visitor->visit(this); - } - -private: - void _doAddDependencies(DepsTracker* deps) const final; - - static constexpr size_t _kInput = 0; - static constexpr size_t _kCollation = 1; -}; - } // namespace mongo diff --git a/src/mongo/db/pipeline/expression_and_test.cpp b/src/mongo/db/pipeline/expression_and_test.cpp index 9671c08b2db..cbfa37a77d1 100644 --- a/src/mongo/db/pipeline/expression_and_test.cpp +++ b/src/mongo/db/pipeline/expression_and_test.cpp @@ -76,8 +76,8 @@ static BSONObj toBson(const Value& value) { } /** Convert Expression to BSON. */ -static BSONObj expressionToBson(const boost::intrusive_ptr<Expression>& expression) { - return BSON("" << expression->serialize()).firstElement().embeddedObject().getOwned(); +static BSONObj expressionToBson(const intrusive_ptr<Expression>& expression) { + return BSON("" << expression->serialize(false)).firstElement().embeddedObject().getOwned(); } /** Convert Document to BSON. */ diff --git a/src/mongo/db/pipeline/expression_compare_test.cpp b/src/mongo/db/pipeline/expression_compare_test.cpp index 941eceeaa59..11836d77267 100644 --- a/src/mongo/db/pipeline/expression_compare_test.cpp +++ b/src/mongo/db/pipeline/expression_compare_test.cpp @@ -75,7 +75,7 @@ static BSONObj toBson(const Value& value) { /** Convert Expression to BSON. */ static BSONObj expressionToBson(const intrusive_ptr<Expression>& expression) { - return BSON("" << expression->serialize()).firstElement().embeddedObject().getOwned(); + return BSON("" << expression->serialize(false)).firstElement().embeddedObject().getOwned(); } /** Convert Document to BSON. */ diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp index 626db2f6016..2c41c612595 100644 --- a/src/mongo/db/pipeline/expression_context.cpp +++ b/src/mongo/db/pipeline/expression_context.cpp @@ -49,49 +49,6 @@ ExpressionContext::ResolvedNamespace::ResolvedNamespace(NamespaceString ns, : ns(std::move(ns)), pipeline(std::move(pipeline)), uuid(collUUID) {} ExpressionContext::ExpressionContext(OperationContext* opCtx, - const FindCommandRequest& findCmd, - std::unique_ptr<CollatorInterface> collator, - bool mayDbProfile, - boost::optional<ExplainOptions::Verbosity> verbosity, - bool allowDiskUseDefault) - // Although both 'find' and 'aggregate' commands have an ExpressionContext, some of the data - // members in the ExpressionContext are used exclusively by the aggregation subsystem. This - // includes the following fields which here we simply initialize to some meaningless default - // value: - // - explain - // - fromMongos - // - needsMerge - // - bypassDocumentValidation - // - mongoProcessInterface - // - resolvedNamespaces - // - uuid - // - // As we change the code to make the find and agg systems more tightly coupled, it would make - // sense to start initializing these fields for find operations as well. - : ExpressionContext(opCtx, - verbosity, - false, // fromMongos - false, // needsMerge - findCmd.getAllowDiskUse().value_or(allowDiskUseDefault), - false, // bypassDocumentValidation - false, // isMapReduceCommand - findCmd.getNamespaceOrUUID().nss().has_value() - ? *findCmd.getNamespaceOrUUID().nss() - : NamespaceString{}, - findCmd.getLegacyRuntimeConstants(), - std::move(collator), - nullptr, // mongoProcessInterface - {}, // resolvedNamespaces - [&findCmd]() -> boost::optional<UUID> { - if (findCmd.getNamespaceOrUUID().uuid().has_value()) { - return findCmd.getNamespaceOrUUID().uuid(); - } - return boost::none; - }(), - findCmd.getLet(), - mayDbProfile) {} - -ExpressionContext::ExpressionContext(OperationContext* opCtx, const AggregateCommandRequest& request, std::unique_ptr<CollatorInterface> collator, std::shared_ptr<MongoProcessInterface> processInterface, @@ -202,44 +159,6 @@ ExpressionContext::ExpressionContext( variables.seedVariablesWithLetParameters(this, *letParameters); } -ExpressionContext::ExpressionContext(OperationContext* opCtx, - const NamespaceString& nss, - const boost::optional<BSONObj>& letParameters) - : explain(boost::none), - allowDiskUse(false), - ns(nss), - opCtx(opCtx), - jsHeapLimitMB(internalQueryJavaScriptHeapSizeLimitMB.load()), - mongoProcessInterface(std::make_shared<StubMongoProcessInterface>()), - timeZoneDatabase(opCtx && opCtx->getServiceContext() - ? TimeZoneDatabase::get(opCtx->getServiceContext()) - : nullptr), - variablesParseState(variables.useIdGenerator()), - maxFeatureCompatibilityVersion(boost::none), // Ensure all features are allowed. - mayDbProfile(true), - _collator(nullptr), - _documentComparator(_collator.get()), - _valueComparator(_collator.get()) { - // This is a shortcut to avoid reading the clock and the vector clock, since we don't actually - // care about their values for this 'blank' ExpressionContext codepath. - variables.setLegacyRuntimeConstants({Date_t::min(), Timestamp()}); - // Expression counters are reported in serverStatus to indicate how often clients use certain - // expressions/stages, so it's a side effect tied to parsing. We must stop expression counters - // before re-parsing to avoid adding to the counters more than once per a given query. - stopExpressionCounters(); - if (letParameters) - variables.seedVariablesWithLetParameters(this, *letParameters); -} - -boost::intrusive_ptr<ExpressionContext> ExpressionContext::makeBlankExpressionContext( - OperationContext* opCtx, - const NamespaceStringOrUUID& nssOrUUID, - boost::optional<BSONObj> shapifiedLet) { - const auto nss = nssOrUUID.nss().has_value() ? *nssOrUUID.nss() : NamespaceString{}; - // This constructor is private, so we can't use `boost::make_instrusive()`. - return new ExpressionContext(opCtx, nss, shapifiedLet); -} - void ExpressionContext::checkForInterruptSlow() { // This check could be expensive, at least in relative terms, so don't check every time. invariant(opCtx); @@ -332,27 +251,11 @@ void ExpressionContext::incrementAggExprCounter(StringData name) { } } -void ExpressionContext::incrementGroupAccumulatorExprCounter(StringData name) { - if (enabledCounters && _expressionCounters) { - ++_expressionCounters.get().groupAccumulatorExprCountersMap[name]; - } -} - -void ExpressionContext::incrementWindowAccumulatorExprCounter(StringData name) { - if (enabledCounters && _expressionCounters) { - ++_expressionCounters.get().windowAccumulatorExprCountersMap[name]; - } -} - void ExpressionContext::stopExpressionCounters() { if (enabledCounters && _expressionCounters) { operatorCountersMatchExpressions.mergeCounters( _expressionCounters.get().matchExprCountersMap); operatorCountersAggExpressions.mergeCounters(_expressionCounters.get().aggExprCountersMap); - operatorCountersGroupAccumulatorExpressions.mergeCounters( - _expressionCounters.get().groupAccumulatorExprCountersMap); - operatorCountersWindowAccumulatorExpressions.mergeCounters( - _expressionCounters.get().windowAccumulatorExprCountersMap); } _expressionCounters = boost::none; } diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h index aa2d212f53d..3f83fba6c8e 100644 --- a/src/mongo/db/pipeline/expression_context.h +++ b/src/mongo/db/pipeline/expression_context.h @@ -60,6 +60,15 @@ namespace mongo { class AggregateCommandRequest; +/** + * The structure ExpressionCounters encapsulates counters for match, aggregate, and other + * expression types as seen in the end-user queries. + */ +struct ExpressionCounters { + StringMap<uint64_t> aggExprCountersMap; + StringMap<uint64_t> matchExprCountersMap; +}; + class ExpressionContext : public RefCountable { public: struct ResolvedNamespace { @@ -109,21 +118,9 @@ public: struct ExpressionCounters { StringMap<uint64_t> aggExprCountersMap; StringMap<uint64_t> matchExprCountersMap; - StringMap<uint64_t> groupAccumulatorExprCountersMap; - StringMap<uint64_t> windowAccumulatorExprCountersMap; }; /** - * Constructs an ExpressionContext to be used for find command parsing and evaluation. - */ - ExpressionContext(OperationContext* opCtx, - const FindCommandRequest& findCmd, - std::unique_ptr<CollatorInterface> collator, - bool mayDbProfile, - boost::optional<ExplainOptions::Verbosity> verbosity = boost::none, - bool allowDiskUseByDefault = false); - - /** * Constructs an ExpressionContext to be used for Pipeline parsing and evaluation. * 'resolvedNamespaces' maps collection names (not full namespaces) to ResolvedNamespaces. */ @@ -171,22 +168,6 @@ public: boost::optional<ExplainOptions::Verbosity> explain = boost::none); /** - * Constructs a blank ExpressionContext suitable for creating Query Shapes, but it could be - * applied to other use cases as well. - * - * The process for creating a Query Shape sometimes requires re-parsing the BSON into a proper - * AST, and for that you need an ExpressionContext. - * - * Note: this is meant for introspection and is not suitable for using to execute queries - - * since it does not contain for example a collation argument or a real MongoProcessInterface - * for execution. - */ - static boost::intrusive_ptr<ExpressionContext> makeBlankExpressionContext( - OperationContext* opCtx, - const NamespaceStringOrUUID& nssOrUUID, - boost::optional<BSONObj> shapifiedLet = boost::none); - - /** * Used by a pipeline to check for interrupts so that killOp() works. Throws a UserAssertion if * this aggregation pipeline has been interrupted. */ @@ -304,8 +285,7 @@ public: */ const ResolvedNamespace& getResolvedNamespace(const NamespaceString& nss) const { auto it = _resolvedNamespaces.find(nss.coll()); - invariant(it != _resolvedNamespaces.end(), - str::stream() << "No resolved namespace provided for " << nss.toString()); + invariant(it != _resolvedNamespaces.end()); return it->second; }; @@ -350,8 +330,10 @@ public: * created yet. Initializes the Scope with the 'jsScope' variables from the runtimeConstants. * Loads the Scope with the functions stored in system.js if the expression isn't executed on * mongos and is called from a MapReduce command or `forceLoadOfStoredProcedures` is true. + * + * Returns a JsExec and a boolean indicating whether the Scope was created as part of this call. */ - JsExecution* getJsExecWithScope(bool forceLoadOfStoredProcedures = false) const { + auto getJsExecWithScope(bool forceLoadOfStoredProcedures = false) const { uassert(31264, "Cannot run server-side javascript without the javascript engine enabled", getGlobalScriptEngine()); @@ -373,17 +355,9 @@ public: "$where."); } - // If there is a cached JsExecution object, return it. This is a performance optimization - // that avoids potentially copying the scope object, which is not needed if a cached exec - // object already exists. - JsExecution* jsExec = JsExecution::getCached(opCtx, loadStoredProcedures); - if (jsExec) { - return jsExec; - } - - BSONObj scopeObj = BSONObj(); + auto scopeObj = BSONObj(); if (variables.hasValue(Variables::kJsScopeId)) { - Value scopeVar = variables.getValue(Variables::kJsScopeId); + auto scopeVar = variables.getValue(Variables::kJsScopeId); invariant(scopeVar.isObject()); scopeObj = scopeVar.getDocument().toBson(); } @@ -406,16 +380,6 @@ public: void incrementAggExprCounter(StringData name); /** - * Increment the counter for the $group accumulator expression with a given name. - */ - void incrementGroupAccumulatorExprCounter(StringData name); - - /** - * Increment the counter for the $setWindowFields accumulator expression with a given name. - */ - void incrementWindowAccumulatorExprCounter(StringData name); - - /** * Merge expression counters from the current expression context into the global maps * and stop counting. */ @@ -469,7 +433,7 @@ public: // Tracks the depth of nested aggregation sub-pipelines. Used to enforce depth limits. long long subPipelineDepth = 0; - // True if this 'ExpressionContext' object is for the inner side of a $lookup or $graphLookup. + // True if this 'ExpressionContext' object is for the inner side of a $lookup. bool inLookup = false; // If set, this will disallow use of features introduced in versions above the provided version. @@ -522,48 +486,6 @@ public: // expression counting. bool enabledCounters = true; - // Sets or clears a flag which tells DocumentSource parsers whether any involved Collection - // may contain extended-range dates. - void setRequiresTimeseriesExtendedRangeSupport(bool v) { - _requiresTimeseriesExtendedRangeSupport = v; - } - bool getRequiresTimeseriesExtendedRangeSupport() const { - return _requiresTimeseriesExtendedRangeSupport; - } - - // This is state that is to be shared between the DocumentInternalSearchMongotRemote and - // DocumentInternalSearchIdLookup stages (these stages are the result of desugaring $search) - // during runtime. - class SharedSearchState { - public: - SharedSearchState() {} - - long long getDocsReturnedByIdLookup() const { - return _docsReturnedByIdLookup; - } - - /** - * Sets the value of _docsReturnedByIdLookup to 0. - */ - void resetDocsReturnedByIdLookup() { - _docsReturnedByIdLookup = 0; - } - - /** - * Increments the value of _docsReturnedByIdLookup by 1. - */ - void incrementDocsReturnedByIdLookup() { - _docsReturnedByIdLookup++; - } - - private: - // When there is an extractable limit in the query, DocumentInternalSearchMongotRemote sends - // a getMore to mongot that specifies how many more documents it needs to fulfill that - // limit, and it incorporates the amount of documents returned by the - // DocumentInternalSearchIdLookup stage into that value. - long long _docsReturnedByIdLookup = 0; - } sharedSearchState; - protected: static const int kInterruptCheckPeriod = 128; @@ -588,17 +510,7 @@ protected: bool _isCappedDelete = false; - bool _requiresTimeseriesExtendedRangeSupport = false; - private: - // Instantiates an ExpressionContext which does not increment expression counters and does not - // enforce FCV restrictions. It is used for implementing the `makeBlankExpressionContext()` - // factory method. Please also note that the runtime constants are not given real/accurate - // values of '$$NOW' and '$$CLUSTER_TIME', in the name of efficiency. - ExpressionContext(OperationContext* opCtx, - const NamespaceString& ns, - const boost::optional<BSONObj>& letParameters = boost::none); - boost::optional<ExpressionCounters> _expressionCounters = boost::none; }; diff --git a/src/mongo/db/pipeline/expression_context_test.cpp b/src/mongo/db/pipeline/expression_context_test.cpp index 2ff0ef20b1e..b105dce531b 100644 --- a/src/mongo/db/pipeline/expression_context_test.cpp +++ b/src/mongo/db/pipeline/expression_context_test.cpp @@ -39,6 +39,15 @@ #include "mongo/db/vector_clock_mutable.h" #include "mongo/unittest/unittest.h" +#define ASSERT_DOES_NOT_THROW(EXPRESSION) \ + try { \ + EXPRESSION; \ + } catch (const AssertionException& e) { \ + str::stream err; \ + err << "Threw an exception incorrectly: " << e.toString(); \ + ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, err).stream(); \ + } + namespace mongo { namespace { diff --git a/src/mongo/db/pipeline/expression_convert_test.cpp b/src/mongo/db/pipeline/expression_convert_test.cpp index c1cfd639ce1..678c29b5b16 100644 --- a/src/mongo/db/pipeline/expression_convert_test.cpp +++ b/src/mongo/db/pipeline/expression_convert_test.cpp @@ -67,11 +67,10 @@ TEST_F(ExpressionConvertTest, ParseAndSerializeWithoutOptionalArguments) { auto convertExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$path1', to: {$const: 'int'}}}")), - convertExp->serialize()); + convertExp->serialize(false)); ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$path1', to: {$const: 'int'}}}")), - convertExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})); + convertExp->serialize(true)); } TEST_F(ExpressionConvertTest, ParseAndSerializeWithOnError) { @@ -86,12 +85,11 @@ TEST_F(ExpressionConvertTest, ParseAndSerializeWithOnError) { ASSERT_VALUE_EQ( Value(fromjson("{$convert: {input: '$path1', to: {$const: 'int'}, onError: {$const: 0}}}")), - convertExp->serialize()); + convertExp->serialize(false)); ASSERT_VALUE_EQ( Value(fromjson("{$convert: {input: '$path1', to: {$const: 'int'}, onError: {$const: 0}}}")), - convertExp->serialize( - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})); + convertExp->serialize(true)); } TEST_F(ExpressionConvertTest, ParseAndSerializeWithOnNull) { @@ -106,12 +104,11 @@ TEST_F(ExpressionConvertTest, ParseAndSerializeWithOnNull) { ASSERT_VALUE_EQ( Value(fromjson("{$convert: {input: '$path1', to: {$const: 'int'}, onNull: {$const: 0}}}")), - convertExp->serialize()); + convertExp->serialize(false)); ASSERT_VALUE_EQ( Value(fromjson("{$convert: {input: '$path1', to: {$const: 'int'}, onNull: {$const: 0}}}")), - convertExp->serialize( - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})); + convertExp->serialize(true)); } TEST_F(ExpressionConvertTest, ConvertWithoutInputFailsToParse) { @@ -144,32 +141,6 @@ TEST_F(ExpressionConvertTest, ConvertWithoutToFailsToParse) { }); } -TEST_F(ExpressionConvertTest, RoundTripSerialization) { - auto expCtx = getExpCtx(); - - // Round-trip serialization of an argument that *looks* like an expression. - auto spec = BSON("$convert" << BSON("input" << BSON("$literal" << BSON("$toString" - << "this is a string")) - << "to" - << "string")); - auto convertExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - - auto opts = SerializationOptions{LiteralSerializationPolicy::kToRepresentativeParseableValue}; - auto serialized = convertExp->serialize(opts); - ASSERT_VALUE_EQ(Value(BSON("$convert" << BSON("input" << BSON("$const" << BSON("?" - << "?")) - << "to" - << BSON("$const" - << "string")))), - serialized); - - auto roundTrip = Expression::parseExpression(expCtx.get(), - serialized.getDocument().toBson(), - expCtx->variablesParseState) - ->serialize(opts); - ASSERT_VALUE_EQ(roundTrip, serialized); -} - TEST_F(ExpressionConvertTest, InvalidTypeNameFails) { auto expCtx = getExpCtx(); @@ -3474,82 +3445,4 @@ TEST_F(ExpressionConvertShortcutsTest, ThrowsOnConversionFailure) { } } // namespace ExpressionConvertShortcutsTest - -namespace ExpressionConvertSerializationTest { - -using ExpressionConvertSerializationTest = AggregationContextFixture; - -TEST_F(ExpressionConvertSerializationTest, ConvertToTypeSerializesCorrectly) { - auto expCtx = getExpCtx(); - auto spec = fromjson("{$convert: {input: '$funnyTest', to: 'bool'}}"); - auto convertExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - - ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$funnyTest', to: {$const: 'bool'}}}")), - convertExp->serialize(SerializationOptions{})); - - ASSERT_VALUE_EQ( - Value(fromjson("{$convert: {input: '$funnyTest', to: {$const: 'bool'}}}")), - convertExp->serialize(SerializationOptions::kRepresentativeQueryShapeSerializeOptions)); - - ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$funnyTest', to: 'bool'}}")), - convertExp->serialize(SerializationOptions::kDebugQueryShapeSerializeOptions)); -} - -TEST_F(ExpressionConvertSerializationTest, ConvertToDollarSerializesCorrectly) { - auto expCtx = getExpCtx(); - auto spec = fromjson("{$convert: {input: '$funnyTest', to: {$add: [7, 2]}}}"); - auto convertExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - - ASSERT_VALUE_EQ( - Value( - fromjson("{$convert: {input: '$funnyTest', to: {$add: [{$const: 7}, {$const: 2}]}}}")), - convertExp->serialize(SerializationOptions{})); - - ASSERT_VALUE_EQ( - Value(fromjson("{$convert: {input: '$funnyTest', to: {$add: [1, 1]}}}")), - convertExp->serialize(SerializationOptions::kRepresentativeQueryShapeSerializeOptions)); - - ASSERT_VALUE_EQ( - Value(fromjson("{$convert: {input: '$funnyTest', to: {$add: '?array<?number>'}}}")), - convertExp->serialize(SerializationOptions::kDebugQueryShapeSerializeOptions)); -} - -TEST_F(ExpressionConvertSerializationTest, ToStringSerializesCorrectly) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$toString" - << "$funnyTest"); - auto convertExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - - ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$funnyTest', to: {$const: 'string'}}}")), - convertExp->serialize(SerializationOptions{})); - - ASSERT_VALUE_EQ( - Value(fromjson("{$convert: {input: '$funnyTest', to: {$const: 'string'}}}")), - convertExp->serialize(SerializationOptions::kRepresentativeQueryShapeSerializeOptions)); - - ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$funnyTest', to: 'string'}}")), - convertExp->serialize(SerializationOptions::kDebugQueryShapeSerializeOptions)); -} - -TEST_F(ExpressionConvertSerializationTest, ToBoolSerializesCorrectly) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$toBool" - << "$funnyTest"); - auto convertExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - - ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$funnyTest', to: {$const: 'bool'}}}")), - convertExp->serialize(SerializationOptions{})); - - ASSERT_VALUE_EQ( - Value(fromjson("{$convert: {input: '$funnyTest', to: {$const: 'bool'}}}")), - convertExp->serialize(SerializationOptions::kRepresentativeQueryShapeSerializeOptions)); - - ASSERT_VALUE_EQ(Value(fromjson("{$convert: {input: '$funnyTest', to: 'bool'}}")), - convertExp->serialize(SerializationOptions::kDebugQueryShapeSerializeOptions)); -} - -} // namespace ExpressionConvertSerializationTest - } // namespace mongo diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp index c4b7a01db42..efe0a577ef6 100644 --- a/src/mongo/db/pipeline/expression_date_test.cpp +++ b/src/mongo/db/pipeline/expression_date_test.cpp @@ -64,10 +64,8 @@ TEST_F(ExpressionDateFromPartsTest, SerializesToObjectSyntax) { {"second", Document{{"$const", 15}}}, {"millisecond", Document{{"$const", 414}}}, {"timezone", Document{{"$const", "America/Los_Angeles"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); } TEST_F(ExpressionDateFromPartsTest, OptimizesToConstantIfAllInputsAreConstant) { @@ -185,10 +183,8 @@ TEST_F(ExpressionDateToPartsTest, SerializesToObjectSyntax) { Document{{"date", Document{{"$const", Date_t{}}}}, {"timezone", Document{{"$const", "Europe/London"_sd}}}, {"iso8601", Document{{"$const", false}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); } TEST_F(ExpressionDateToPartsTest, OptimizesToConstantIfAllInputsAreConstant) { @@ -434,28 +430,22 @@ TEST_F(DateExpressionTest, SerializesToObjectSyntax) { Value(Document{{expName, Document{{"date", Document{{"$const", Date_t{}}}}, {"timezone", Document{{"$const", "Europe/London"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); // Test that it serializes to the full format if given a date. spec = BSON(expName << Date_t{}); expectedSerialization = Value(Document{{expName, Document{{"date", Document{{"$const", Date_t{}}}}}}}); dateExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); // Test that it serializes to the full format if given a date within an array. spec = BSON(expName << BSON_ARRAY(Date_t{})); dateExp = Expression::parseExpression(expCtx.get(), spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); } } @@ -608,10 +598,8 @@ TEST_F(ExpressionDateToStringTest, SerializesToObjectSyntax) { {"timezone", Document{{"$const", "Europe/London"_sd}}}, {"onNull", Document{{"$const", "nullDefault"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); } TEST_F(ExpressionDateToStringTest, OptimizesToConstantIfAllInputsAreConstant) { @@ -836,10 +824,8 @@ TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { Document{{"$dateFromString", Document{{"dateString", Document{{"$const", "2017-07-04T13:06:44Z"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); // Test that it serializes to the full format if given an object specification. spec = BSON("$dateFromString" << BSON("dateString" @@ -852,10 +838,8 @@ TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { Document{{"dateString", Document{{"$const", "2017-07-04T13:06:44Z"_sd}}}, {"timezone", Document{{"$const", "Europe/London"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); spec = BSON("$dateFromString" << BSON("dateString" << "2017-07-04T13:06:44Z" @@ -870,10 +854,8 @@ TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { {"timezone", Document{{"$const", "Europe/London"_sd}}}, {"format", Document{{"$const", "%Y-%d-%mT%H:%M:%S"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); spec = BSON("$dateFromString" << BSON("dateString" << "2017-07-04T13:06:44Z" @@ -894,10 +876,8 @@ TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { {"onNull", Document{{"$const", "nullDefault"_sd}}}, {"onError", Document{{"$const", "errorDefault"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); } TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) { @@ -1436,15 +1416,13 @@ void assertParsesAndSerializesExpression(boost::intrusive_ptr<ExpressionContextF const auto expression = Expression::parseExpression(expCtx.get(), expressionSpec, expCtx->variablesParseState); const auto expectedSerialization = Value(expectedSerializedExpressionSpec); - ASSERT_VALUE_EQ(expression->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(expression->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(expression->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(expression->serialize(false), expectedSerialization); // Verify that parsed and then serialized expression is the same. ASSERT_VALUE_EQ(Expression::parseExpression( expCtx.get(), expectedSerializedExpressionSpec, expCtx->variablesParseState) - ->serialize(), + ->serialize(false), expectedSerialization); } @@ -1793,7 +1771,7 @@ TEST_F(ExpressionDateDiffTest, AddsDependencies) { auto depsTracker = dateDiffExpression->getDependencies(); ASSERT_TRUE( (depsTracker.fields == - OrderedPathSet{ + std::set<std::string>{ "startDateField", "endDateField", "unitField", "timezoneField", "startOfWeekField"})); } } // namespace ExpressionDateDiffTest @@ -1891,7 +1869,7 @@ TEST_F(ExpressionDateTruncTest, AddsDependencies) { const auto depsTracker = dateTruncExpression->getDependencies(); ASSERT_TRUE( (depsTracker.fields == - OrderedPathSet{ + std::set<std::string>{ "dateField", "unitField", "binSizeField", "timezoneField", "startOfWeekField"})); } } // namespace @@ -1915,10 +1893,8 @@ TEST_F(ExpressionDateArithmeticsTest, SerializesToObject) { Document{{"startDate", Document{{"$const", Date_t{}}}}, {"unit", Document{{"$const", "day"_sd}}}, {"amount", Document{{"$const", 1}}}}}}); - ASSERT_VALUE_EQ(dateAddExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateAddExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateAddExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateAddExp->serialize(false), expectedSerialization); // with timezone doc = BSON(expName << BSON("startDate" << Date_t{} << "unit" @@ -1932,10 +1908,8 @@ TEST_F(ExpressionDateArithmeticsTest, SerializesToObject) { {"unit", Document{{"$const", "day"_sd}}}, {"amount", Document{{"$const", -1}}}, {"timezone", Document{{"$const", "America/New_York"_sd}}}}}}); - ASSERT_VALUE_EQ(dateAddExp->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}), - expectedSerialization); - ASSERT_VALUE_EQ(dateAddExp->serialize(), expectedSerialization); + ASSERT_VALUE_EQ(dateAddExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateAddExp->serialize(false), expectedSerialization); } } diff --git a/src/mongo/db/pipeline/expression_field_path_test.cpp b/src/mongo/db/pipeline/expression_field_path_test.cpp index b04484c47ab..9fa73620dfe 100644 --- a/src/mongo/db/pipeline/expression_field_path_test.cpp +++ b/src/mongo/db/pipeline/expression_field_path_test.cpp @@ -200,72 +200,6 @@ TEST(FieldPath, ScalarVariableWithDottedFieldPathOptimizesToConstantMissingValue ASSERT_VALUE_EQ(Value(), constantExpr->getValue()); } -TEST(FieldPath, SerializeWithRedaction) { - SerializationOptions options = SerializationOptions::kMarkIdentifiers_FOR_TEST; - - auto expCtx = ExpressionContextForTest{}; - intrusive_ptr<Expression> expression = - ExpressionFieldPath::createPathFromString(&expCtx, "bar", expCtx.variablesParseState); - ASSERT_VALUE_EQ_AUTO( // NOLINT - "\"$HASH<bar>\"", - expression->serialize(options)); - - // Repeat with a dotted path. - expression = - ExpressionFieldPath::createPathFromString(&expCtx, "a.b.c", expCtx.variablesParseState); - ASSERT_VALUE_EQ_AUTO( // NOLINT - "\"$HASH<a>.HASH<b>.HASH<c>\"", - expression->serialize(options)); - - auto expr = [&](const std::string& json) { - return Expression::parseExpression(&expCtx, fromjson(json), expCtx.variablesParseState); - }; - - // Expression with multiple field paths. - expression = expr(R"({$and: [{$gt: ["$foo", 5]}, {$lt: ["$foo", 10]}]})"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$and": [ - { - "$gt": [ - "$HASH<foo>", - { - "$const": 5 - } - ] - }, - { - "$lt": [ - "$HASH<foo>", - { - "$const": 10 - } - ] - } - ] - })", - expression->serialize(options).getDocument()); - - // Test that a variable followed by user fields is properly hashed. - options.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString; - - expression = expr(R"({$gt: ["$$ROOT.a.b", 5]})"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$gt":["$$ROOT.HASH<a>.HASH<b>","?number"]})", - expression->serialize(options).getDocument()); - - expression = expr(R"({$gt: ["$foo", "$$NOW"]})"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$gt":["$HASH<foo>","$$NOW"]})", - expression->serialize(options).getDocument()); - - // Repeat the above test with a dotted path. - expression = expr(R"({$gt: ["$foo.a.b", "$$NOW"]})"); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$gt":["$HASH<foo>.HASH<a>.HASH<b>","$$NOW"]})", - expression->serialize(options).getDocument()); -} - /** The field path itself is a dependency. */ class Dependencies { public: @@ -490,7 +424,7 @@ public: ExpressionFieldPath::deprecatedCreate(&expCtx, "a.b.c"); ASSERT_BSONOBJ_BINARY_EQ(BSON("foo" << "$a.b.c"), - BSON("foo" << expression->serialize())); + BSON("foo" << expression->serialize(false))); } }; @@ -502,7 +436,7 @@ public: intrusive_ptr<Expression> expression = ExpressionFieldPath::deprecatedCreate(&expCtx, "a.b.c"); BSONArrayBuilder bab; - bab << expression->serialize(); + bab << expression->serialize(false); ASSERT_BSONOBJ_BINARY_EQ(BSON_ARRAY("$a.b.c"), bab.arr()); } }; diff --git a/src/mongo/db/pipeline/expression_find_internal.h b/src/mongo/db/pipeline/expression_find_internal.h index 36f88ded4aa..68d535e5b28 100644 --- a/src/mongo/db/pipeline/expression_find_internal.h +++ b/src/mongo/db/pipeline/expression_find_internal.h @@ -78,7 +78,7 @@ public: return visitor->visit(this); } - Value serialize(const SerializationOptions& options) const final { + Value serialize(bool explain) const final { MONGO_UNREACHABLE; } @@ -153,7 +153,7 @@ public: return visitor->visit(this); } - Value serialize(const SerializationOptions& options) const final { + Value serialize(bool explain) const final { MONGO_UNREACHABLE; } @@ -214,7 +214,7 @@ public: return visitor->visit(this); } - Value serialize(const SerializationOptions& options) const final { + Value serialize(bool explain) const final { MONGO_UNREACHABLE; } diff --git a/src/mongo/db/pipeline/expression_function.cpp b/src/mongo/db/pipeline/expression_function.cpp index 7bf43ffd1c9..c53b0004b31 100644 --- a/src/mongo/db/pipeline/expression_function.cpp +++ b/src/mongo/db/pipeline/expression_function.cpp @@ -46,18 +46,16 @@ ExpressionFunction::ExpressionFunction(ExpressionContext* const expCtx, expCtx->sbeCompatible = false; } -Value ExpressionFunction::serialize(const SerializationOptions& options) const { - MutableDocument innerOpts(Document{{"body"_sd, options.serializeLiteral(_funcSource)}, - {"args"_sd, _passedArgs->serialize(options)}, - // "lang" is purposefully not treated as a literal since it - // is more of a selection of an enum - {"lang"_sd, _lang}}); - +Value ExpressionFunction::serialize(bool explain) const { + MutableDocument d; + d["body"] = Value(_funcSource); + d["args"] = Value(_passedArgs->serialize(explain)); + d["lang"] = Value(_lang); // This field will only be seralized when desugaring $where in $expr + $_internalJs if (_assignFirstArgToThis) { - innerOpts["_internalSetObjToThis"] = options.serializeLiteral(_assignFirstArgToThis); + d["_internalSetObjToThis"] = Value(_assignFirstArgToThis); } - return Value(Document{{kExpressionName, innerOpts.freezeToValue()}}); + return Value(Document{{kExpressionName, d.freezeToValue()}}); } void ExpressionFunction::_doAddDependencies(mongo::DepsTracker* deps) const { diff --git a/src/mongo/db/pipeline/expression_function.h b/src/mongo/db/pipeline/expression_function.h index a4d50dcb588..b8d6b95e35a 100644 --- a/src/mongo/db/pipeline/expression_function.h +++ b/src/mongo/db/pipeline/expression_function.h @@ -69,7 +69,7 @@ public: Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); diff --git a/src/mongo/db/pipeline/expression_function_test.cpp b/src/mongo/db/pipeline/expression_function_test.cpp deleted file mode 100644 index aa631a53c77..00000000000 --- a/src/mongo/db/pipeline/expression_function_test.cpp +++ /dev/null @@ -1,57 +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 "mongo/platform/basic.h" - -#include "mongo/db/exec/document_value/document_value_test_util.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/db/pipeline/expression_function.h" -#include "mongo/db/query/query_shape/serialization_options.h" -#include "mongo/dbtests/dbtests.h" - -namespace mongo { - -namespace { - -TEST(ExpressionFunction, SerializeAndRedactArgs) { - SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST; - - auto expCtx = ExpressionContextForTest(); - auto expr = BSON("$function" << BSON("body" - << "function(age) {return age >= 21;}" - << "args" << BSON_ARRAY("$age") << "lang" - << "js")); - VariablesParseState vps = expCtx.variablesParseState; - auto exprFunc = ExpressionFunction::parse(&expCtx, expr.firstElement(), vps); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({"$function":{"body":"?string","args":["$HASH<age>"],"lang":"js"}})", - exprFunc->serialize(options).getDocument()); -} -} // namespace -} // namespace mongo diff --git a/src/mongo/db/pipeline/expression_js_emit.cpp b/src/mongo/db/pipeline/expression_js_emit.cpp index dd43f587016..d19a9c53191 100644 --- a/src/mongo/db/pipeline/expression_js_emit.cpp +++ b/src/mongo/db/pipeline/expression_js_emit.cpp @@ -130,10 +130,10 @@ boost::intrusive_ptr<Expression> ExpressionInternalJsEmit::parse(ExpressionConte return new ExpressionInternalJsEmit(expCtx, std::move(thisRef), std::move(funcSourceString)); } -Value ExpressionInternalJsEmit::serialize(const SerializationOptions& options) const { +Value ExpressionInternalJsEmit::serialize(bool explain) const { return Value( Document{{kExpressionName, - Document{{"eval", _funcSource}, {"this", _thisRef->serialize(options)}}}}); + Document{{"eval", _funcSource}, {"this", _thisRef->serialize(explain)}}}}); } Value ExpressionInternalJsEmit::evaluate(const Document& root, Variables* variables) const { @@ -145,14 +145,11 @@ Value ExpressionInternalJsEmit::evaluate(const Document& root, Variables* variab ExpressionContext* expCtx = getExpressionContext(); auto jsExec = expCtx->getJsExecWithScope(); - - // Inject the native "emit" function to be called from the user-defined map function. - // - // We reinject this function on every invocation of evaluate(), because there is a single - // JsExecution instance for the OperationContext, which may be shared by multiple aggregation - // pipelines and we need to ensure that the injected function still points to the valid - // contextual data ('_emitState'). - jsExec->injectEmit(emitFromJS, &_emitState); + // Inject the native "emit" function to be called from the user-defined map function. This + // particular Expression/ExpressionContext may be reattached to a new OperationContext (and thus + // a new JS Scope) when used across getMore operations, so this method will handle that case for + // us by only injecting if we haven't already. + jsExec->injectEmitIfNecessary(emitFromJS, &_emitState); // Although inefficient to "create" a new function every time we evaluate, this will usually end // up being a simple cache lookup. This is needed because the JS Scope may have been recreated diff --git a/src/mongo/db/pipeline/expression_js_emit.h b/src/mongo/db/pipeline/expression_js_emit.h index 5202582d8ee..e19907de84e 100644 --- a/src/mongo/db/pipeline/expression_js_emit.h +++ b/src/mongo/db/pipeline/expression_js_emit.h @@ -55,7 +55,7 @@ public: Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); diff --git a/src/mongo/db/pipeline/expression_let_test.cpp b/src/mongo/db/pipeline/expression_let_test.cpp deleted file mode 100644 index 6b851f18523..00000000000 --- a/src/mongo/db/pipeline/expression_let_test.cpp +++ /dev/null @@ -1,103 +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 "mongo/platform/basic.h" - -#include "mongo/db/exec/document_value/document_value_test_util.h" -#include "mongo/db/pipeline/expression.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace ExpressionTests { -namespace { - -TEST(RedactionTest, ExpressionLet) { - SerializationOptions options = SerializationOptions::kMarkIdentifiers_FOR_TEST; - - auto expCtx = ExpressionContextForTest{}; - - auto expression = Expression::parseExpression(&expCtx, - fromjson(R"( - {$let: {vars: {foo: 35}, in: {$gt: ["$$foo", 23]}}} - )"), - expCtx.variablesParseState); - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$let": { - "vars": { - "HASH<foo>": { - "$const": 35 - } - }, - "in": { - "$gt": [ - "$$HASH<foo>", - { - "$const": 23 - } - ] - } - } - })", - expression->serialize(options).getDocument()); - - expression = Expression::parseExpression(&expCtx, - fromjson(R"( - {$let: {vars: {foo: 35, myNow: "$$NOW", obj: {hello: 23}}, in: {$gt: ["$$myNow", "$$obj.hello"]}}} - )"), - expCtx.variablesParseState); - - ASSERT_DOCUMENT_EQ_AUTO( // NOLINT - R"({ - "$let": { - "vars": { - "HASH<foo>": { - "$const": 35 - }, - "HASH<myNow>": "$$NOW", - "HASH<obj>": { - "HASH<hello>": { - "$const": 23 - } - } - }, - "in": { - "$gt": [ - "$$HASH<myNow>", - "$$HASH<obj>.HASH<hello>" - ] - } - } - })", - expression->serialize(options).getDocument()); -} -} // namespace -} // namespace ExpressionTests -} // namespace mongo diff --git a/src/mongo/db/pipeline/expression_nary_test.cpp b/src/mongo/db/pipeline/expression_nary_test.cpp index cc01d807462..15b8e824f7c 100644 --- a/src/mongo/db/pipeline/expression_nary_test.cpp +++ b/src/mongo/db/pipeline/expression_nary_test.cpp @@ -120,7 +120,7 @@ static BSONObj constify(const BSONObj& obj, bool parentIsArray = false) { /** Convert Expression to BSON. */ static BSONObj expressionToBson(const intrusive_ptr<Expression>& expression) { - return BSON("" << expression->serialize()).firstElement().embeddedObject().getOwned(); + return BSON("" << expression->serialize(false)).firstElement().embeddedObject().getOwned(); } class ExpressionBaseTest : public unittest::Test { @@ -249,59 +249,15 @@ TEST_F(ExpressionNaryTest, ValidateObjectExpressionDependency) { TEST_F(ExpressionNaryTest, SerializationToBsonObj) { _notAssociativeNorCommutative->addOperand(ExpressionConstant::create(&expCtx, Value(5))); ASSERT_BSONOBJ_EQ(BSON("foo" << BSON("$testable" << BSON_ARRAY(BSON("$const" << 5)))), - BSON("foo" << _notAssociativeNorCommutative->serialize())); + BSON("foo" << _notAssociativeNorCommutative->serialize(false))); } TEST_F(ExpressionNaryTest, SerializationToBsonArr) { _notAssociativeNorCommutative->addOperand(ExpressionConstant::create(&expCtx, Value(5))); ASSERT_BSONOBJ_EQ(constify(BSON_ARRAY(BSON("$testable" << BSON_ARRAY(5)))), - BSON_ARRAY(_notAssociativeNorCommutative->serialize())); + BSON_ARRAY(_notAssociativeNorCommutative->serialize(false))); } -TEST_F(ExpressionNaryTest, RedactsCorrectlyWithConstantArguments) { - _notAssociativeNorCommutative->addOperand(ExpressionConstant::create(&expCtx, Value(5))); - _notAssociativeNorCommutative->addOperand(ExpressionConstant::create(&expCtx, Value(10))); - _notAssociativeNorCommutative->addOperand(ExpressionConstant::create(&expCtx, Value(15))); - - SerializationOptions opts; - - // The default shape should wrap the constants in $const. - ASSERT_BSONOBJ_EQ( - BSON("foo" << BSON("$testable" << BSON_ARRAY(BSON("$const" << 5) << BSON("$const" << 10) - << BSON("$const" << 15)))), - BSON("foo" << _notAssociativeNorCommutative->serialize(opts))); - - // The representative shape should be an array of raw constants (i.e. not wrapped in $const). - opts.literalPolicy = LiteralSerializationPolicy::kToRepresentativeParseableValue; - ASSERT_BSONOBJ_EQ(BSON("foo" << BSON("$testable" << BSON_ARRAY(1 << 1 << 1))), - BSON("foo" << _notAssociativeNorCommutative->serialize(opts))); -} - -TEST_F(ExpressionNaryTest, RedactsCorrectlyWithMixedArguments) { - VariablesParseState vps = expCtx.variablesParseState; - _notAssociativeNorCommutative->addOperand(ExpressionConstant::create(&expCtx, Value(5))); - _notAssociativeNorCommutative->addOperand( - Expression::parseExpression(&expCtx, BSON("$sum" << BSON_ARRAY(1 << 2)), vps)); - _notAssociativeNorCommutative->addOperand(ExpressionFieldPath::parse(&expCtx, "$b", vps)); - - SerializationOptions opts; - - // The default shape should wrap the constants in $const. - ASSERT_BSONOBJ_EQ(BSON("foo" << BSON("$testable" << BSON_ARRAY( - BSON("$const" << 5) - << BSON("$sum" << BSON_ARRAY(BSON("$const" << 1) - << BSON("$const" << 2))) - << "$b"))), - BSON("foo" << _notAssociativeNorCommutative->serialize(opts))); - - // The representative shape should not wrap the constant in $const. - opts.literalPolicy = LiteralSerializationPolicy::kToRepresentativeParseableValue; - ASSERT_BSONOBJ_EQ(BSON("foo" << BSON("$testable" << BSON_ARRAY( - 1 << BSON("$sum" << BSON_ARRAY(1 << 1)) << "$b"))), - BSON("foo" << _notAssociativeNorCommutative->serialize(opts))); -} - - // Verify that the internal operands are optimized TEST_F(ExpressionNaryTest, InternalOperandOptimizationIsDone) { BSONArray spec = BSON_ARRAY(BSON("$and" << BSONArray()) << "$abc"); diff --git a/src/mongo/db/pipeline/expression_object_test.cpp b/src/mongo/db/pipeline/expression_object_test.cpp index b308b94efaa..7246e9569e3 100644 --- a/src/mongo/db/pipeline/expression_object_test.cpp +++ b/src/mongo/db/pipeline/expression_object_test.cpp @@ -64,7 +64,7 @@ TEST(ExpressionObjectParse, ShouldAcceptEmptyObject) { auto expCtx = ExpressionContextForTest{}; VariablesParseState vps = expCtx.variablesParseState; auto object = ExpressionObject::parse(&expCtx, BSONObj(), vps); - ASSERT_VALUE_EQ(Value(Document{}), object->serialize()); + ASSERT_VALUE_EQ(Value(Document{}), object->serialize(false)); } TEST(ExpressionObjectParse, ShouldAcceptLiteralsAsValues) { @@ -77,7 +77,7 @@ TEST(ExpressionObjectParse, ShouldAcceptLiteralsAsValues) { vps); auto expectedResult = Value(Document{{"a", literal(5)}, {"b", literal("string"_sd)}, {"c", literal(BSONNULL)}}); - ASSERT_VALUE_EQ(expectedResult, object->serialize()); + ASSERT_VALUE_EQ(expectedResult, object->serialize(false)); } TEST(ExpressionObjectParse, ShouldAccept_idAsFieldName) { @@ -85,7 +85,7 @@ TEST(ExpressionObjectParse, ShouldAccept_idAsFieldName) { VariablesParseState vps = expCtx.variablesParseState; auto object = ExpressionObject::parse(&expCtx, BSON("_id" << 5), vps); auto expectedResult = Value(Document{{"_id", literal(5)}}); - ASSERT_VALUE_EQ(expectedResult, object->serialize()); + ASSERT_VALUE_EQ(expectedResult, object->serialize(false)); } TEST(ExpressionObjectParse, ShouldAcceptFieldNameContainingDollar) { @@ -93,7 +93,7 @@ TEST(ExpressionObjectParse, ShouldAcceptFieldNameContainingDollar) { VariablesParseState vps = expCtx.variablesParseState; auto object = ExpressionObject::parse(&expCtx, BSON("a$b" << 5), vps); auto expectedResult = Value(Document{{"a$b", literal(5)}}); - ASSERT_VALUE_EQ(expectedResult, object->serialize()); + ASSERT_VALUE_EQ(expectedResult, object->serialize(false)); } TEST(ExpressionObjectParse, ShouldAcceptNestedObjects) { @@ -104,7 +104,7 @@ TEST(ExpressionObjectParse, ShouldAcceptNestedObjects) { auto expectedResult = Value(Document{{"a", Document{{"b", literal(1)}}}, {"c", Document{{"d", Document{{"e", literal(1)}, {"f", literal(1)}}}}}}); - ASSERT_VALUE_EQ(expectedResult, object->serialize()); + ASSERT_VALUE_EQ(expectedResult, object->serialize(false)); } TEST(ExpressionObjectParse, ShouldAcceptArrays) { @@ -113,14 +113,15 @@ TEST(ExpressionObjectParse, ShouldAcceptArrays) { auto object = ExpressionObject::parse(&expCtx, fromjson("{a: [1, 2]}"), vps); auto expectedResult = Value(Document{{"a", vector<Value>{Value(literal(1)), Value(literal(2))}}}); - ASSERT_VALUE_EQ(expectedResult, object->serialize()); + ASSERT_VALUE_EQ(expectedResult, object->serialize(false)); } TEST(ObjectParsing, ShouldAcceptExpressionAsValue) { auto expCtx = ExpressionContextForTest{}; VariablesParseState vps = expCtx.variablesParseState; auto object = ExpressionObject::parse(&expCtx, BSON("a" << BSON("$and" << BSONArray())), vps); - ASSERT_VALUE_EQ(object->serialize(), Value(Document{{"a", Document{{"$and", BSONArray()}}}})); + ASSERT_VALUE_EQ(object->serialize(false), + Value(Document{{"a", Document{{"$and", BSONArray()}}}})); } // diff --git a/src/mongo/db/pipeline/expression_or_test.cpp b/src/mongo/db/pipeline/expression_or_test.cpp index 00d8a9deb74..ca4f5fdf843 100644 --- a/src/mongo/db/pipeline/expression_or_test.cpp +++ b/src/mongo/db/pipeline/expression_or_test.cpp @@ -77,7 +77,7 @@ static BSONObj toBson(const Value& value) { /** Convert Expression to BSON. */ static BSONObj expressionToBson(const intrusive_ptr<Expression>& expression) { - return BSON("" << expression->serialize()).firstElement().embeddedObject().getOwned(); + return BSON("" << expression->serialize(false)).firstElement().embeddedObject().getOwned(); } /** Convert Document to BSON. */ diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index 645dcacdb68..fd6f1c3490e 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -44,7 +44,6 @@ #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/collation/collator_interface_mock.h" -#include "mongo/db/query/query_shape/serialization_options.h" #include "mongo/dbtests/dbtests.h" #include "mongo/idl/server_parameter_test_util.h" #include "mongo/logv2/log.h" @@ -124,7 +123,7 @@ static BSONObj toBson(const Value& value) { /** Convert Expression to BSON. */ static BSONObj expressionToBson(const intrusive_ptr<Expression>& expression) { - return BSON("" << expression->serialize()).firstElement().embeddedObject().getOwned(); + return BSON("" << expression->serialize(false)).firstElement().embeddedObject().getOwned(); } /** Convert Document to BSON. */ @@ -170,41 +169,6 @@ void parseAndVerifyResults( ASSERT_VALUE_EQ(expr->evaluate({}, &expCtx.variables), expected); } -/** - * Generates a random double with a variable number of decimal places between 1 and 15. - */ -double randomDouble() { - // Create a random number generator engine. - std::random_device rd; - std::mt19937 gen(rd()); - - // Create a distribution and generate a double between -1 and 1. - std::uniform_real_distribution<double> dis(-1.0, 1.0); - double randomValue = dis(gen); - - std::uniform_int_distribution<int> multiplier(0, 15); - int shift = multiplier(gen); - double factor = std::pow(10.0, shift); - - // Multiply the random number by the factor to set the decimal places - double result = randomValue * factor; - return result; -} - -/** - * Verifies that a double can correctly convert to a string and round-trip back to the original - * double. - */ -void verifyStringDoubleConvertRoundtripsCorrectly(double doubleToConvert) { - Value doubleConvertedToString = evaluateExpression("$toString", {doubleToConvert}); - ASSERT_EQ(doubleConvertedToString.getType(), BSONType::String); - - Value stringConvertedToDouble = evaluateExpression("$toDouble", {doubleConvertedToString}); - ASSERT_EQ(stringConvertedToDouble.getType(), BSONType::NumberDouble); - - // Verify the conversion round-trips correctly. - ASSERT_VALUE_EQ(stringConvertedToDouble, Value(doubleToConvert)); -} /* ------------------------- ExpressionArrayToObject -------------------------- */ @@ -710,7 +674,7 @@ public: private: static BSONObj toBsonObj(const intrusive_ptr<Expression>& expression) { - return BSON("field" << expression->serialize()); + return BSON("field" << expression->serialize(false)); } }; @@ -729,7 +693,7 @@ public: private: static BSONArray toBsonArray(const intrusive_ptr<Expression>& expression) { BSONArrayBuilder bab; - bab << expression->serialize(); + bab << expression->serialize(false); return bab.arr(); } }; @@ -805,7 +769,7 @@ public: private: static BSONObj toBsonObj(const intrusive_ptr<Expression>& expression) { - return BSON("field" << expression->serialize()); + return BSON("field" << expression->serialize(false)); } }; @@ -822,7 +786,7 @@ public: private: static BSONObj toBsonArray(const intrusive_ptr<Expression>& expression) { BSONArrayBuilder bab; - bab << expression->serialize(); + bab << expression->serialize(false); return bab.obj(); } }; @@ -840,26 +804,7 @@ TEST(ExpressionConstantTest, ConstantOfValueMissingSerializesToRemoveSystemVar) intrusive_ptr<Expression> expression = ExpressionConstant::create(&expCtx, Value()); ASSERT_BSONOBJ_BINARY_EQ(BSON("field" << "$$REMOVE"), - BSON("field" << expression->serialize())); -} - -TEST(ExpressionConstantTest, ConstantRedaction) { - SerializationOptions options; - options.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString; - - // Test that a constant is replaced. - auto expCtx = ExpressionContextForTest{}; - intrusive_ptr<Expression> expression = ExpressionConstant::create(&expCtx, Value("my_ssn"_sd)); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"field":"?string"})", - BSON("field" << expression->serialize(options))); - - auto expressionBSON = BSON("$and" << BSON_ARRAY(BSON("$gt" << BSON_ARRAY("$foo" << 5)) - << BSON("$lt" << BSON_ARRAY("$foo" << 10)))); - expression = Expression::parseExpression(&expCtx, expressionBSON, expCtx.variablesParseState); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"field":{"$and":[{"$gt":["$foo","?number"]},{"$lt":["$foo","?number"]}]}})", - BSON("field" << expression->serialize(options))); + BSON("field" << expression->serialize(false))); } } // namespace Constant @@ -1176,7 +1121,7 @@ TEST(ExpressionSwitch, ExpressionSwitchWithAllConstantFalsesAndNoDefaultErrors) ASSERT_THROWS_CODE(switchExp->optimize(), AssertionException, 40069); } -TEST(ExpressionSwitch, ExpressionSwitchWithZeroAsConstantFalseAndNoDefaultErrors) { +TEST(ExpressionSwitch, ExpressionSwitchWithZeroAsConstantFalsesAndNoDefaulErrors) { auto expCtx = ExpressionContextForTest{}; VariablesParseState vps = expCtx.variablesParseState; @@ -1288,62 +1233,6 @@ TEST(ExpressionSwitch, ExpressionSwitchWithNoConstantsShouldStayTheSame) { ASSERT_BSONOBJ_BINARY_EQ(switchQ, expressionToBson(optimizedStaySame)); } -// This test was designed to provide coverage for SERVER-70190, a bug in which optimizing a $switch -// expression could leave its children vector in a bad state. By walking the tree after optimizing -// we make sure that the expected children are found. -TEST(ExpressionSwitch, CaseEliminationShouldLeaveTreeInWalkableState) { - auto expCtx = ExpressionContextForTest{}; - VariablesParseState vps = expCtx.variablesParseState; - - BSONObj switchQ = fromjson(R"( - {$switch: { - branches: [ - {case: false, then: {$const: 0}}, - {case: "$z", then: {$const: 1}}, - {case: "$y", then: {$const: 3}}, - {case: true, then: {$const: 4}}, - {case: "$a", then: {$const: 5}}, - {case: "$b", then: {$const: 6}}, - {case: "$c", then: {$const: 7}} - ], - default: {$const: 8} - }} - )"); - auto switchExp = ExpressionSwitch::parse(&expCtx, switchQ.firstElement(), vps); - auto optimizedExpr = switchExp->optimize(); - - BSONObj optimizedQ = fromjson(R"( - {$switch: { - branches: [ - {case: "$z", then: {$const: 1}}, - {case: "$y", then: {$const: 3}} - ], - default: {$const: 4} - }} - )"); - - ASSERT_BSONOBJ_BINARY_EQ(optimizedQ, expressionToBson(optimizedExpr)); - - // Make sure that the expression tree appears as expected when the children are traversed using - // a for-each loop. - int childNum = 0; - int numConstants = 0; - for (auto&& child : optimizedExpr->getChildren()) { - // Children 0 and 2 are field path expressions, whereas 1, 3, and 4 are constants. - auto constExpr = dynamic_cast<ExpressionConstant*>(child.get()); - if (constExpr) { - ASSERT_VALUE_EQ(constExpr->getValue(), Value{childNum}); - ++numConstants; - } else { - ASSERT(dynamic_cast<ExpressionFieldPath*>(child.get())); - } - ++childNum; - } - // We should have seen 5 children total, 3 of which are constants. - ASSERT_EQ(childNum, 5); - ASSERT_EQ(numConstants, 3); -} - TEST(ExpressionArray, ExpressionArrayShouldOptimizeSubExpressionToExpressionConstant) { auto expCtx = ExpressionContextForTest{}; VariablesParseState vps = expCtx.variablesParseState; @@ -1602,7 +1491,7 @@ TEST(ParseExpression, ShouldRecognizeConstExpression) { auto resultExpression = parseExpression(BSON("$const" << 5)); auto constExpression = dynamic_cast<ExpressionConstant*>(resultExpression.get()); ASSERT_TRUE(constExpression); - ASSERT_VALUE_EQ(constExpression->serialize(), Value(Document{{"$const", 5}})); + ASSERT_VALUE_EQ(constExpression->serialize(false), Value(Document{{"$const", 5}})); } TEST(ParseExpression, ShouldRejectUnknownExpression) { @@ -1636,14 +1525,15 @@ TEST(ParseExpression, ShouldParseExpressionWithMultipleArguments) { ASSERT_TRUE(strCaseCmpExpression); vector<Value> arguments = {Value(Document{{"$const", "foo"_sd}}), Value(Document{{"$const", "FOO"_sd}})}; - ASSERT_VALUE_EQ(strCaseCmpExpression->serialize(), Value(Document{{"$strcasecmp", arguments}})); + ASSERT_VALUE_EQ(strCaseCmpExpression->serialize(false), + Value(Document{{"$strcasecmp", arguments}})); } TEST(ParseExpression, ShouldParseExpressionWithNoArguments) { auto resultExpression = parseExpression(BSON("$and" << BSONArray())); auto andExpression = dynamic_cast<ExpressionAnd*>(resultExpression.get()); ASSERT_TRUE(andExpression); - ASSERT_VALUE_EQ(andExpression->serialize(), Value(Document{{"$and", vector<Value>{}}})); + ASSERT_VALUE_EQ(andExpression->serialize(false), Value(Document{{"$and", vector<Value>{}}})); } TEST(ParseExpression, ShouldParseExpressionWithOneArgument) { @@ -1651,7 +1541,7 @@ TEST(ParseExpression, ShouldParseExpressionWithOneArgument) { auto andExpression = dynamic_cast<ExpressionAnd*>(resultExpression.get()); ASSERT_TRUE(andExpression); vector<Value> arguments = {Value(Document{{"$const", 1}})}; - ASSERT_VALUE_EQ(andExpression->serialize(), Value(Document{{"$and", arguments}})); + ASSERT_VALUE_EQ(andExpression->serialize(false), Value(Document{{"$and", arguments}})); } TEST(ParseExpression, ShouldAcceptArgumentWithoutArrayForVariadicExpressions) { @@ -1659,7 +1549,7 @@ TEST(ParseExpression, ShouldAcceptArgumentWithoutArrayForVariadicExpressions) { auto andExpression = dynamic_cast<ExpressionAnd*>(resultExpression.get()); ASSERT_TRUE(andExpression); vector<Value> arguments = {Value(Document{{"$const", 1}})}; - ASSERT_VALUE_EQ(andExpression->serialize(), Value(Document{{"$and", arguments}})); + ASSERT_VALUE_EQ(andExpression->serialize(false), Value(Document{{"$and", arguments}})); } TEST(ParseExpression, ShouldAcceptArgumentWithoutArrayAsSingleArgument) { @@ -1667,7 +1557,7 @@ TEST(ParseExpression, ShouldAcceptArgumentWithoutArrayAsSingleArgument) { auto notExpression = dynamic_cast<ExpressionNot*>(resultExpression.get()); ASSERT_TRUE(notExpression); vector<Value> arguments = {Value(Document{{"$const", 1}})}; - ASSERT_VALUE_EQ(notExpression->serialize(), Value(Document{{"$not", arguments}})); + ASSERT_VALUE_EQ(notExpression->serialize(false), Value(Document{{"$not", arguments}})); } TEST(ParseExpression, ShouldAcceptObjectAsSingleArgument) { @@ -1675,7 +1565,7 @@ TEST(ParseExpression, ShouldAcceptObjectAsSingleArgument) { auto andExpression = dynamic_cast<ExpressionAnd*>(resultExpression.get()); ASSERT_TRUE(andExpression); vector<Value> arguments = {Value(Document{{"$const", 1}})}; - ASSERT_VALUE_EQ(andExpression->serialize(), Value(Document{{"$and", arguments}})); + ASSERT_VALUE_EQ(andExpression->serialize(false), Value(Document{{"$and", arguments}})); } TEST(ParseExpression, ShouldAcceptObjectInsideArrayAsSingleArgument) { @@ -1683,7 +1573,7 @@ TEST(ParseExpression, ShouldAcceptObjectInsideArrayAsSingleArgument) { auto andExpression = dynamic_cast<ExpressionAnd*>(resultExpression.get()); ASSERT_TRUE(andExpression); vector<Value> arguments = {Value(Document{{"$const", 1}})}; - ASSERT_VALUE_EQ(andExpression->serialize(), Value(Document{{"$and", arguments}})); + ASSERT_VALUE_EQ(andExpression->serialize(false), Value(Document{{"$and", arguments}})); } } // namespace Expression @@ -1709,7 +1599,7 @@ TEST(ParseOperand, ShouldRecognizeFieldPath) { << "$field")); auto fieldPathExpression = dynamic_cast<ExpressionFieldPath*>(resultExpression.get()); ASSERT_TRUE(fieldPathExpression); - ASSERT_VALUE_EQ(fieldPathExpression->serialize(), Value("$field"_sd)); + ASSERT_VALUE_EQ(fieldPathExpression->serialize(false), Value("$field"_sd)); } TEST(ParseOperand, ShouldRecognizeStringLiteral) { @@ -1717,7 +1607,7 @@ TEST(ParseOperand, ShouldRecognizeStringLiteral) { << "foo")); auto constantExpression = dynamic_cast<ExpressionConstant*>(resultExpression.get()); ASSERT_TRUE(constantExpression); - ASSERT_VALUE_EQ(constantExpression->serialize(), Value(Document{{"$const", "foo"_sd}})); + ASSERT_VALUE_EQ(constantExpression->serialize(false), Value(Document{{"$const", "foo"_sd}})); } TEST(ParseOperand, ShouldRecognizeNestedArray) { @@ -1727,21 +1617,21 @@ TEST(ParseOperand, ShouldRecognizeNestedArray) { ASSERT_TRUE(arrayExpression); vector<Value> expectedSerializedArray = {Value(Document{{"$const", "foo"_sd}}), Value("$field"_sd)}; - ASSERT_VALUE_EQ(arrayExpression->serialize(), Value(expectedSerializedArray)); + ASSERT_VALUE_EQ(arrayExpression->serialize(false), Value(expectedSerializedArray)); } TEST(ParseOperand, ShouldRecognizeNumberLiteral) { auto resultExpression = parseOperand(BSON("" << 5)); auto constantExpression = dynamic_cast<ExpressionConstant*>(resultExpression.get()); ASSERT_TRUE(constantExpression); - ASSERT_VALUE_EQ(constantExpression->serialize(), Value(Document{{"$const", 5}})); + ASSERT_VALUE_EQ(constantExpression->serialize(false), Value(Document{{"$const", 5}})); } TEST(ParseOperand, ShouldRecognizeNestedExpression) { auto resultExpression = parseOperand(BSON("" << BSON("$and" << BSONArray()))); auto andExpression = dynamic_cast<ExpressionAnd*>(resultExpression.get()); ASSERT_TRUE(andExpression); - ASSERT_VALUE_EQ(andExpression->serialize(), Value(Document{{"$and", vector<Value>{}}})); + ASSERT_VALUE_EQ(andExpression->serialize(false), Value(Document{{"$and", vector<Value>{}}})); } } // namespace Operand @@ -1782,7 +1672,7 @@ public: if (ValueComparator().evaluate(result != expected)) { string errMsg = str::stream() << "for expression " << field.first.toString() << " with argument " - << args.toString() << " full tree: " << expr->serialize().toString() + << args.toString() << " full tree: " << expr->serialize(false).toString() << " expected: " << expected.toString() << " but got: " << result.toString(); FAIL(errMsg); @@ -2594,7 +2484,7 @@ TEST(BuiltinRemoveVariableTest, RemoveSerializesCorrectly) { auto expression = ExpressionFieldPath::parse(&expCtx, "$$REMOVE", vps); ASSERT_BSONOBJ_EQ(BSON("foo" << "$$REMOVE"), - BSON("foo" << expression->serialize())); + BSON("foo" << expression->serialize(false))); } TEST(BuiltinRemoveVariableTest, RemoveSerializesCorrectlyWithTrailingPath) { @@ -2603,7 +2493,7 @@ TEST(BuiltinRemoveVariableTest, RemoveSerializesCorrectlyWithTrailingPath) { auto expression = ExpressionFieldPath::parse(&expCtx, "$$REMOVE.a.b", vps); ASSERT_BSONOBJ_EQ(BSON("foo" << "$$REMOVE.a.b"), - BSON("foo" << expression->serialize())); + BSON("foo" << expression->serialize(false))); } TEST(BuiltinRemoveVariableTest, RemoveSerializesCorrectlyAfterOptimization) { @@ -2614,7 +2504,7 @@ TEST(BuiltinRemoveVariableTest, RemoveSerializesCorrectlyAfterOptimization) { ASSERT(dynamic_cast<ExpressionConstant*>(optimizedExpression.get())); ASSERT_BSONOBJ_EQ(BSON("foo" << "$$REMOVE"), - BSON("foo" << optimizedExpression->serialize())); + BSON("foo" << optimizedExpression->serialize(false))); } } // namespace BuiltinRemoveVariable @@ -2832,7 +2722,7 @@ public: if (ValueComparator().evaluate(result != expected)) { string errMsg = str::stream() << "for expression " << field.first.toString() << " with argument " - << args.toString() << " full tree: " << expr->serialize().toString() + << args.toString() << " full tree: " << expr->serialize(false).toString() << " expected: " << expected.toString() << " but got: " << result.toString(); FAIL(errMsg); @@ -3215,18 +3105,6 @@ TEST(ExpressionMetaTest, ExpressionMetaSearchScoreDetails) { Value val = expressionMeta->evaluate(doc.freeze(), &expCtx.variables); ASSERT_DOCUMENT_EQ(val.getDocument(), Document(details)); } - -TEST(ExpressionMetaTest, ExpressionMetaVectorSearchScore) { - auto expCtx = ExpressionContextForTest{}; - BSONObj expr = fromjson("{$meta: \"vectorSearchScore\"}"); - auto expressionMeta = - ExpressionMeta::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState); - - MutableDocument doc; - doc.metadata().setVectorSearchScore(1.23); - Value val = expressionMeta->evaluate(doc.freeze(), &expCtx.variables); - ASSERT_EQ(val.getDouble(), 1.23); -} } // namespace expression_meta_test namespace ExpressionRegexTest { @@ -3523,7 +3401,7 @@ TEST(ExpressionRandom, Basic) { intrusive_ptr<Expression> expression = Expression::parseExpression(&expCtx, fromjson("{ $rand: {} }"), vps); - const std::string& serialized = expression->serialize().getDocument().toString(); + const std::string& serialized = expression->serialize(false).getDocument().toString(); ASSERT_EQ("{$rand: {}}", serialized); const auto randFn = [&expression, &expCtx]() -> double { @@ -3660,18 +3538,11 @@ TEST(ExpressionGetFieldTest, GetFieldSerializesStringArgumentCorrectly) { VariablesParseState vps = expCtx.variablesParseState; BSONObj expr = fromjson("{$meta: \"foo\"}"); auto expression = ExpressionGetField::parse(&expCtx, expr.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "ignoredField": { - "$getField": { - "field": { - "$const": "foo" - }, - "input": "$$CURRENT" - } - } - })", - BSON("ignoredField" << expression->serialize())); + ASSERT_BSONOBJ_EQ(BSON("ignoredField" << BSON("$getField" << BSON("field" << BSON("$const" + << "foo") + << "input" + << "$$CURRENT"))), + BSON("ignoredField" << expression->serialize(false))); } TEST(ExpressionGetFieldTest, GetFieldSerializesCorrectly) { @@ -3679,220 +3550,12 @@ TEST(ExpressionGetFieldTest, GetFieldSerializesCorrectly) { VariablesParseState vps = expCtx.variablesParseState; BSONObj expr = fromjson("{$meta: {\"field\": \"foo\", \"input\": {a: 1}}}"); auto expression = ExpressionGetField::parse(&expCtx, expr.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "ignoredField": { - "$getField": { - "field": { - "$const": "foo" - }, - "input": { - "a": { - "$const": 1 - } - } - } - } - })", - BSON("ignoredField" << expression->serialize())); -} - -TEST(ExpressionGetFieldTest, GetFieldSerializesAndRedactsCorrectly) { - SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST; - auto expCtx = ExpressionContextForTest{}; - VariablesParseState vps = expCtx.variablesParseState; - - BSONObj expressionBSON = BSON("$getField" << BSON("field" - << "a" - << "input" - << "$b")); - - auto expression = ExpressionGetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"field":{"$getField":{"field":"HASH<a>","input":"$HASH<b>"}}})", - BSON("field" << expression->serialize(options))); - - // Test the shorthand syntax. - expressionBSON = BSON("$getField" - << "a"); - - expression = ExpressionGetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({"field":{"$getField":{"field":"HASH<a>","input":"$$CURRENT"}}})", - BSON("field" << expression->serialize(options))); - - // Test a field with '.' characters. - expressionBSON = BSON("$getField" - << "a.b.c"); - - expression = ExpressionGetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$getField": { - "field": "HASH<a>.HASH<b>.HASH<c>", - "input": "$$CURRENT" - } - } - })", - BSON("field" << expression->serialize(options))); - - // Test a field with a '$' character. - expressionBSON = BSON("$getField" - << "a.$b.c"); - - expression = ExpressionGetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$getField": { - "field": "HASH<a>.HASH<$b>.HASH<c>", - "input": "$$CURRENT" - } - } - })", - BSON("field" << expression->serialize(options))); - - // Test a field with a trailing '.' character (invalid FieldPath). - expressionBSON = BSON("$getField" - << "a.b.c."); - - expression = ExpressionGetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$getField": { - "field": "HASH<invalidFieldPathPlaceholder>", - "input": "$$CURRENT" - } - } - })", - BSON("field" << expression->serialize(options))); -} - -TEST(ExpressionSetFieldTest, SetFieldRedactsCorrectly) { - SerializationOptions options = SerializationOptions::kDebugShapeAndMarkIdentifiers_FOR_TEST; - auto expCtx = ExpressionContextForTest{}; - VariablesParseState vps = expCtx.variablesParseState; - - // Test that a set field redacts properly. - BSONObj expressionBSON = BSON("$setField" << BSON("field" - << "a" - << "input" - << "$b" - << "value" - << "$c")); - auto expression = ExpressionSetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$setField": { - "field": "HASH<a>", - "input": "$HASH<b>", - "value": "$HASH<c>" - } - } - })", - BSON("field" << expression->serialize(options))); - - // Object as input. - expressionBSON = BSON("$setField" << BSON("field" - << "a" - << "input" << BSON("a" << true) << "value" << 10)); - expression = ExpressionSetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$setField": { - "field": "HASH<a>", - "input": "?object", - "value": "?number" - } - } - })", - BSON("field" << expression->serialize(options))); - - // Nested object as input. - expressionBSON = - BSON("$setField" << BSON("field" - << "a" - << "input" << BSON("a" << BSON("b" << 5)) << "value" << 10)); - expression = ExpressionSetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$setField": { - "field": "HASH<a>", - "input": "?object", - "value": "?number" - } - } - })", - BSON("field" << expression->serialize(options))); - - // Object with field path in input. - expressionBSON = BSON("$setField" << BSON("field" - << "a" - << "input" - << BSON("a" - << "$field") - << "value" << 10)); - expression = ExpressionSetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$setField": { - "field": "HASH<a>", - "input": { - "HASH<a>": "$HASH<field>" - }, - "value": "?number" - } - } - })", - BSON("field" << expression->serialize(options))); - - // Object with field path in value. - expressionBSON = BSON("$setField" << BSON("field" - << "a" - << "input" - << BSON("a" - << "b") - << "value" - << BSON("c" - << "$d"))); - expression = ExpressionSetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$setField": { - "field": "HASH<a>", - "input": "?object", - "value": { - "HASH<c>": "$HASH<d>" - } - } - } - })", - BSON("field" << expression->serialize(options))); - - // Array as input. - expressionBSON = BSON("$setField" << BSON("field" - << "a" - << "input" << BSON("a" << BSON_ARRAY(3 << 4 << 5)) - << "value" << 10)); - expression = ExpressionSetField::parse(&expCtx, expressionBSON.firstElement(), vps); - ASSERT_BSONOBJ_EQ_AUTO( // NOLINT - R"({ - "field": { - "$setField": { - "field": "HASH<a>", - "input": "?object", - "value": "?number" - } - } - })", - BSON("field" << expression->serialize(options))); + ASSERT_BSONOBJ_EQ( + BSON("ignoredField" << BSON( + "$getField" << BSON("field" << BSON("$const" + << "foo") + << "input" << BSON("a" << BSON("$const" << 1))))), + BSON("ignoredField" << expression->serialize(false))); } TEST(ExpressionSetFieldTest, SetFieldSerializesCorrectly) { @@ -3906,7 +3569,7 @@ TEST(ExpressionSetFieldTest, SetFieldSerializesCorrectly) { << "foo") << "input" << BSON("a" << BSON("$const" << 1)) << "value" << BSON("$const" << 24)))), - BSON("ignoredField" << expression->serialize())); + BSON("ignoredField" << expression->serialize(false))); } TEST(ExpressionIfNullTest, OptimizedExpressionIfNullShouldRemoveNullConstant) { @@ -3926,7 +3589,7 @@ TEST(ExpressionIfNullTest, auto expr = fromjson("{$ifNull: [null, \"$a\"]}"); auto exprIfNull = ExpressionIfNull::parse(&expCtx, expr.firstElement(), vps); auto optimizedNullRemoved = exprIfNull->optimize(); - ASSERT_VALUE_EQ(optimizedNullRemoved->serialize(), Value("$a"_sd)); + ASSERT_VALUE_EQ(optimizedNullRemoved->serialize(false), Value("$a"_sd)); } TEST(ExpressionIfNullTest, OptimizedExpressionIfNullShouldRemoveAllNullConstantsButLast) { @@ -4258,7 +3921,7 @@ TEST(ExpressionFLETest, TestBinData_RoundTrip) { ASSERT_VALUE_EQ(exprFle->evaluate({}, &expCtx.variables), Value(true)); // Verify it round trips - auto value = exprFle->serialize(); + auto value = exprFle->serialize(false); auto roundTripExpr = fromjson(R"({$_internalFleEq: { field: { @@ -4283,22 +3946,10 @@ TEST(ExpressionFLETest, TestBinData_RoundTrip) { "subType": "6" } } - } })"); + } })"); ASSERT_BSONOBJ_EQ(value.getDocument().toBson(), roundTripExpr); } -/** - * Test case for round-trip conversion of random double using $convert. - * - * Generates 1000 random doubles and verifies they can be correctly converted to string values and - * back to double. - */ -TEST(ExpressionConvert, StringToDouble) { - for (int i = 0; i < 1000; ++i) { - verifyStringDoubleConvertRoundtripsCorrectly(randomDouble()); - } -} - } // namespace ExpressionTests diff --git a/src/mongo/db/pipeline/expression_test_api_version.cpp b/src/mongo/db/pipeline/expression_test_api_version.cpp index 08938f1afa8..893ddf301be 100644 --- a/src/mongo/db/pipeline/expression_test_api_version.cpp +++ b/src/mongo/db/pipeline/expression_test_api_version.cpp @@ -89,7 +89,7 @@ boost::intrusive_ptr<Expression> ExpressionTestApiVersion::parse(ExpressionConte return new ExpressionTestApiVersion(expCtx, unstableField, deprecatedField); } -Value ExpressionTestApiVersion::serialize(const SerializationOptions& options) const { +Value ExpressionTestApiVersion::serialize(bool explain) const { return Value(Document{{"$_testApiVersion", Document{{"unstable", _unstable ? Value(_unstable) : Value()}, {"deprecated", _deprecated ? Value(_deprecated) : Value()}}}}); diff --git a/src/mongo/db/pipeline/expression_test_api_version.h b/src/mongo/db/pipeline/expression_test_api_version.h index 0b67556efbe..cd5436d61b3 100644 --- a/src/mongo/db/pipeline/expression_test_api_version.h +++ b/src/mongo/db/pipeline/expression_test_api_version.h @@ -48,7 +48,7 @@ public: Value evaluate(const Document& root, Variables* variables) const final; - Value serialize(const SerializationOptions& options) const final; + Value serialize(bool explain) const final; void acceptVisitor(ExpressionMutableVisitor* visitor) final { return visitor->visit(this); diff --git a/src/mongo/db/pipeline/expression_test_api_version_test.cpp b/src/mongo/db/pipeline/expression_test_api_version_test.cpp index 231a47fbc27..05c9156ddf0 100644 --- a/src/mongo/db/pipeline/expression_test_api_version_test.cpp +++ b/src/mongo/db/pipeline/expression_test_api_version_test.cpp @@ -48,7 +48,7 @@ TEST_F(TestApiVersion, UnstableAcceptsBooleanValue) { expCtx->variablesParseState); ASSERT_VALUE_EQ(Value(DOC("$_testApiVersion" << DOC("unstable" << true))), - expression->serialize()); + expression->serialize(false)); } TEST_F(TestApiVersion, UnstableDoesNotAcceptNumericValue) { @@ -70,7 +70,7 @@ TEST_F(TestApiVersion, DeprecatedAcceptsBooleanValue) { expCtx->variablesParseState); ASSERT_VALUE_EQ(Value(DOC("$_testApiVersion" << DOC("deprecated" << true))), - expression->serialize()); + expression->serialize(false)); } TEST_F(TestApiVersion, DeprecatedDoesNotAcceptNumericValue) { diff --git a/src/mongo/db/pipeline/expression_trim_test.cpp b/src/mongo/db/pipeline/expression_trim_test.cpp index bf5a5d63302..620b371c5f7 100644 --- a/src/mongo/db/pipeline/expression_trim_test.cpp +++ b/src/mongo/db/pipeline/expression_trim_test.cpp @@ -722,16 +722,14 @@ TEST(ExpressionTrimTest, DoesSerializeCorrectly) { BSON("$trim" << BSON("input" << " abc ")), expCtx.variablesParseState); - ASSERT_VALUE_EQ(trim->serialize(), - trim->serialize(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})); + ASSERT_VALUE_EQ(trim->serialize(false), trim->serialize(true)); ASSERT_VALUE_EQ( - trim->serialize(), + trim->serialize(false), Value(Document{{"$trim", Document{{"input", Document{{"$const", " abc "_sd}}}}}})); // Make sure we can re-parse it and evaluate it. auto reparsedTrim = Expression::parseExpression( - &expCtx, trim->serialize().getDocument().toBson(), expCtx.variablesParseState); + &expCtx, trim->serialize(false).getDocument().toBson(), expCtx.variablesParseState); ASSERT_VALUE_EQ(reparsedTrim->evaluate({}, &expCtx.variables), Value("abc"_sd)); // Use $ltrim, and specify the 'chars' option. @@ -742,12 +740,12 @@ TEST(ExpressionTrimTest, DoesSerializeCorrectly) { << "$$CURRENT.a")), expCtx.variablesParseState); ASSERT_VALUE_EQ( - trim->serialize(), + trim->serialize(false), Value(Document{{"$ltrim", Document{{"input", "$inputField"_sd}, {"chars", "$a"_sd}}}})); // Make sure we can re-parse it and evaluate it. reparsedTrim = Expression::parseExpression( - &expCtx, trim->serialize().getDocument().toBson(), expCtx.variablesParseState); + &expCtx, trim->serialize(false).getDocument().toBson(), expCtx.variablesParseState); ASSERT_VALUE_EQ(reparsedTrim->evaluate(Document{{"inputField", " , 4"_sd}, {"a", " ,"_sd}}, &expCtx.variables), Value("4"_sd)); diff --git a/src/mongo/db/pipeline/expression_visitor.h b/src/mongo/db/pipeline/expression_visitor.h index 70804bc3400..6b7c4fc4cdd 100644 --- a/src/mongo/db/pipeline/expression_visitor.h +++ b/src/mongo/db/pipeline/expression_visitor.h @@ -179,7 +179,6 @@ class AccumulatorMergeObjects; class ExpressionTsSecond; class ExpressionTsIncrement; -class ExpressionInternalKeyStringValue; template <typename AccumulatorState> class ExpressionFromAccumulator; @@ -366,8 +365,6 @@ public: virtual void visit(expression_walker::MaybeConstPtr<IsConst, ExpressionSetField>) = 0; virtual void visit(expression_walker::MaybeConstPtr<IsConst, ExpressionTsSecond>) = 0; virtual void visit(expression_walker::MaybeConstPtr<IsConst, ExpressionTsIncrement>) = 0; - virtual void visit( - expression_walker::MaybeConstPtr<IsConst, ExpressionInternalKeyStringValue>) = 0; }; using ExpressionMutableVisitor = ExpressionVisitor<false>; @@ -528,6 +525,5 @@ struct SelectiveConstExpressionVisitorBase : public ExpressionConstVisitor { void visit(const ExpressionSetField*) override {} void visit(const ExpressionTsSecond*) override {} void visit(const ExpressionTsIncrement*) override {} - void visit(const ExpressionInternalKeyStringValue*) override {} }; } // namespace mongo diff --git a/src/mongo/db/pipeline/expression_walker_test.cpp b/src/mongo/db/pipeline/expression_walker_test.cpp index b03673c8df5..36fbe505cc8 100644 --- a/src/mongo/db/pipeline/expression_walker_test.cpp +++ b/src/mongo/db/pipeline/expression_walker_test.cpp @@ -198,7 +198,7 @@ TEST_F(ExpressionWalkerTest, SubstitutePathOnlySubstitutesPrefix) { auto expression = parseExpression("{$concat: ['$a', '$b', '$a.a', '$b.a', '$$NOW']}"); walk<Expression>(expression.get(), &substituteWalker); ASSERT_BSONOBJ_EQ(fromjson("{$concat: ['$b', '$b', '$b.a', '$b.a', '$$NOW']}"), - expression->serialize().getDocument().toBson()); + expression->serialize(false).getDocument().toBson()); } TEST_F(ExpressionWalkerTest, SubstitutePathSubstitutesWhenThereAreDottedFields) { @@ -207,7 +207,7 @@ TEST_F(ExpressionWalkerTest, SubstitutePathSubstitutesWhenThereAreDottedFields) auto expression = parseExpression("{$concat: ['$a.b', '$a.b.c', '$c', '$d.e.f']}"); walk<Expression>(expression.get(), &substituteWalker); ASSERT_BSONOBJ_EQ(fromjson("{$concat: ['$a.b', '$x', '$q.r', '$y.f']}"), - expression->serialize().getDocument().toBson()); + expression->serialize(false).getDocument().toBson()); } TEST_F(ExpressionWalkerTest, SubstitutePathSubstitutesWhenExpressionIsNested) { @@ -217,7 +217,7 @@ TEST_F(ExpressionWalkerTest, SubstitutePathSubstitutesWhenExpressionIsNested) { parseExpression("{$multiply: [{$add: ['$a.b', '$c']}, {$ifNull: ['$a.b.c', '$d']}]}"); walk<Expression>(expression.get(), &substituteWalker); ASSERT_BSONOBJ_EQ(fromjson("{$multiply: [{$add: ['$x', '$y']}, {$ifNull: ['$x.c', '$d']}]}"), - expression->serialize().getDocument().toBson()); + expression->serialize(false).getDocument().toBson()); } TEST_F(ExpressionWalkerTest, SubstitutePathDoesNotSubstitutesWhenExpressionHasNoFieldPaths) { @@ -226,7 +226,7 @@ TEST_F(ExpressionWalkerTest, SubstitutePathDoesNotSubstitutesWhenExpressionHasNo auto expression = parseExpression("{$multiply: [1, 2, 3, 4]}"); walk<Expression>(expression.get(), &substituteWalker); ASSERT_BSONOBJ_EQ(fromjson("{$multiply: [{$const: 1}, {$const: 2}, {$const: 3}, {$const: 4}]}"), - expression->serialize().getDocument().toBson()); + expression->serialize(false).getDocument().toBson()); } } // namespace diff --git a/src/mongo/db/pipeline/field_path.cpp b/src/mongo/db/pipeline/field_path.cpp index 28cd63e2c05..ab00617bbd3 100644 --- a/src/mongo/db/pipeline/field_path.cpp +++ b/src/mongo/db/pipeline/field_path.cpp @@ -54,12 +54,7 @@ const StringDataSet kAllowedDollarPrefixedFields = { "$sortKey", // This is necessary for the "showRecordId" feature. - "$recordId", - - // This is necessary for $search queries with a specified sort. - "$searchSortValues"_sd, - "$searchScore"_sd, -}; + "$recordId"}; } // namespace @@ -74,7 +69,7 @@ string FieldPath::getFullyQualifiedPath(StringData prefix, StringData suffix) { return str::stream() << prefix << "." << suffix; } -FieldPath::FieldPath(std::string inputPath, bool validateFieldNames) +FieldPath::FieldPath(std::string inputPath) : _fieldPath(std::move(inputPath)), _fieldPathDotPosition{string::npos}, _fieldHash{kHashUninitialized} { @@ -98,10 +93,7 @@ FieldPath::FieldPath(std::string inputPath, bool validateFieldNames) "FieldPath is too long", pathLength <= BSONDepth::getMaxAllowableDepth()); for (size_t i = 0; i < pathLength; ++i) { - const auto& fieldName = getFieldName(i); - if (validateFieldNames) { - uassertValidFieldName(fieldName); - } + uassertValidFieldName(getFieldName(i)); } } diff --git a/src/mongo/db/pipeline/field_path.h b/src/mongo/db/pipeline/field_path.h index 5b9516e8d27..d2ee93734e7 100644 --- a/src/mongo/db/pipeline/field_path.h +++ b/src/mongo/db/pipeline/field_path.h @@ -69,11 +69,9 @@ public: * * Field names are validated using uassertValidFieldName(). */ - /* implicit */ FieldPath(std::string inputPath, bool validateFieldNames = true); - /* implicit */ FieldPath(StringData inputPath, bool validateFieldNames = true) - : FieldPath(inputPath.toString(), validateFieldNames) {} - /* implicit */ FieldPath(const char* inputPath, bool validateFieldNames = true) - : FieldPath(std::string(inputPath), validateFieldNames) {} + /* implicit */ FieldPath(std::string inputPath); + /* implicit */ FieldPath(StringData inputPath) : FieldPath(inputPath.toString()) {} + /* implicit */ FieldPath(const char* inputPath) : FieldPath(std::string(inputPath)) {} /** * Returns the number of path elements in the field path. @@ -141,7 +139,6 @@ public: std::string fullPathWithPrefix() const { return prefix + _fieldPath; } - /** * A FieldPath like this but missing the first element (useful for recursion). * Precondition getPathLength() > 1. diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.cpp b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp deleted file mode 100644 index c766bcb44d0..00000000000 --- a/src/mongo/db/pipeline/group_from_first_document_transformation.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/db/pipeline/group_from_first_document_transformation.h" - -namespace mongo { -Document GroupFromFirstDocumentTransformation::applyTransformation(const Document& input) { - MutableDocument output(_accumulatorExprs.size()); - - for (auto&& expr : _accumulatorExprs) { - auto value = expr.second->evaluate(input, &expr.second->getExpressionContext()->variables); - output.addField(expr.first, value.missing() ? Value(BSONNULL) : std::move(value)); - } - - return output.freeze(); -} - -void GroupFromFirstDocumentTransformation::optimize() { - for (auto&& expr : _accumulatorExprs) { - expr.second = expr.second->optimize(); - } -} - -Document GroupFromFirstDocumentTransformation::serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain, const SerializationOptions& options) const { - MutableDocument newRoot(_accumulatorExprs.size()); - - for (auto&& expr : _accumulatorExprs) { - newRoot.addField(expr.first, expr.second->serialize(options)); - } - - return {{"newRoot", newRoot.freezeToValue()}}; -} - -DepsTracker::State GroupFromFirstDocumentTransformation::addDependencies(DepsTracker* deps) const { - for (auto&& expr : _accumulatorExprs) { - expr.second->addDependencies(deps); - } - - // This stage will replace the entire document with a new document, so any existing fields - // will be replaced and cannot be required as dependencies. We use EXHAUSTIVE_ALL here - // instead of EXHAUSTIVE_FIELDS, as in ReplaceRootTransformation, because the stages that - // follow a $group stage should not depend on document metadata. - return DepsTracker::State::EXHAUSTIVE_ALL; -} - -DocumentSource::GetModPathsReturn GroupFromFirstDocumentTransformation::getModifiedPaths() const { - // Replaces the entire root, so all paths are modified. - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; -} - -std::unique_ptr<GroupFromFirstDocumentTransformation> GroupFromFirstDocumentTransformation::create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const std::string& groupId, - StringData originalStageName, - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs) { - return std::make_unique<GroupFromFirstDocumentTransformation>( - groupId, originalStageName, std::move(accumulatorExprs)); -} - -} // namespace mongo diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.h b/src/mongo/db/pipeline/group_from_first_document_transformation.h deleted file mode 100644 index 318a18cdb89..00000000000 --- a/src/mongo/db/pipeline/group_from_first_document_transformation.h +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include "mongo/db/pipeline/expression.h" -#include "mongo/db/pipeline/transformer_interface.h" - -namespace mongo { - -/** - * GroupFromFirstTransformation consists of a list of (field name, expression pairs). It returns a - * document synthesized by assigning each field name in the output document to the result of - * evaluating the corresponding expression. If the expression evaluates to missing, we assign a - * value of BSONNULL. This is necessary to match the semantics of $first for missing fields. - */ -class GroupFromFirstDocumentTransformation final : public TransformerInterface { -public: - GroupFromFirstDocumentTransformation( - const std::string& groupId, - StringData originalStageName, - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs) - : _accumulatorExprs(std::move(accumulatorExprs)), - _groupId(groupId), - _originalStageName(originalStageName) {} - - TransformerType getType() const final { - return TransformerType::kGroupFromFirstDocument; - } - - /** - * The path of the field that we are grouping on: i.e., the field in the input document that we - * will use to create the _id field of the ouptut document. - */ - const std::string& groupId() const { - return _groupId; - } - - StringData originalStageName() const { - return _originalStageName; - } - - Document applyTransformation(const Document& input) final; - - void optimize() final; - - Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options = {}) const final; - - DepsTracker::State addDependencies(DepsTracker* deps) const final; - - DocumentSource::GetModPathsReturn getModifiedPaths() const final; - - static std::unique_ptr<GroupFromFirstDocumentTransformation> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const std::string& groupId, - StringData originalStageName, - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs); - -private: - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> _accumulatorExprs; - std::string _groupId; - StringData _originalStageName; -}; - -} // namespace mongo diff --git a/src/mongo/db/pipeline/javascript_execution.cpp b/src/mongo/db/pipeline/javascript_execution.cpp index 8b6d3bc6f9b..f9ce429ba76 100644 --- a/src/mongo/db/pipeline/javascript_execution.cpp +++ b/src/mongo/db/pipeline/javascript_execution.cpp @@ -47,35 +47,20 @@ JsExecution* JsExecution::get(OperationContext* opCtx, StringData database, bool loadStoredProcedures, boost::optional<int> jsHeapLimitMB) { - // If a JsExecution object has already been created, return it. - JsExecution* jsExec = getCached(opCtx, loadStoredProcedures); - if (jsExec) { - return jsExec; - } - - // There is no cached JsExecution object, so create and cache one now. auto& exec = getExec(opCtx); - exec = std::make_unique<JsExecution>(opCtx, scope, jsHeapLimitMB); - exec->getScope()->setLocalDB(database); - if (loadStoredProcedures) { - exec->getScope()->loadStored(opCtx, true); - } - exec->_storedProceduresLoaded = loadStoredProcedures; - - return exec.get(); -} - -JsExecution* JsExecution::getCached(OperationContext* opCtx, bool loadStoredProcedures) { - auto& exec = getExec(opCtx); - if (exec) { - if (loadStoredProcedures == exec->_storedProceduresLoaded) { - return exec.get(); + if (!exec) { + exec = std::make_unique<JsExecution>(opCtx, scope, jsHeapLimitMB); + exec->getScope()->setLocalDB(database); + if (loadStoredProcedures) { + exec->getScope()->loadStored(opCtx, true); } - tasserted( - 9136200, - "A single operation cannot use both JavaScript aggregation expressions and $where."); + exec->_storedProceduresLoaded = loadStoredProcedures; + } else { + uassert(31438, + "A single operation cannot use both JavaScript aggregation expressions and $where.", + loadStoredProcedures == exec->_storedProceduresLoaded); } - return nullptr; + return exec.get(); } Value JsExecution::callFunction(ScriptingFunction func, diff --git a/src/mongo/db/pipeline/javascript_execution.h b/src/mongo/db/pipeline/javascript_execution.h index a66dee6c6ca..924ae447216 100644 --- a/src/mongo/db/pipeline/javascript_execution.h +++ b/src/mongo/db/pipeline/javascript_execution.h @@ -58,15 +58,6 @@ public: StringData database, bool loadStoredProcedures, boost::optional<int> jsHeapLimitMB); - - /** - * Gets a pointer to the cached JsExecution instance if it exists, else returns nullptr. This - * allows the caller to skip passing all the arguments to the more general JsExecution::get() - * method, which avoids having to potentially construct and create a heap copy of a large scope - * object that will not be used because a cached JsExecution object already exists. - */ - static JsExecution* getCached(OperationContext* opCtx, bool loadStoredProcedures); - /** * Construct with a thread-local scope and initialize with the given scope variables. */ @@ -113,8 +104,11 @@ public: * Injects the given function 'emitFn' as a native JS function named 'emit', callable from * user-defined functions. */ - void injectEmit(NativeFunction emitFn, void* data) { - _scope->injectNative("emit", emitFn, data); + void injectEmitIfNecessary(NativeFunction emitFn, void* data) { + if (!_emitCreated) { + _scope->injectNative("emit", emitFn, data); + _emitCreated = true; + } } Scope* getScope() { @@ -124,6 +118,7 @@ public: private: BSONObj _scopeVars; std::unique_ptr<Scope> _scope; + bool _emitCreated = false; bool _storedProceduresLoaded = false; int _fnCallTimeoutMillis; diff --git a/src/mongo/db/pipeline/lite_parsed_document_source.h b/src/mongo/db/pipeline/lite_parsed_document_source.h index aed1d4fefcf..34648576917 100644 --- a/src/mongo/db/pipeline/lite_parsed_document_source.h +++ b/src/mongo/db/pipeline/lite_parsed_document_source.h @@ -171,20 +171,6 @@ public: } /** - * Returns true if this is a $changeStreamSplitLargeEvent stage. - */ - virtual bool isChangeStreamSplitLargeEvent() const { - return false; - } - - /** - * Returns true if this is a $documents stage. - */ - virtual bool isDocuments() const { - return false; - } - - /** * Returns true if this stage does not require an input source. */ virtual bool isInitialSource() const { diff --git a/src/mongo/db/pipeline/lite_parsed_pipeline.cpp b/src/mongo/db/pipeline/lite_parsed_pipeline.cpp index f5760429206..36b1d78d9b7 100644 --- a/src/mongo/db/pipeline/lite_parsed_pipeline.cpp +++ b/src/mongo/db/pipeline/lite_parsed_pipeline.cpp @@ -157,6 +157,7 @@ void LiteParsedPipeline::tickGlobalStageCounters() const { void LiteParsedPipeline::validate(const OperationContext* opCtx, bool performApiVersionChecks) const { + int internalUnpackBucketCount = 0; for (auto&& stage : _stageSpecs) { const auto& stageName = stage->getParseTimeName(); const auto& stageInfo = LiteParsedDocumentSource::getInfo(stageName); @@ -178,10 +179,25 @@ void LiteParsedPipeline::validate(const OperationContext* opCtx, sometimesCallback); } + internalUnpackBucketCount += + (DocumentSourceInternalUnpackBucket::kStageNameInternal == stageName || + DocumentSourceInternalUnpackBucket::kStageNameExternal == stageName) + ? 1 + : 0; + for (auto&& subPipeline : stage->getSubPipelines()) { subPipeline.validate(opCtx, performApiVersionChecks); } } + + + // Validates that the pipeline contains at most one $_internalUnpackBucket or $_unpackBucket + // stage. + uassert(5348302, + str::stream() << "Encountered pipeline with more than one " + << DocumentSourceInternalUnpackBucket::kStageNameInternal << " or " + << DocumentSourceInternalUnpackBucket::kStageNameExternal << " stage", + internalUnpackBucketCount <= 1); } } // namespace mongo diff --git a/src/mongo/db/pipeline/lite_parsed_pipeline.h b/src/mongo/db/pipeline/lite_parsed_pipeline.h index 279135ece43..712d23c32bd 100644 --- a/src/mongo/db/pipeline/lite_parsed_pipeline.h +++ b/src/mongo/db/pipeline/lite_parsed_pipeline.h @@ -37,7 +37,6 @@ #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregate_command_gen.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/query/util/deferred.h" #include "mongo/db/read_concern_support_result.h" namespace mongo { @@ -67,8 +66,14 @@ public: /** * Returns all foreign namespaces referenced by stages within this pipeline, if any. */ - const stdx::unordered_set<NamespaceString>& getInvolvedNamespaces() const { - return _involvedNamespaces.get(_stageSpecs); + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const { + stdx::unordered_set<NamespaceString> involvedNamespaces; + for (auto&& spec : _stageSpecs) { + auto stagesInvolvedNamespaces = spec->getInvolvedNamespaces(); + involvedNamespaces.insert(stagesInvolvedNamespaces.begin(), + stagesInvolvedNamespaces.end()); + } + return involvedNamespaces; } /** @@ -118,24 +123,12 @@ public: } /** - * Returns true if the pipeline begins with a $documents stage. - */ - bool startsWithDocuments() const { - return !_stageSpecs.empty() && _stageSpecs.front()->isDocuments(); - } - - /** * Returns true if the pipeline has a $changeStream stage. */ bool hasChangeStream() const { - return _hasChangeStream.get(_stageSpecs); - } - - /** - * Returns true if the pipeline ends with a $changeStreamSplitLargeEvent stage. - */ - bool endsWithChangeStreamSplitLargeEvent() const { - return !_stageSpecs.empty() && _stageSpecs.back()->isChangeStreamSplitLargeEvent(); + return std::any_of(_stageSpecs.begin(), _stageSpecs.end(), [](auto&& spec) { + return spec->isChangeStream(); + }); } /** @@ -214,30 +207,13 @@ public: /** * Verifies that the pipeline contains valid stages. Optionally calls - * 'validatePipelineStagesforAPIVersion' with 'opCtx'. + * 'validatePipelineStagesforAPIVersion' with 'opCtx', and throws UserException if there is + * more than one $_internalUnpackBucket stage in the pipeline. */ void validate(const OperationContext* opCtx, bool performApiVersionChecks = true) const; private: - // This is logically const - any changes to _stageSpecs will invalidate cached copies of - // "_hasChangeStream" and "_involvedNamespaces" below. std::vector<std::unique_ptr<LiteParsedDocumentSource>> _stageSpecs; - Deferred<bool, const decltype(_stageSpecs)&> _hasChangeStream{[](const auto& stageSpecs) { - return std::any_of(stageSpecs.begin(), stageSpecs.end(), [](auto&& spec) { - return spec->isChangeStream(); - }); - }}; - - Deferred<stdx::unordered_set<NamespaceString>, const decltype(_stageSpecs)&> - _involvedNamespaces{[](const auto& stageSpecs) -> stdx::unordered_set<NamespaceString> { - stdx::unordered_set<NamespaceString> involvedNamespaces; - for (const auto& spec : stageSpecs) { - auto stagesInvolvedNamespaces = spec->getInvolvedNamespaces(); - involvedNamespaces.insert(stagesInvolvedNamespaces.begin(), - stagesInvolvedNamespaces.end()); - } - return involvedNamespaces; - }}; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/lookup_set_cache_test.cpp b/src/mongo/db/pipeline/lookup_set_cache_test.cpp index ec78b0b0572..6670b5b5971 100644 --- a/src/mongo/db/pipeline/lookup_set_cache_test.cpp +++ b/src/mongo/db/pipeline/lookup_set_cache_test.cpp @@ -214,7 +214,7 @@ TEST(LookupSetCacheTest, DocumentWithStorageCachePopulated) { // initialization. BSONObj input = BSON("a" << 1); const auto doc1 = Document(input); - const auto sizeOfDoc1Before = doc1.getCurrentApproximateSize(); + const auto sizeOfDoc1Before = doc1.getApproximateSize(); auto key = Value("foo"_sd); // Insert a cache entry and verify that both the key and the document are accounted for in the @@ -228,14 +228,14 @@ TEST(LookupSetCacheTest, DocumentWithStorageCachePopulated) { auto prevCacheSize = cache.getMemoryUsage(); const auto doc2 = Document({{"a", 2}}); cache.insert(key, doc2); - ASSERT_EQ(cache.getMemoryUsage(), prevCacheSize + doc2.getCurrentApproximateSize()); + ASSERT_EQ(cache.getMemoryUsage(), prevCacheSize + doc2.getApproximateSize()); // Calling serializeForSorter() should grow the overall document size. Verify that growing the // size of the 'Document' object does not have impact on the size stored in 'cache'. prevCacheSize = cache.getMemoryUsage(); BufBuilder builder; doc1.serializeForSorter(builder); - ASSERT_LT(sizeOfDoc1Before, doc1.getCurrentApproximateSize()); + ASSERT_LT(sizeOfDoc1Before, doc1.getApproximateSize()); ASSERT_EQ(prevCacheSize, cache.getMemoryUsage()); cache.evictOne(); diff --git a/src/mongo/db/pipeline/map_reduce_options.idl b/src/mongo/db/pipeline/map_reduce_options.idl deleted file mode 100644 index 55d5e133b6f..00000000000 --- a/src/mongo/db/pipeline/map_reduce_options.idl +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (C) 2022-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. -# - -global: - cpp_namespace: "mongo" - -server_parameters: - mrEnableSingleReduceOptimization: - description: > - In version 4.2 and before, MongoDB MapReduce will not call the reduce function - for a key that has only a single value. In version 4.4 and later, the reduce - function is still called in order to validate the JavaScript reduce function - even when there is only one value. This setting will re-enable the old optimization. - set_at: startup - cpp_vartype: bool - cpp_varname: mrSingleReduceOptimizationEnabled - default: false diff --git a/src/mongo/db/pipeline/memory_usage_tracker.h b/src/mongo/db/pipeline/memory_usage_tracker.h index b91f0eb57ac..275183081ef 100644 --- a/src/mongo/db/pipeline/memory_usage_tracker.h +++ b/src/mongo/db/pipeline/memory_usage_tracker.h @@ -50,8 +50,20 @@ public: PerFunctionMemoryTracker() = delete; void update(long long diff) { - // TODO SERVER-61281: Check for memory underflow. - set(std::max(_currentMemoryBytes + diff, 0ll)); + + // TODO SERVER-61281: this is a temporary measure in tackling the problem in this + // ticket. It prevents the underflow from happening but doesn't address the cause + // which is inaccurate tracking. + // Once inaccurate tracking is resolved, the underflow assertion below could be + // restored. + // tassert(5578603, + // str::stream() << "Underflow on memory tracking, attempting to add " << + // diff + // << " but only " << _currentMemoryBytes << " available", + // diff >= 0 || _currentMemoryBytes >= std::abs(diff)); + // set(_currentMemoryBytes + diff); + + set(std::max(_currentMemoryBytes + diff, 0LL)); } void set(long long total) { @@ -142,8 +154,11 @@ public: * Updates total memory usage. */ void update(long long diff) { - // TODO SERVER-61281: Check for memory underflow. - set(std::max(_memoryUsageBytes + diff, 0ll)); + tassert(5578602, + str::stream() << "Underflow on memory tracking, attempting to add " << diff + << " but only " << _memoryUsageBytes << " available", + diff >= 0 || (int)_memoryUsageBytes >= -1 * diff); + set(_memoryUsageBytes + diff); } auto currentMemoryBytes() const { diff --git a/src/mongo/db/pipeline/memory_usage_tracker_test.cpp b/src/mongo/db/pipeline/memory_usage_tracker_test.cpp index f60933a4655..cc354e7fd69 100644 --- a/src/mongo/db/pipeline/memory_usage_tracker_test.cpp +++ b/src/mongo/db/pipeline/memory_usage_tracker_test.cpp @@ -99,28 +99,28 @@ TEST_F(MemoryUsageTrackerTest, UpdateUsageUpdatesGlobal) { ASSERT_EQ(_tracker.maxMemoryBytes(), 150LL); } -// TODO SERVER-61281: Switch to 'DEATH_TEST_F' checking the underflow case. -TEST_F(MemoryUsageTrackerTest, UpdateFunctionUsageToNegativeIsDisallowed) { - _funcTracker.set(50LL); - ASSERT_EQ(_funcTracker.currentMemoryBytes(), 50LL); - ASSERT_EQ(_funcTracker.maxMemoryBytes(), 50LL); +DEATH_TEST_F(MemoryUsageTrackerTest, + UpdateGlobalToNegativeIsDisallowed, + "Underflow on memory tracking") { + _tracker.set(50LL); ASSERT_EQ(_tracker.currentMemoryBytes(), 50LL); ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); - _funcTracker.update(-100); - ASSERT_EQ(_tracker.currentMemoryBytes(), 0LL); - ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); + _tracker.update(-100); } -// TODO SERVER-61281: Switch to 'DEATH_TEST_F' checking the underflow case. -TEST_F(MemoryUsageTrackerTest, UpdateMemUsageToNegativeIsDisallowed) { - _tracker.set(50LL); +TEST_F(MemoryUsageTrackerTest, UpdateFunctionUsageToNegativeIsDisallowed) { + _funcTracker.set(50LL); ASSERT_EQ(_tracker.currentMemoryBytes(), 50LL); ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); - _tracker.update(-100); + // TODO SERVER-61281: Temporarily disable the assert (and associated test) in + // PerFunctionMemoryTracker.update() to prevent inaccurate tracking to cause underflow errors + // Once accurate tracking is implemented and no underflow should happen, this negative test + // could be restored to verify that "Underflow on memory tracking" is reported. + + _funcTracker.update(-100); ASSERT_EQ(_tracker.currentMemoryBytes(), 0LL); - ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); } } // namespace diff --git a/src/mongo/db/pipeline/monotonic_expression.cpp b/src/mongo/db/pipeline/monotonic_expression.cpp deleted file mode 100644 index bc34b756fec..00000000000 --- a/src/mongo/db/pipeline/monotonic_expression.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/db/pipeline/monotonic_expression.h" - -namespace mongo::monotonic { - -State opposite(State state) { - switch (state) { - case State::NonMonotonic: - case State::Constant: - return state; - case State::Increasing: - return State::Decreasing; - case State::Decreasing: - return State::Increasing; - }; - MONGO_UNREACHABLE; -} - -State combine(State lhs, State rhs) { - if (lhs == State::NonMonotonic || rhs == State::NonMonotonic) { - return State::NonMonotonic; - } - if (lhs == rhs || lhs == State::Constant) { - return rhs; - } - if (rhs == State::Constant) { - return lhs; - } - return State::NonMonotonic; -} - -} // namespace mongo::monotonic diff --git a/src/mongo/db/pipeline/monotonic_expression.h b/src/mongo/db/pipeline/monotonic_expression.h deleted file mode 100644 index 6f1664cf199..00000000000 --- a/src/mongo/db/pipeline/monotonic_expression.h +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (C) 2022-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. - */ - -#pragma once - -#include "mongo/db/pipeline/field_path.h" - -namespace mongo::monotonic { - -enum class State { NonMonotonic, Constant, Increasing, Decreasing }; - -/** - * Given monotonic states of function f(x), returns monotonic state of -f(x). If function is - * constant or non monotonic, it will remain the same. If it is increasing, it will become - * decreasing, and vice versa. - */ -State opposite(State state); - -/** - * Given monotonic states of functions f(x) and g(x), returns monotonic state of f(x)+g(x). Plus - * operator can be replaced with any operation that preserves monotonic behavior. - * - * If any argument is non monotonic, then the whole function is non monotonic. - * If one of the arguments is a constant, then the whole function has the same monotonic state as - * the other argument. If all arguments have the the same monotonic state, then the whole function - * has the same monotonic state. Otherwise, the result is NonMonotonic. - */ -State combine(State lhs, State rhs); - -template <typename ExpressionsContainer> -State combineExpressions(const FieldPath& sortedFieldPath, const ExpressionsContainer& container) { - return std::accumulate(container.begin(), - container.end(), - State::Constant, - [&](State state, const auto& expression) { - if (expression == nullptr) { - return state; - } - return combine(state, - expression->getMonotonicState(sortedFieldPath)); - }); -} - -} // namespace mongo::monotonic diff --git a/src/mongo/db/pipeline/monotonic_expression_test.cpp b/src/mongo/db/pipeline/monotonic_expression_test.cpp deleted file mode 100644 index 0c667108201..00000000000 --- a/src/mongo/db/pipeline/monotonic_expression_test.cpp +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (C) 2022-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 "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/db/pipeline/monotonic_expression.h" -#include "mongo/db/service_context_d_test_fixture.h" - -namespace mongo { -namespace { - -class MonotonicExpressionFixture : public ServiceContextMongoDTest { -public: - bool isMonotonicExpression(BSONObj expressionSpec, const FieldPath& monotonicField) { - auto expression = - Expression::parseExpression(&_expCtx, expressionSpec, _expCtx.variablesParseState); - return expression->isMonotonic(monotonicField); - } - -private: - ExpressionContextForTest _expCtx; -}; - -} // namespace - -TEST_F(MonotonicExpressionFixture, ConstIsMonotonic) { - ASSERT_TRUE(isMonotonicExpression(BSON("$const" << 1), "a")); -} - -TEST_F(MonotonicExpressionFixture, MonotonicFieldIsMonotonic) { - ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$a")), "a")); -} - -TEST_F(MonotonicExpressionFixture, NonMonotonicFieldIsNonMonotonic) { - ASSERT_FALSE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$b")), "a")); -} - -TEST_F(MonotonicExpressionFixture, MonotonicWithOppositeSignIsStillMonotonic) { - ASSERT_TRUE(isMonotonicExpression(BSON("$subtract" << BSON_ARRAY(1 << "$a")), "a")); -} - -TEST_F(MonotonicExpressionFixture, NestedOppositesAreProcessedCorrectlyForMonotonic) { - // Test expression is: dateDiff(1 - (1 - (1 - a))), a) - const auto& startDate = fromjson("{$subtract: [1, {$subtract: [1, {$subtract: [1, '$a']}]}]}"); - const auto& expression = BSON("$dateDiff" << BSON("startDate" << startDate << "endDate" - << "$a" - << "unit" - << "hours")); - ASSERT_TRUE(isMonotonicExpression(expression, "a")); -} - -TEST_F(MonotonicExpressionFixture, NestedOppositesAreProcessedCorrectlyForNonMonotonic) { - // LHS expression (1 - (1 - floor(a)) - 1 is increasing assuming $a is increasing. - const auto& lhs = - fromjson("{$subtract: [{$subtract: [1, {$subtract: [1, {$floor: '$a'}]}]}, 1]}"); - // RHS expression 1 - (1 - (1 - ceil(a))) is decreasing assuming $a is decreasing. - const auto& rhs = - fromjson("{$subtract: [1, {$subtract: [1, {$subtract: [1, {$ceil: '$a'}]}]}]}"); - // Both LHS and RHS are monotonic - ASSERT_TRUE(isMonotonicExpression(lhs, "a")); - ASSERT_TRUE(isMonotonicExpression(rhs, "a")); - // Because they are monotonic in different directions, their sum is non monotonic, but their - // difference is monotonic. - ASSERT_FALSE(isMonotonicExpression(BSON("$add" << BSON_ARRAY(lhs << rhs)), "a")); - ASSERT_TRUE(isMonotonicExpression(BSON("$subtract" << BSON_ARRAY(lhs << rhs)), "a")); -} - -TEST_F(MonotonicExpressionFixture, MonotonicAndNonMonotonicFieldIsNonMonotonic) { - ASSERT_FALSE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$a" - << "$b")), - "a")); -} - -TEST_F(MonotonicExpressionFixture, MonotonicAndConstantIsMonotonic) { - ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$a" - << "1")), - "a")); -} - -TEST_F(MonotonicExpressionFixture, ConstantAndConstantIsMonotonic) { - ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("1" - << "2")), - "a")); -} - -TEST_F(MonotonicExpressionFixture, MonotonicAndOppositeMonotonicIsNonMonotonic) { - ASSERT_FALSE(isMonotonicExpression(BSON("$subtract" << BSON_ARRAY("$a" - << "$a")), - "a")); -} - -TEST_F(MonotonicExpressionFixture, MonotonicAndMonotonicIsMonotonic) { - const auto& dateTrunc = BSON("$dateTrunc" << BSON("date" - << "$time" - << "unit" - << "hour" - << "timezone" - << "America/New_York")); - ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY(dateTrunc << "$time")), "time")); - ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$time" - << "$time")), - "time")); -} -TEST_F(MonotonicExpressionFixture, FunctionWithConstantNonMonotonicChildrenIsMonotonic) { - ASSERT_TRUE(isMonotonicExpression(BSON("$dateTrunc" << BSON("date" - << "$time" - << "unit" - << "hour" - << "timezone" - << "America/New_York")), - "time")); -} - -TEST_F(MonotonicExpressionFixture, FunctionWithNonConstantNonMonotonicChildrenIsNonMonotonic) { - ASSERT_FALSE(isMonotonicExpression(BSON("$dateTrunc" << BSON("date" - << "$time" - << "unit" - << "hour" - << "binSize" - << "$time")), - "time")); -} - -} // namespace mongo diff --git a/src/mongo/db/pipeline/pipeline.cpp b/src/mongo/db/pipeline/pipeline.cpp index 30dbcba6290..97a896e5898 100644 --- a/src/mongo/db/pipeline/pipeline.cpp +++ b/src/mongo/db/pipeline/pipeline.cpp @@ -40,7 +40,6 @@ #include "mongo/db/jsobj.h" #include "mongo/db/operation_context.h" #include "mongo/db/pipeline/accumulator.h" -#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/document_source_match.h" #include "mongo/db/pipeline/document_source_merge.h" @@ -106,32 +105,14 @@ void validateTopLevelPipeline(const Pipeline& pipeline) { // If the first stage is a $changeStream stage, then all stages in the pipeline must be // either $changeStream stages or allowlisted as being able to run in a change stream. - const bool isChangeStream = firstStageConstraints.isChangeStreamStage(); - // Record whether any of the stages in the pipeline is a $changeStreamSplitLargeEvent. - bool hasChangeStreamSplitLargeEventStage = false; - for (auto&& source : sources) { - uassert(ErrorCodes::IllegalOperation, - str::stream() << source->getSourceName() - << " is not permitted in a $changeStream pipeline", - !(isChangeStream && !source->constraints().isAllowedInChangeStream())); - // Check whether any stages must only be run in a change stream pipeline. - uassert(ErrorCodes::IllegalOperation, - str::stream() << source->getSourceName() - << " can only be used in a $changeStream pipeline", - !(source->constraints().requiresChangeStream() && !isChangeStream)); - // Check whether this is a change stream split stage. - if ("$changeStreamSplitLargeEvent"_sd == source->getSourceName()) { - hasChangeStreamSplitLargeEventStage = true; + if (firstStageConstraints.isChangeStreamStage()) { + for (auto&& source : sources) { + uassert(ErrorCodes::IllegalOperation, + str::stream() << source->getSourceName() + << " is not permitted in a $changeStream pipeline", + source->constraints().isAllowedInChangeStream()); } } - auto expCtx = pipeline.getContext(); - auto spec = isChangeStream ? expCtx->changeStreamSpec : boost::none; - auto hasSplitEventResumeToken = spec && - change_stream::resolveResumeTokenFromSpec(expCtx, *spec).fragmentNum.has_value(); - uassert(ErrorCodes::ChangeStreamFatalError, - "To resume from a split event, the $changeStream pipeline must include a " - "$changeStreamSplitLargeEvent stage", - !(hasSplitEventResumeToken && !hasChangeStreamSplitLargeEventStage)); } // Verify that usage of $searchMeta and $search is legal. @@ -174,12 +155,11 @@ Pipeline::~Pipeline() { std::unique_ptr<Pipeline, PipelineDeleter> Pipeline::clone( const boost::intrusive_ptr<ExpressionContext>& newExpCtx) const { - auto expCtx = newExpCtx ? newExpCtx : getContext(); SourceContainer clonedStages; for (auto&& stage : _sources) { - clonedStages.push_back(stage->clone(expCtx)); + clonedStages.push_back(stage->clone(newExpCtx)); } - return create(clonedStages, expCtx); + return create(clonedStages, newExpCtx ? newExpCtx : getContext()); } template <class T> @@ -251,43 +231,39 @@ std::unique_ptr<Pipeline, PipelineDeleter> Pipeline::create( } void Pipeline::validateCommon(bool alreadyOptimized) const { + size_t i = 0; + uassert(ErrorCodes::FailedToParse, str::stream() << "Pipeline length must be no longer than " << internalPipelineLengthLimit << " stages", static_cast<int>(_sources.size()) <= internalPipelineLengthLimit); - checkValidOperationContext(); - - // Keep track of stages which can only appear once. - std::set<StringData> singleUseStages; - - for (auto sourceIter = _sources.begin(); sourceIter != _sources.end(); ++sourceIter) { - auto& stage = *sourceIter; + for (auto&& stage : _sources) { auto constraints = stage->constraints(_splitState); // Verify that all stages adhere to their PositionRequirement constraints. uassert(40602, str::stream() << stage->getSourceName() << " is only valid as the first stage in a pipeline", - !(constraints.requiredPosition == PositionRequirement::kFirst && - sourceIter != _sources.begin())); + !(constraints.requiredPosition == PositionRequirement::kFirst && i != 0)); + uassert(40603, + str::stream() << stage->getSourceName() + << " is only valid as the first stage in an optimized pipeline", + !(alreadyOptimized && + constraints.requiredPosition == PositionRequirement::kFirstAfterOptimization && + i != 0)); - // TODO SERVER-73790: use PositionRequirement::kCustom to validate $match. auto matchStage = dynamic_cast<DocumentSourceMatch*>(stage.get()); uassert(17313, "$match with $text is only allowed as the first pipeline stage", - !(sourceIter != _sources.begin() && matchStage && matchStage->isTextQuery())); + !(i != 0 && matchStage && matchStage->isTextQuery())); uassert(40601, str::stream() << stage->getSourceName() << " can only be the final stage in the pipeline", !(constraints.requiredPosition == PositionRequirement::kLast && - std::next(sourceIter) != _sources.end())); - - // If the stage has a special requirement about its position, validate it. - if (constraints.requiredPosition == PositionRequirement::kCustom) { - stage->validatePipelinePosition(alreadyOptimized, sourceIter, _sources); - } + i != _sources.size() - 1)); + ++i; // Verify that we are not attempting to run a mongoS-only stage on mongoD. uassert(40644, @@ -299,17 +275,6 @@ void Pipeline::validateCommon(bool alreadyOptimized) const { str::stream() << "Stage not supported inside of a multi-document transaction: " << stage->getSourceName(), !(pCtx->opCtx->inMultiDocumentTransaction() && !constraints.isAllowedInTransaction())); - - // Verify that a stage which can only appear once doesn't appear more than that. - uassert(7183900, - str::stream() << stage->getSourceName() << " can only be used once in the pipeline", - !(constraints.canAppearOnlyOnceInPipeline && - !singleUseStages.insert(stage->getSourceName()).second)); - - tassert(7355707, - "If a stage is broadcast to all shard servers then it must be a data source.", - constraints.hostRequirement != HostTypeRequirement::kAllShardServers || - !constraints.requiresInputDocSource); } } @@ -318,29 +283,21 @@ void Pipeline::optimizePipeline() { if (MONGO_unlikely(disablePipelineOptimization.shouldFail())) { return; } + optimizeContainer(&_sources); - optimizeEachStage(&_sources); } void Pipeline::optimizeContainer(SourceContainer* container) { + SourceContainer optimizedSources; + SourceContainer::iterator itr = container->begin(); try { while (itr != container->end()) { invariant((*itr).get()); itr = (*itr).get()->optimizeAt(itr, container); } - } catch (DBException& ex) { - ex.addContext("Failed to optimize pipeline"); - throw; - } - stitch(container); -} - -void Pipeline::optimizeEachStage(SourceContainer* container) { - SourceContainer optimizedSources; - try { - // We should have our final number of stages. Optimize each individually. + // Once we have reached our final number of stages, optimize each individually. for (auto&& source : *container) { if (auto out = source->optimize()) { optimizedSources.push_back(out); @@ -381,9 +338,6 @@ void Pipeline::detachFromOperationContext() { for (auto&& source : _sources) { source->detachFromOperationContext(); } - - // Check for a null operation context to make sure that all children detached correctly. - checkValidOperationContext(); } void Pipeline::reattachToOperationContext(OperationContext* opCtx) { @@ -392,24 +346,6 @@ void Pipeline::reattachToOperationContext(OperationContext* opCtx) { for (auto&& source : _sources) { source->reattachToOperationContext(opCtx); } - - checkValidOperationContext(); -} - -bool Pipeline::validateOperationContext(const OperationContext* opCtx) const { - return std::all_of(_sources.begin(), _sources.end(), [this, opCtx](const auto& s) { - // All sources in a pipeline must share its expression context. Subpipelines may have a - // different expression context, but must point to the same operation context. Let the - // sources validate this themselves since they don't all have the same subpipelines, etc. - return s->getContext() == getContext() && s->validateOperationContext(opCtx); - }); -} - -void Pipeline::checkValidOperationContext() const { - tassert(7406000, - str::stream() - << "All DocumentSources and subpipelines must have the same operation context", - validateOperationContext(getContext()->opCtx)); } void Pipeline::dispose(OperationContext* opCtx) { @@ -461,19 +397,11 @@ bool Pipeline::needsMongosMerger() const { }); } -bool Pipeline::needsAllShardServers() const { - return std::any_of(_sources.begin(), _sources.end(), [&](const auto& stage) { - return stage->constraints().resolvedHostTypeRequirement(pCtx) == - HostTypeRequirement::kAllShardServers; - }); -} - bool Pipeline::needsShard() const { return std::any_of(_sources.begin(), _sources.end(), [&](const auto& stage) { auto hostType = stage->constraints().resolvedHostTypeRequirement(pCtx); return (hostType == HostTypeRequirement::kAnyShard || - hostType == HostTypeRequirement::kPrimaryShard || - hostType == HostTypeRequirement::kAllShardServers); + hostType == HostTypeRequirement::kPrimaryShard); }); } @@ -519,20 +447,20 @@ stdx::unordered_set<NamespaceString> Pipeline::getInvolvedCollections() const { } vector<Value> Pipeline::serializeContainer(const SourceContainer& container, - boost::optional<const SerializationOptions&> opts) { + boost::optional<ExplainOptions::Verbosity> explain) { vector<Value> serializedSources; for (auto&& source : container) { - source->serializeToArray(serializedSources, opts ? opts.get() : SerializationOptions()); + source->serializeToArray(serializedSources, explain); } return serializedSources; } - -vector<Value> Pipeline::serialize(boost::optional<const SerializationOptions&> opts) const { - return serializeContainer(_sources, opts); +vector<Value> Pipeline::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + return serializeContainer(_sources, explain); } -vector<BSONObj> Pipeline::serializeToBson(boost::optional<const SerializationOptions&> opts) const { - const auto serialized = serialize(opts); +vector<BSONObj> Pipeline::serializeToBson( + boost::optional<ExplainOptions::Verbosity> explain) const { + const auto serialized = serialize(explain); std::vector<BSONObj> asBson; asBson.reserve(serialized.size()); for (auto&& stage : serialized) { @@ -573,16 +501,16 @@ boost::optional<Document> Pipeline::getNext() { : boost::optional<Document>{nextResult.releaseDocument()}; } -vector<Value> Pipeline::writeExplainOps(const SerializationOptions& opts) const { +vector<Value> Pipeline::writeExplainOps(ExplainOptions::Verbosity verbosity) const { vector<Value> array; for (auto&& stage : _sources) { auto beforeSize = array.size(); - stage->serializeToArray(array, opts); + stage->serializeToArray(array, verbosity); auto afterSize = array.size(); // Append execution stats to the serialized stage if the specified verbosity is // 'executionStats' or 'allPlansExecution'. invariant(afterSize - beforeSize == 1u); - if (*opts.verbosity >= ExplainOptions::Verbosity::kExecStats) { + if (verbosity >= ExplainOptions::Verbosity::kExecStats) { auto serializedStage = array.back(); array.back() = appendCommonExecStats(serializedStage, stage->getCommonStats()); } @@ -673,8 +601,7 @@ Status Pipeline::_pipelineCanRunOnMongoS() const { auto hostRequirement = constraints.resolvedHostTypeRequirement(pCtx); const bool needsShard = (hostRequirement == HostTypeRequirement::kAnyShard || - hostRequirement == HostTypeRequirement::kPrimaryShard || - hostRequirement == HostTypeRequirement::kAllShardServers); + hostRequirement == HostTypeRequirement::kPrimaryShard); const bool mustWriteToDisk = (constraints.diskRequirement == DiskUseRequirement::kWritesPersistentData); @@ -761,31 +688,17 @@ boost::intrusive_ptr<DocumentSource> Pipeline::popFrontWithNameAndCriteria( return popFront(); } -void Pipeline::appendPipeline(std::unique_ptr<Pipeline, PipelineDeleter> otherPipeline) { - auto& otherPipelineSources = otherPipeline->getSources(); - while (!otherPipelineSources.empty()) { - _sources.push_back(std::move(otherPipelineSources.front())); - otherPipelineSources.pop_front(); - } - constexpr bool alreadyOptimized = false; - validateCommon(alreadyOptimized); - stitch(); -} - - std::unique_ptr<Pipeline, PipelineDeleter> Pipeline::makePipeline( const std::vector<BSONObj>& rawPipeline, const boost::intrusive_ptr<ExpressionContext>& expCtx, const MakePipelineOptions opts) { auto pipeline = Pipeline::parse(rawPipeline, expCtx, opts.validator); - bool alreadyOptimized = opts.alreadyOptimized; - if (opts.optimize) { pipeline->optimizePipeline(); - alreadyOptimized = true; } + constexpr bool alreadyOptimized = true; pipeline->validateCommon(alreadyOptimized); if (opts.attachCursorSource) { @@ -802,7 +715,6 @@ Pipeline::SourceContainer::iterator Pipeline::optimizeEndOfPipeline( // optimize, since otherwise calls to optimizeAt() will overrun these limits. auto endOfPipeline = Pipeline::SourceContainer(std::next(itr), container->end()); Pipeline::optimizeContainer(&endOfPipeline); - Pipeline::optimizeEachStage(&endOfPipeline); container->erase(std::next(itr), container->end()); container->splice(std::next(itr), endOfPipeline); diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h index 367b4382941..96cf6426be3 100644 --- a/src/mongo/db/pipeline/pipeline.h +++ b/src/mongo/db/pipeline/pipeline.h @@ -45,6 +45,7 @@ #include "mongo/db/query/explain_options.h" #include "mongo/db/query/query_knobs_gen.h" #include "mongo/executor/task_executor.h" +#include "mongo/s/query/async_results_merger_params_gen.h" #include "mongo/util/intrusive_counter.h" #include "mongo/util/timer.h" @@ -69,10 +70,6 @@ using PipelineValidatorCallback = std::function<void(const Pipeline&)>; struct MakePipelineOptions { bool optimize = true; - // It is assumed that the pipeline has already been optimized when we create the - // MakePipelineOptions. If this is not the case, the caller is responsible for setting - // alreadyOptimized to false. - bool alreadyOptimized = true; bool attachCursorSource = true; ShardTargetingPolicy shardTargetingPolicy = ShardTargetingPolicy::kAllowed; PipelineValidatorCallback validator = nullptr; @@ -204,12 +201,6 @@ public: void reattachToOperationContext(OperationContext* opCtx); /** - * Recursively validate the operation contexts associated with this pipeline. Return true if - * all document sources and subpipelines point to the given operation context. - */ - bool validateOperationContext(const OperationContext* opCtx) const; - - /** * Releases any resources held by this pipeline such as PlanExecutors or in-memory structures. * Must be called before deleting a Pipeline. * @@ -262,11 +253,6 @@ public: bool needsShard() const; /** - * Returns 'true' if any stage in the pipeline requires being run on all shards. - */ - bool needsAllShardServers() const; - - /** * Returns true if the pipeline can run on mongoS, but is not obliged to; that is, it can run * either on mongoS or on a shard. */ @@ -284,13 +270,7 @@ public: void optimizePipeline(); /** - * Modifies the container, optimizes each stage individually. - */ - static void optimizeEachStage(SourceContainer* container); - - /** - * Modifies the container, optimizing it by combining, swapping, dropping and/or inserting - * stages. + * Modifies the container, optimizing it by combining and swapping stages. */ static void optimizeContainer(SourceContainer* container); @@ -305,12 +285,11 @@ public: * Helpers to serialize a pipeline. */ std::vector<Value> serialize( - boost::optional<const SerializationOptions&> opts = boost::none) const; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const; std::vector<BSONObj> serializeToBson( - boost::optional<const SerializationOptions&> opts = boost::none) const; + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const; static std::vector<Value> serializeContainer( - const SourceContainer& container, - boost::optional<const SerializationOptions&> opts = boost::none); + const SourceContainer& container, boost::optional<ExplainOptions::Verbosity> = boost::none); /** * Serializes the pipeline into BSON for explain/debug logging purposes. @@ -331,8 +310,7 @@ public: * Write the pipeline's operators to a std::vector<Value>, providing the level of detail * specified by 'verbosity'. */ - std::vector<Value> writeExplainOps( - const SerializationOptions& opts = SerializationOptions{}) const; + std::vector<Value> writeExplainOps(ExplainOptions::Verbosity verbosity) const; /** * Returns the dependencies needed by this pipeline. 'unavailableMetadata' should reflect what @@ -404,12 +382,6 @@ public: StringData targetStageName, std::function<bool(const DocumentSource* const)> predicate); /** - * Appends another pipeline to the existing pipeline. - * NOTE: The other pipeline will be destroyed. - */ - void appendPipeline(std::unique_ptr<Pipeline, PipelineDeleter> otherPipeline); - - /** * Performs common validation for top-level or facet pipelines. Throws if the pipeline is * invalid. * @@ -474,12 +446,6 @@ private: */ Status _pipelineCanRunOnMongoS() const; - /** - * Asserts whether operation contexts associated with this pipeline are consistent across - * sources. - */ - void checkValidOperationContext() const; - SourceContainer _sources; SplitState _splitState = SplitState::kUnsplit; diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp index f35c91523eb..b21e8635c51 100644 --- a/src/mongo/db/pipeline/pipeline_d.cpp +++ b/src/mongo/db/pipeline/pipeline_d.cpp @@ -42,6 +42,7 @@ #include "mongo/db/catalog/database.h" #include "mongo/db/catalog/index_catalog.h" #include "mongo/db/concurrency/d_concurrency.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/exec/cached_plan.h" #include "mongo/db/exec/collection_scan.h" @@ -641,7 +642,9 @@ StatusWith<unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::createRan std::move(ws), std::move(root), &coll, - PlanYieldPolicy::YieldPolicy::YIELD_AUTO, + opCtx->inMultiDocumentTransaction() + ? PlanYieldPolicy::YieldPolicy::INTERRUPT_ONLY + : PlanYieldPolicy::YieldPolicy::YIELD_AUTO, QueryPlannerParams::RETURN_OWNED_DATA); if (!execStatus.isOK()) { return execStatus.getStatus(); @@ -799,10 +802,10 @@ namespace { * the case of a $sort with a non-null value for getLimitSrc(), indicating that there was previously * a $limit stage that was optimized away. */ -std::pair<boost::intrusive_ptr<DocumentSourceSort>, boost::intrusive_ptr<DocumentSourceGroupBase>> +std::pair<boost::intrusive_ptr<DocumentSourceSort>, boost::intrusive_ptr<DocumentSourceGroup>> getSortAndGroupStagesFromPipeline(const Pipeline::SourceContainer& sources) { boost::intrusive_ptr<DocumentSourceSort> sortStage = nullptr; - boost::intrusive_ptr<DocumentSourceGroupBase> groupStage = nullptr; + boost::intrusive_ptr<DocumentSourceGroup> groupStage = nullptr; auto sourcesIt = sources.begin(); if (sourcesIt != sources.end()) { @@ -818,7 +821,7 @@ getSortAndGroupStagesFromPipeline(const Pipeline::SourceContainer& sources) { } if (sourcesIt != sources.end()) { - groupStage = dynamic_cast<DocumentSourceGroupBase*>(sourcesIt->get()); + groupStage = dynamic_cast<DocumentSourceGroup*>(sourcesIt->get()); } return std::make_pair(sortStage, groupStage); @@ -926,10 +929,8 @@ PipelineD::supportsSort(const BucketUnpacker& bucketUnpacker, const CollectionScan* scan = static_cast<CollectionScan*>(root); if (sort.size() == 1) { auto part = sort[0]; - // Check the sort we're asking for is on time, and that the buckets are actually - // ordered on time. - if (part.fieldPath && *part.fieldPath == bucketUnpacker.getTimeField() && - !bucketUnpacker.bucketSpec().usesExtendedRange()) { + // Check the sort we're asking for is on time. + if (part.fieldPath && *part.fieldPath == bucketUnpacker.getTimeField()) { // Check that the directions agree. if ((scan->getDirection() == CollectionScanParams::Direction::FORWARD) == part.isAscending) @@ -1036,15 +1037,6 @@ PipelineD::supportsSort(const BucketUnpacker& bucketUnpacker, if (ixField != controlMinTime && ixField != controlMaxTime) return boost::none; - // If we've inserted a date before 1-1-1970, we round the min up towards 1970, - // rather then down, which has the effect of increasing the control.min.t. - // This means the minimum time in the bucket is likely to be lower than - // indicated and thus, actual dates may be out of order relative to what's - // indicated by the bucket bounds. - if (ixField == controlMinTime && - bucketUnpacker.bucketSpec().usesExtendedRange()) - return boost::none; - if (!directionCompatible(*keyPatternIter, *sortIter)) return boost::none; @@ -1216,7 +1208,12 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll // sort optimization. We check eligibility and perform the rewrite here. auto [unpack, sort] = findUnpackThenSort(pipeline->_sources); QueryPlannerParams plannerOpts; - if (feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV() && unpack && sort) { + if (serverGlobalParams.featureCompatibility.isVersionInitialized() && + serverGlobalParams.featureCompatibility.isGreaterThanOrEqualTo( + multiversion::FeatureCompatibilityVersion::kVersion_6_0) && + feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabled( + serverGlobalParams.featureCompatibility) && + unpack && sort) { plannerOpts.traversalPreference = createTimeSeriesTraversalPreference(unpack, sort); } @@ -1238,7 +1235,12 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll // If this is a query on a time-series collection then it may be eligible for a post-planning // sort optimization. We check eligibility and perform the rewrite here. - if (feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV() && unpack && sort) { + if (serverGlobalParams.featureCompatibility.isVersionInitialized() && + serverGlobalParams.featureCompatibility.isGreaterThanOrEqualTo( + multiversion::FeatureCompatibilityVersion::kVersion_6_0) && + feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabled( + serverGlobalParams.featureCompatibility) && + unpack && sort) { auto execImpl = dynamic_cast<PlanExecutorImpl*>(exec.get()); if (execImpl) { @@ -1304,7 +1306,7 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll dynamic_cast<const DocumentSourceInternalUnpackBucket*>( iter->get())) { unpackIter = iter; - tassert(6505001, + uassert(6505001, str::stream() << "Expected at most one " << DocumentSourceInternalUnpackBucket::kStageNameInternal @@ -1667,7 +1669,7 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep // will handle the sort, and the groupTransform (added below) will handle the $group // stage. pipeline->popFrontWithName(DocumentSourceSort::kStageName); - pipeline->popFrontWithName(rewrittenGroupStage->originalStageName()); + pipeline->popFrontWithName(DocumentSourceGroup::kStageName); boost::intrusive_ptr<DocumentSource> groupTransform( new DocumentSourceSingleDocumentTransformation( diff --git a/src/mongo/db/pipeline/pipeline_d.h b/src/mongo/db/pipeline/pipeline_d.h index c0bbc43fa5c..c109e75b1b8 100644 --- a/src/mongo/db/pipeline/pipeline_d.h +++ b/src/mongo/db/pipeline/pipeline_d.h @@ -146,21 +146,11 @@ public: BSONObj userCollation, const CollectionPtr& collection) { if (!collection || !collection->getDefaultCollator()) { - if (userCollation.isEmpty()) { - return {nullptr, ExpressionContext::CollationMatchesDefault::kYes}; - } else { - auto userCollator = - uassertStatusOK(CollatorFactoryInterface::get(opCtx->getServiceContext()) - ->makeFromBSON(userCollation)); - return {std::move(userCollator), - // If the user explicitly provided a simple collation, we can still treat it - // as 'CollationMatchesDefault::kYes', as no collation and simple collation - // are functionally equivalent in the query code. - (SimpleBSONObjComparator::kInstance.evaluate(userCollation == - CollationSpec::kSimpleSpec)) - ? ExpressionContext::CollationMatchesDefault::kYes - : ExpressionContext::CollationMatchesDefault::kNo}; - } + return {userCollation.isEmpty() + ? nullptr + : uassertStatusOK(CollatorFactoryInterface::get(opCtx->getServiceContext()) + ->makeFromBSON(userCollation)), + ExpressionContext::CollationMatchesDefault::kNoDefault}; } if (userCollation.isEmpty()) { return {collection->getDefaultCollator()->clone(), @@ -168,11 +158,11 @@ public: } auto userCollator = uassertStatusOK( CollatorFactoryInterface::get(opCtx->getServiceContext())->makeFromBSON(userCollation)); - auto userCollatorPtr = userCollator.get(); - return {std::move(userCollator), - CollatorInterface::collatorsMatch(collection->getDefaultCollator(), userCollatorPtr) - ? ExpressionContext::CollationMatchesDefault::kYes - : ExpressionContext::CollationMatchesDefault::kNo}; + return { + std::move(userCollator), + CollatorInterface::collatorsMatch(collection->getDefaultCollator(), userCollator.get()) + ? ExpressionContext::CollationMatchesDefault::kYes + : ExpressionContext::CollationMatchesDefault::kNo}; } private: diff --git a/src/mongo/db/pipeline/pipeline_metadata_tree_test.cpp b/src/mongo/db/pipeline/pipeline_metadata_tree_test.cpp index b34a1cfc2c3..8b8f6597596 100644 --- a/src/mongo/db/pipeline/pipeline_metadata_tree_test.cpp +++ b/src/mongo/db/pipeline/pipeline_metadata_tree_test.cpp @@ -58,6 +58,16 @@ #include "mongo/unittest/temp_dir.h" #include "mongo/unittest/unittest.h" +#define ASSERT_DOES_NOT_THROW(EXPRESSION) \ + try { \ + EXPRESSION; \ + } catch (const AssertionException& e) { \ + ::mongo::str::stream err; \ + err << "Threw an exception incorrectly: " << e.toString() \ + << " Exception occured in: " << #EXPRESSION; \ + ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, err).stream(); \ + } + namespace mongo { namespace { diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp index d36681531fe..1854c07a97a 100644 --- a/src/mongo/db/pipeline/pipeline_test.cpp +++ b/src/mongo/db/pipeline/pipeline_test.cpp @@ -42,9 +42,6 @@ #include "mongo/db/pipeline/document_source_change_stream.h" #include "mongo/db/pipeline/document_source_change_stream_add_post_image.h" #include "mongo/db/pipeline/document_source_change_stream_add_pre_image.h" -#include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" -#include "mongo/db/pipeline/document_source_change_stream_gen.h" -#include "mongo/db/pipeline/document_source_change_stream_handle_topology_change.h" #include "mongo/db/pipeline/document_source_facet.h" #include "mongo/db/pipeline/document_source_graph_lookup.h" #include "mongo/db/pipeline/document_source_internal_split_pipeline.h" @@ -53,7 +50,6 @@ #include "mongo/db/pipeline/document_source_mock.h" #include "mongo/db/pipeline/document_source_out.h" #include "mongo/db/pipeline/document_source_project.h" -#include "mongo/db/pipeline/document_source_redact.h" #include "mongo/db/pipeline/document_source_sort.h" #include "mongo/db/pipeline/document_source_test_optimizations.h" #include "mongo/db/pipeline/expression_context_for_test.h" @@ -79,9 +75,6 @@ using std::string; using std::vector; const NamespaceString kTestNss = NamespaceString("a.collection"); -const NamespaceString kAdminCollectionlessNss = NamespaceString("admin.$cmd.aggregate"); -const auto kExplain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; constexpr size_t getChangeStreamStageSize() { return 6; @@ -93,25 +86,6 @@ void setMockReplicationCoordinatorOnOpCtx(OperationContext* opCtx) { std::make_unique<repl::ReplicationCoordinatorMock>(opCtx->getServiceContext())); } -DocumentSource* getStageAtPos(const Pipeline::SourceContainer& stages, int pos) { - if (pos >= 0) { - auto it = stages.begin(); - std::advance(it, pos); - return (*it).get(); - } else { - auto it = stages.rbegin(); - std::advance( - it, - -pos - 1); // Subtract 1 because rbegin() points to the element before the last one. - return (*it).get(); - } -} - -template <typename T> -void assertStageAtPos(const Pipeline::SourceContainer& stages, int pos) { - ASSERT(dynamic_cast<T*>(getStageAtPos(stages, pos))); -} - namespace Optimizations { namespace Local { @@ -125,8 +99,7 @@ class StubExplainInterface : public StubMongoProcessInterface { std::unique_ptr<Pipeline, PipelineDeleter> pipeline( ownedPipeline, PipelineDeleter(ownedPipeline->getContext()->opCtx)); BSONArrayBuilder bab; - auto opts = SerializationOptions{boost::make_optional(verbosity)}; - auto pipelineVec = pipeline->writeExplainOps(opts); + auto pipelineVec = pipeline->writeExplainOps(verbosity); for (auto&& stage : pipelineVec) { bab << stage; } @@ -141,8 +114,7 @@ class StubExplainInterface : public StubMongoProcessInterface { }; void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson, std::string outputPipeJson, - std::string serializedPipeJson, - NamespaceString aggNss = kTestNss) { + std::string serializedPipeJson) { QueryTestServiceContext testServiceContext; auto opCtx = testServiceContext.makeOperationContext(); @@ -156,7 +128,7 @@ void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson, ASSERT_EQUALS(stageElem.type(), BSONType::Object); rawPipeline.push_back(stageElem.embeddedObject()); } - AggregateCommandRequest request(aggNss, rawPipeline); + AggregateCommandRequest request(kTestNss, rawPipeline); intrusive_ptr<ExpressionContextForTest> ctx = new ExpressionContextForTest(opCtx.get(), request); ctx->mongoProcessInterface = std::make_shared<StubExplainInterface>(); @@ -173,8 +145,7 @@ void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson, auto outputPipe = Pipeline::parse(request.getPipeline(), ctx); outputPipe->optimizePipeline(); - ASSERT_VALUE_EQ(Value(outputPipe->writeExplainOps(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})), + ASSERT_VALUE_EQ(Value(outputPipe->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner)), Value(outputPipeExpected["pipeline"])); ASSERT_VALUE_EQ(Value(outputPipe->serialize()), Value(serializePipeExpected["pipeline"])); } @@ -522,22 +493,6 @@ TEST(PipelineOptimizationTest, SortDoesNotSwapBeforeUnwindBecauseUnwindPathEqual assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); } -TEST(PipelineOptimizationTest, SortDoesNotSwapBeforeUnwindBecauseArrayIndexField) { - std::string inputPipe = - "[{$unwind : {path: '$a', includeArrayIndex: 'i'}}" - ",{$sort : {i: 1}}" - "]"; - std::string outputPipe = - "[{$unwind : {path: '$a', includeArrayIndex: 'i'}}" - ",{$sort : {sortKey: {i: 1}}}" - "]"; - std::string serializedPipe = - "[{$unwind : {path: '$a', includeArrayIndex: 'i'}}" - ",{$sort : {i: 1}}" - "]"; - assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); -} - TEST(PipelineOptimizationTest, LookupShouldCoalesceWithUnwindOnAsSortDoesNotInterfere) { string inputPipe = "[{$lookup: {from : 'lookupColl', as : 'same', localField: 'left', foreignField: " @@ -2684,164 +2639,125 @@ TEST(PipelineOptimizationTest, MatchOnFmodShouldSwapWithAdjacentStage) { assertPipelineOptimizesTo(inputPipe, outputPipe); } -class ChangeStreamPipelineOptimizationTest : public ServiceContextTest { -public: - ChangeStreamPipelineOptimizationTest() - : ChangeStreamPipelineOptimizationTest(false /* inMongos */) {} - - ChangeStreamPipelineOptimizationTest(bool inMongos) { - _opCtx = _testServiceContext.makeOperationContext(); - _expCtx = make_intrusive<ExpressionContextForTest>(_opCtx.get(), kTestNss); - _expCtx->opCtx = _opCtx.get(); - _expCtx->uuid = UUID::gen(); - _expCtx->inMongos = inMongos; - setMockReplicationCoordinatorOnOpCtx(_expCtx->opCtx); - } - BSONObj changestreamStage(const std::string& stageStr) { - return fromjson("{$changeStream: " + stageStr + "}"); - } - BSONObj matchStage(const std::string& stageStr) { - return fromjson("{$match: " + stageStr + "}"); - } - BSONObj redactStage(const std::string& stageStr) { - return fromjson("{$redact: " + stageStr + "}"); - } - std::unique_ptr<Pipeline, PipelineDeleter> makePipeline( - const std::vector<BSONObj>& rawPipeline) { - auto pipeline = Pipeline::parse(rawPipeline, _expCtx); - return pipeline; - } - - static std::string generateEventResumeToken() { - ResumeTokenData resumeTokenDataIn{Timestamp{1001, 3}, - ResumeTokenData::kDefaultTokenVersion, - 0, - UUID::gen(), - Value(Document{{"operationType", "drop"_sd}})}; - return ResumeToken(resumeTokenDataIn).toBSON().toString(); - } - -private: - QueryTestServiceContext _testServiceContext; - ServiceContext::UniqueOperationContext _opCtx; - boost::intrusive_ptr<ExpressionContextForTest> _expCtx; -}; +TEST(PipelineOptimizationTest, ChangeStreamLookupSwapsWithIndependentMatch) { + QueryTestServiceContext testServiceContext; + auto opCtx = testServiceContext.makeOperationContext(); -TEST_F(ChangeStreamPipelineOptimizationTest, ChangeStreamLookUpSize) { - auto pipeline = makePipeline( - {changestreamStage("{fullDocument: 'updateLookup', showExpandedEvents: true}")}); - ASSERT_EQ(pipeline->getSources().size(), getChangeStreamStageSize()); - // Make sure the change lookup is at the end. - assertStageAtPos<DocumentSourceChangeStreamAddPostImage>(pipeline->getSources(), -1 /* pos */); -} + intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest(kTestNss)); + expCtx->opCtx = opCtx.get(); + expCtx->uuid = UUID::gen(); + setMockReplicationCoordinatorOnOpCtx(expCtx->opCtx); -TEST_F(ChangeStreamPipelineOptimizationTest, ChangeStreamLookupSwapsWithIndependentMatch) { // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which // filters out newly added events. - auto pipeline = - makePipeline({changestreamStage("{fullDocument: 'updateLookup', showExpandedEvents: true}"), - matchStage("{extra: 'predicate'}")}); + auto spec = BSON("$changeStream" << BSON( + "fullDocument" + << "updateLookup" + << DocumentSourceChangeStreamSpec::kShowExpandedEventsFieldName << true)); + auto stages = DocumentSourceChangeStream::createFromBson(spec.firstElement(), expCtx); + ASSERT_EQ(stages.size(), getChangeStreamStageSize()); + // Make sure the change lookup is at the end. + ASSERT(dynamic_cast<DocumentSourceChangeStreamAddPostImage*>(stages.back().get())); + + auto matchPredicate = BSON("extra" + << "predicate"); + stages.push_back(DocumentSourceMatch::create(matchPredicate, expCtx)); + auto pipeline = Pipeline::create(stages, expCtx); pipeline->optimizePipeline(); + // Make sure the $match stage has swapped before the change look up. - assertStageAtPos<DocumentSourceChangeStreamAddPostImage>(pipeline->getSources(), -1 /* pos */); + ASSERT( + dynamic_cast<DocumentSourceChangeStreamAddPostImage*>(pipeline->getSources().back().get())); } -TEST_F(ChangeStreamPipelineOptimizationTest, ChangeStreamLookupDoesNotSwapWithMatchOnPostImage) { - // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which - // filters out newly added eve - auto pipeline = - makePipeline({changestreamStage("{fullDocument: 'updateLookup', showExpandedEvents: true}"), - matchStage("{fullDocument: null}")}); - pipeline->optimizePipeline(); - // Make sure the $match stage stays at the end. - assertStageAtPos<DocumentSourceMatch>(pipeline->getSources(), -1 /* pos */); -} +TEST(PipelineOptimizationTest, ChangeStreamLookupDoesNotSwapWithMatchOnPostImage) { + QueryTestServiceContext testServiceContext; + auto opCtx = testServiceContext.makeOperationContext(); -TEST_F(ChangeStreamPipelineOptimizationTest, FullDocumentBeforeChangeLookupSize) { - // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which - // filters out newly added events. - auto pipeline = makePipeline( - {changestreamStage("{fullDocumentBeforeChange: 'required', showExpandedEvents: true}")}); - ASSERT_EQ(pipeline->getSources().size(), getChangeStreamStageSize()); - // Make sure the pre-image lookup is at the end. - assertStageAtPos<DocumentSourceChangeStreamAddPreImage>(pipeline->getSources(), -1 /* pos */); -} + intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest(kTestNss)); + expCtx->opCtx = opCtx.get(); + expCtx->uuid = UUID::gen(); + setMockReplicationCoordinatorOnOpCtx(expCtx->opCtx); -TEST_F(ChangeStreamPipelineOptimizationTest, - FullDocumentBeforeChangeLookupSwapsWithIndependentMatch) { // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which // filters out newly added events. - auto pipeline = makePipeline( - {changestreamStage("{fullDocumentBeforeChange: 'required', showExpandedEvents: true}"), - matchStage("{extra: 'predicate'}")}); - pipeline->optimizePipeline(); - // Make sure the $match stage has swapped before the change look up. - assertStageAtPos<DocumentSourceChangeStreamAddPreImage>(pipeline->getSources(), -1 /* pos */); -} + auto spec = BSON("$changeStream" << BSON( + "fullDocument" + << "updateLookup" + << DocumentSourceChangeStreamSpec::kShowExpandedEventsFieldName << true)); + auto stages = DocumentSourceChangeStream::createFromBson(spec.firstElement(), expCtx); + ASSERT_EQ(stages.size(), getChangeStreamStageSize()); + // Make sure the change lookup is at the end. + ASSERT(dynamic_cast<DocumentSourceChangeStreamAddPostImage*>(stages.back().get())); -TEST_F(ChangeStreamPipelineOptimizationTest, - FullDocumentBeforeChangeDoesNotSwapWithMatchOnPreImage) { - // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which - // filters out newly added events. - auto pipeline = makePipeline( - {changestreamStage("{fullDocumentBeforeChange: 'required', showExpandedEvents: true}"), - matchStage("{fullDocumentBeforeChange: null}")}); + stages.push_back(DocumentSourceMatch::create( + BSON(DocumentSourceChangeStreamAddPostImage::kFullDocumentFieldName << BSONNULL), expCtx)); + auto pipeline = Pipeline::create(stages, expCtx); pipeline->optimizePipeline(); + // Make sure the $match stage stays at the end. - assertStageAtPos<DocumentSourceMatch>(pipeline->getSources(), -1 /* pos */); + ASSERT(dynamic_cast<DocumentSourceMatch*>(pipeline->getSources().back().get())); } -TEST_F(ChangeStreamPipelineOptimizationTest, - ChangeStreamEnsureResumeTokenSwapsWithJsonSchemaMatch) { - auto pipeline = makePipeline( - {changestreamStage("{resumeAfter: " + generateEventResumeToken() + "}"), - matchStage( - "{$jsonSchema: {properties: {documentKey: {properties: {_id: {enum: [1, 2]}}}}}}")}); +TEST(PipelineOptimizationTest, FullDocumentBeforeChangeLookupSwapsWithIndependentMatch) { + QueryTestServiceContext testServiceContext; + auto opCtx = testServiceContext.makeOperationContext(); - // Assert $match is the last stage before optimization. - assertStageAtPos<DocumentSourceMatch>(pipeline->getSources(), -1); + intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest(kTestNss)); + expCtx->opCtx = opCtx.get(); + expCtx->uuid = UUID::gen(); + setMockReplicationCoordinatorOnOpCtx(expCtx->opCtx); + // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which + // filters out newly added events. + auto spec = BSON("$changeStream" << BSON( + "fullDocumentBeforeChange" + << "required" + << DocumentSourceChangeStreamSpec::kShowExpandedEventsFieldName << true)); + auto stages = DocumentSourceChangeStream::createFromBson(spec.firstElement(), expCtx); + ASSERT_EQ(stages.size(), getChangeStreamStageSize()); + // Make sure the pre-image lookup is at the end. + ASSERT(dynamic_cast<DocumentSourceChangeStreamAddPreImage*>(stages.back().get())); + + auto matchPredicate = BSON("extra" + << "predicate"); + stages.push_back(DocumentSourceMatch::create(matchPredicate, expCtx)); + auto pipeline = Pipeline::create(stages, expCtx); pipeline->optimizePipeline(); - // Assert that $match swaps with $_internalChangeStreamHandleTopologyChange after optimization. - assertStageAtPos<DocumentSourceMatch>(pipeline->getSources(), -2); - assertStageAtPos<DocumentSourceChangeStreamEnsureResumeTokenPresent>(pipeline->getSources(), - -1); + // Make sure the $match stage has swapped before the change look up. + ASSERT( + dynamic_cast<DocumentSourceChangeStreamAddPreImage*>(pipeline->getSources().back().get())); } -// To enforce the $_internalChangeStreamHandleTopologyChange stage. -class ChangeStreamPipelineOptimizationTestWithMongoS : public ChangeStreamPipelineOptimizationTest { -public: - ChangeStreamPipelineOptimizationTestWithMongoS() - : ChangeStreamPipelineOptimizationTest(true /* inMongos */) {} -}; - -TEST_F(ChangeStreamPipelineOptimizationTestWithMongoS, - ChangeStreamHandleTopologyChangeSwapsWithRedact) { - auto pipeline = - makePipeline({changestreamStage("{showExpandedEvents: true}"), redactStage("'$$PRUNE'")}); - pipeline->optimizePipeline(); - // Assert that $redact swaps with $_internalChangeStreamHandleTopologyChange after optimization. - assertStageAtPos<DocumentSourceRedact>(pipeline->getSources(), -2 /* pos */); - assertStageAtPos<DocumentSourceChangeStreamHandleTopologyChange>(pipeline->getSources(), - -1 /* pos */); -} +TEST(PipelineOptimizationTest, FullDocumentBeforeChangeDoesNotSwapWithMatchOnPreImage) { + QueryTestServiceContext testServiceContext; + auto opCtx = testServiceContext.makeOperationContext(); -TEST_F(ChangeStreamPipelineOptimizationTestWithMongoS, - ChangeStreamHandleTopologyChangeSwapsWithJsonSchemaMatch) { - auto pipeline = makePipeline( - {changestreamStage("{}"), - matchStage( - "{$jsonSchema: {properties: {documentKey: {properties: {_id: {enum: [1, 2]}}}}}}")}); + intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest(kTestNss)); + expCtx->opCtx = opCtx.get(); + expCtx->uuid = UUID::gen(); + setMockReplicationCoordinatorOnOpCtx(expCtx->opCtx); - // Assert $match is the last stage before optimization. - assertStageAtPos<DocumentSourceMatch>(pipeline->getSources(), -1); + // We enable the 'showExpandedEvents' flag to avoid injecting an additional $match stage which + // filters out newly added events. + auto spec = BSON("$changeStream" << BSON( + "fullDocumentBeforeChange" + << "required" + << DocumentSourceChangeStreamSpec::kShowExpandedEventsFieldName << true)); + auto stages = DocumentSourceChangeStream::createFromBson(spec.firstElement(), expCtx); + ASSERT_EQ(stages.size(), getChangeStreamStageSize()); + // Make sure the pre-image lookup is at the end. + ASSERT(dynamic_cast<DocumentSourceChangeStreamAddPreImage*>(stages.back().get())); + stages.push_back(DocumentSourceMatch::create( + BSON(DocumentSourceChangeStreamAddPreImage::kFullDocumentBeforeChangeFieldName << BSONNULL), + expCtx)); + auto pipeline = Pipeline::create(stages, expCtx); pipeline->optimizePipeline(); - // Assert that $match swaps with $_internalChangeStreamHandleTopologyChange after optimization. - assertStageAtPos<DocumentSourceMatch>(pipeline->getSources(), -2); - assertStageAtPos<DocumentSourceChangeStreamHandleTopologyChange>(pipeline->getSources(), -1); + // Make sure the $match stage stays at the end. + ASSERT(dynamic_cast<DocumentSourceMatch*>(pipeline->getSources().back().get())); } TEST(PipelineOptimizationTest, SortLimProjLimBecomesTopKSortProj) { @@ -3065,71 +2981,6 @@ TEST(PipelineOptimizationTest, MatchGetsPushedIntoBothChildrenOfUnion) { "]"); } -TEST(PipelineOptimizationTest, internalAllCollectionStatsAbsorbsMatchOnNs) { - std::string inputPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {ns: 'test.foo', a: 10}}" - "]"; - std::string outputPipe = - "[" - " {$_internalAllCollectionStats: {match: {ns: {$eq: 'test.foo'}}}}," - " {$match: {a: {$eq: 10}}}" - "]"; - std::string serializedPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {ns: {$eq: 'test.foo'}}}," - " {$match: {a: {$eq: 10}}}" - "]"; - assertPipelineOptimizesAndSerializesTo( - inputPipe, outputPipe, serializedPipe, kAdminCollectionlessNss); -} - -TEST(PipelineOptimizationTest, internalAllCollectionStatsAbsorbsSeveralMatchesOnNs) { - std::string inputPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {ns: {$gt: 0}}}," - " {$match: {a: 10}}," - " {$match: {ns: {$ne: 5}}}" - "]"; - std::string outputPipe = - "[" - " {$_internalAllCollectionStats: {match: {$and: [{ns: {$gt: 0}}, {ns: {$not: {$eq: " - "5}}}]}}}," - " {$match: {a: {$eq: 10}}}" - "]"; - std::string serializedPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {$and: [{ns: {$gt: 0}}, {ns: {$not: {$eq: 5}}}]}}," - " {$match: {a: {$eq: 10}}}" - "]"; - assertPipelineOptimizesAndSerializesTo( - inputPipe, outputPipe, serializedPipe, kAdminCollectionlessNss); -} - -TEST(PipelineOptimizationTest, internalAllCollectionStatsDoesNotAbsorbMatchNotOnNs) { - std::string inputPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {a: 10}}" - "]"; - std::string outputPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {a: {$eq: 10}}}" - "]"; - std::string serializedPipe = - "[" - " {$_internalAllCollectionStats: {}}," - " {$match: {a: 10}}" - "]"; - assertPipelineOptimizesAndSerializesTo( - inputPipe, outputPipe, serializedPipe, kAdminCollectionlessNss); -} - TEST(PipelineOptimizationTest, ProjectGetsPushedIntoBothChildrenOfUnion) { assertPipelineOptimizesTo( "[" @@ -3244,8 +3095,7 @@ void assertTwoPipelinesOptimizeAndMergeTo(const std::string inputPipe1, } pipeline1->optimizePipeline(); - ASSERT_VALUE_EQ(Value(pipeline1->writeExplainOps(SerializationOptions{ - boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)})), + ASSERT_VALUE_EQ(Value(pipeline1->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner)), Value(outputBson["pipeline"])); } @@ -3363,11 +3213,12 @@ public: mergePipe->optimizePipeline(); auto splitPipeline = sharded_agg_helpers::splitPipeline(std::move(mergePipe)); - const auto explain = - SerializationOptions{boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner)}; - ASSERT_VALUE_EQ(Value(splitPipeline.shardsPipeline->writeExplainOps(explain)), + + ASSERT_VALUE_EQ(Value(splitPipeline.shardsPipeline->writeExplainOps( + ExplainOptions::Verbosity::kQueryPlanner)), Value(shardPipeExpected["pipeline"])); - ASSERT_VALUE_EQ(Value(splitPipeline.mergePipeline->writeExplainOps(explain)), + ASSERT_VALUE_EQ(Value(splitPipeline.mergePipeline->writeExplainOps( + ExplainOptions::Verbosity::kQueryPlanner)), Value(mergePipeExpected["pipeline"])); shardPipe = std::move(splitPipeline.shardsPipeline); @@ -3910,7 +3761,7 @@ class Out : public ShardMergerBase { return "[]"; } string mergePipeJson() { - return "[{$out: {coll: 'outColl', db: 'a'}}]"; + return "[{$out: {db: 'a', coll: 'outColl'}}]"; } }; @@ -4429,55 +4280,6 @@ TEST_F(PipelineValidateTest, ChangeStreamIsNotValidIfNotFirstStageInFacet) { ASSERT_THROWS_CODE(Pipeline::parse(rawPipeline, ctx), AssertionException, 40600); } -TEST_F(PipelineValidateTest, ChangeStreamSplitLargeEventIsValid) { - const std::vector<BSONObj> rawPipeline = {fromjson("{$changeStream: {}}"), - fromjson("{$changeStreamSplitLargeEvent: {}}")}; - auto ctx = getExpCtx(); - setMockReplicationCoordinatorOnOpCtx(ctx->opCtx); - ctx->ns = NamespaceString("a.collection"); - Pipeline::parse(rawPipeline, ctx); -} - -TEST_F(PipelineValidateTest, ChangeStreamSplitLargeEventIsNotValidWithoutChangeStream) { - const std::vector<BSONObj> rawPipeline = {fromjson("{$changeStreamSplitLargeEvent: {}}")}; - auto ctx = getExpCtx(); - ctx->changeStreamSpec = boost::none; - setMockReplicationCoordinatorOnOpCtx(ctx->opCtx); - ctx->ns = NamespaceString("a.collection"); - ASSERT_THROWS_CODE( - Pipeline::parse(rawPipeline, ctx), DBException, ErrorCodes::IllegalOperation); -} - -TEST_F(PipelineValidateTest, ChangeStreamSplitLargeEventIsNotLastStage) { - const std::vector<BSONObj> rawPipeline = {fromjson("{$changeStream: {}}"), - fromjson("{$changeStreamSplitLargeEvent: {}}"), - fromjson("{$match: {}}")}; - auto ctx = getExpCtx(); - setMockReplicationCoordinatorOnOpCtx(ctx->opCtx); - ctx->ns = NamespaceString("a.collection"); - ASSERT_THROWS_CODE(Pipeline::parse(rawPipeline, ctx), DBException, 7182802); -} - -TEST_F(PipelineValidateTest, ChangeStreamSplitLargeEventIsValidAfterMatch) { - const std::vector<BSONObj> rawPipeline = {fromjson("{$changeStream: {}}"), - fromjson("{$match: {custom: 'filter'}}"), - fromjson("{$changeStreamSplitLargeEvent: {}}")}; - auto ctx = getExpCtx(); - setMockReplicationCoordinatorOnOpCtx(ctx->opCtx); - ctx->ns = NamespaceString("a.collection"); - Pipeline::parse(rawPipeline, ctx); -} - -TEST_F(PipelineValidateTest, ChangeStreamSplitLargeEventIsValidAfterRedact) { - const std::vector<BSONObj> rawPipeline = {fromjson("{$changeStream: {}}"), - fromjson("{$redact: '$$PRUNE'}"), - fromjson("{$changeStreamSplitLargeEvent: {}}")}; - auto ctx = getExpCtx(); - setMockReplicationCoordinatorOnOpCtx(ctx->opCtx); - ctx->ns = NamespaceString("a.collection"); - Pipeline::parse(rawPipeline, ctx); -} - class DocumentSourceDisallowedInTransactions : public DocumentSourceMock { public: DocumentSourceDisallowedInTransactions(const boost::intrusive_ptr<ExpressionContext>& expCtx) @@ -4797,7 +4599,7 @@ public: * Returns a description which communicate that this stage modifies nothing. */ GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet(), {}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>(), {}}; } }; @@ -4874,7 +4676,7 @@ public: * Returns a description which communicate that this stage modifies nothing. */ GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet(), {}}; + return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>(), {}}; } }; @@ -4910,7 +4712,7 @@ public: return new RenamesAToB(expCtx); } GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"b", "a"}}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"b", "a"}}}; } }; @@ -5034,7 +4836,7 @@ public: return new RenamesBToC(expCtx); } GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"c", "b"}}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"c", "b"}}}; } }; @@ -5077,7 +4879,7 @@ public: return new RenamesBToA(expCtx); } GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"a", "b"}}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"a", "b"}}}; } }; diff --git a/src/mongo/db/pipeline/plan_executor_pipeline.cpp b/src/mongo/db/pipeline/plan_executor_pipeline.cpp index 7e39571b589..0b26a7db813 100644 --- a/src/mongo/db/pipeline/plan_executor_pipeline.cpp +++ b/src/mongo/db/pipeline/plan_executor_pipeline.cpp @@ -37,14 +37,8 @@ #include "mongo/db/pipeline/plan_explainer_pipeline.h" #include "mongo/db/pipeline/resume_token.h" #include "mongo/db/repl/speculative_majority_read_info.h" -#include "mongo/util/duration.h" namespace mongo { -namespace { -Counter64 changeStreamsLargeEventsFailedCounter; -ServerStatusMetricField<Counter64> dChangeStreamsLargeEventsFailedCounter( - "changeStreams.largeEventsFailed", &changeStreamsLargeEventsFailedCounter); -} // namespace PlanExecutorPipeline::PlanExecutorPipeline(boost::intrusive_ptr<ExpressionContext> expCtx, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, @@ -84,7 +78,9 @@ PlanExecutor::ExecState PlanExecutorPipeline::getNext(BSONObj* objOut, RecordId* Document docOut; auto execState = getNextDocument(&docOut, nullptr); if (execState == PlanExecutor::ADVANCED) { - *objOut = _trySerializeToBson(docOut); + // Include metadata if the output will be consumed by a merging node. + *objOut = _expCtx->needsMerge || _expCtx->forPerShardCursor ? docOut.toBsonWithMetaData() + : docOut.toBson(); } return execState; } @@ -144,19 +140,6 @@ boost::optional<Document> PlanExecutorPipeline::_tryGetNext() try { return Document::fromBsonWithMetaData(extraInfo->getStartAfterInvalidateEvent()); } -BSONObj PlanExecutorPipeline::_trySerializeToBson(const Document& doc) try { - // Include metadata if the output will be consumed by a merging node. - return _expCtx->needsMerge || _expCtx->forPerShardCursor ? doc.toBsonWithMetaData() - : doc.toBson(); -} catch (const ExceptionFor<ErrorCodes::BSONObjectTooLarge>&) { - // If in a change stream pipeline, increment change stream large event failed error - // count metric. - if (ResumableScanType::kChangeStream == _resumableScanType) { - changeStreamsLargeEventsFailedCounter.increment(); - } - throw; -} - void PlanExecutorPipeline::_updateResumableScanState(const boost::optional<Document>& document) { switch (_resumableScanType) { case ResumableScanType::kChangeStream: @@ -202,8 +185,9 @@ void PlanExecutorPipeline::_performChangeStreamsAccounting(const boost::optional void PlanExecutorPipeline::_validateChangeStreamsResumeToken(const Document& event) const { // Confirm that the document _id field matches the original resume token in the sort key field. + auto eventBSON = event.toBson(); auto resumeToken = event.metadata().getSortKey(); - auto idField = event.getField("_id"); + auto idField = eventBSON.getObjectField("_id"); invariant(!resumeToken.missing()); uassert(ErrorCodes::ChangeStreamFatalError, str::stream() << "Encountered an event whose _id field, which contains the resume " @@ -212,9 +196,9 @@ void PlanExecutorPipeline::_validateChangeStreamsResumeToken(const Document& eve "transformations that retain the unmodified _id field are allowed. " "Expected: " << BSON("_id" << resumeToken) << " but found: " - << (idField.missing() ? BSONObj() : BSON("_id" << idField)), - resumeToken.getType() == BSONType::Object && - ValueComparator::kInstance.evaluate(idField == resumeToken)); + << (eventBSON["_id"] ? BSON("_id" << eventBSON["_id"]) : BSONObj()), + (resumeToken.getType() == BSONType::Object) && + idField.binaryEqual(resumeToken.getDocument().toBson())); } void PlanExecutorPipeline::_performResumableOplogScanAccounting() { diff --git a/src/mongo/db/pipeline/plan_executor_pipeline.h b/src/mongo/db/pipeline/plan_executor_pipeline.h index 577a2d3fe3f..7192de3668f 100644 --- a/src/mongo/db/pipeline/plan_executor_pipeline.h +++ b/src/mongo/db/pipeline/plan_executor_pipeline.h @@ -29,14 +29,12 @@ #pragma once -#include "mongo/util/duration.h" #include <queue> #include "mongo/db/exec/document_value/document.h" #include "mongo/db/pipeline/pipeline.h" #include "mongo/db/pipeline/plan_explainer_pipeline.h" #include "mongo/db/query/plan_executor.h" -#include "mongo/db/query/query_shape/serialization_options.h" namespace mongo { @@ -157,9 +155,7 @@ public: * providing the level of detail specified by 'verbosity'. */ std::vector<Value> writeExplainOps(ExplainOptions::Verbosity verbosity) const { - auto opts = SerializationOptions{}; - opts.verbosity = verbosity; - return _pipeline->writeExplainOps(opts); + return _pipeline->writeExplainOps(verbosity); } void enableSaveRecoveryUnitAcrossCommandsIfSupported() override {} @@ -186,11 +182,6 @@ private: boost::optional<Document> _tryGetNext(); /** - * Serialize the given document to BSON while updating stats for BSONObjectTooLarge exception. - */ - BSONObj _trySerializeToBson(const Document& doc); - - /** * For a change stream or resumable oplog scan, updates the scan state based on the latest * document returned by the underlying pipeline. */ diff --git a/src/mongo/db/pipeline/plan_explainer_pipeline.cpp b/src/mongo/db/pipeline/plan_explainer_pipeline.cpp index 7ac26493a83..7128ef650c8 100644 --- a/src/mongo/db/pipeline/plan_explainer_pipeline.cpp +++ b/src/mongo/db/pipeline/plan_explainer_pipeline.cpp @@ -96,7 +96,9 @@ void PlanExplainerPipeline::getSummaryStats(PlanSummaryStats* statsOut) const { } } - statsOut->nReturned = _nReturned; + if (_nReturned) { + statsOut->nReturned = _nReturned; + } } PlanExplainer::PlanStatsDetails PlanExplainerPipeline::getWinningPlanStats( diff --git a/src/mongo/db/pipeline/plan_explainer_pipeline.h b/src/mongo/db/pipeline/plan_explainer_pipeline.h index e6c1a689a16..ae2dca8239f 100644 --- a/src/mongo/db/pipeline/plan_explainer_pipeline.h +++ b/src/mongo/db/pipeline/plan_explainer_pipeline.h @@ -31,7 +31,6 @@ #include "mongo/db/pipeline/pipeline.h" #include "mongo/db/query/plan_explainer.h" -#include "mongo/util/duration.h" namespace mongo { /** diff --git a/src/mongo/db/pipeline/process_interface/SConscript b/src/mongo/db/pipeline/process_interface/SConscript index dc09fa5d114..44b0afe1591 100644 --- a/src/mongo/db/pipeline/process_interface/SConscript +++ b/src/mongo/db/pipeline/process_interface/SConscript @@ -47,7 +47,6 @@ env.Library( '$BUILD_DIR/mongo/db/catalog/catalog_helpers', '$BUILD_DIR/mongo/db/catalog/database_holder', '$BUILD_DIR/mongo/db/collection_index_usage_tracker', - '$BUILD_DIR/mongo/db/concurrency/exception_util', '$BUILD_DIR/mongo/db/concurrency/flow_control_ticketholder', '$BUILD_DIR/mongo/db/dbhelpers', '$BUILD_DIR/mongo/db/index_builds_coordinator_mongod', @@ -56,7 +55,6 @@ env.Library( '$BUILD_DIR/mongo/db/session_catalog', '$BUILD_DIR/mongo/db/stats/fill_locker_info', '$BUILD_DIR/mongo/db/storage/backup_cursor_hooks', - '$BUILD_DIR/mongo/db/storage/durable_catalog_impl', '$BUILD_DIR/mongo/scripting/scripting_common', ], ) diff --git a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp index 3dfb002c54d..469ce5821aa 100644 --- a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp @@ -37,7 +37,6 @@ #include "mongo/db/auth/authorization_session.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/collection_catalog.h" -#include "mongo/db/catalog/collection_uuid_mismatch.h" #include "mongo/db/catalog/create_collection.h" #include "mongo/db/catalog/database_holder.h" #include "mongo/db/catalog/document_validation.h" @@ -46,7 +45,7 @@ #include "mongo/db/catalog/list_indexes.h" #include "mongo/db/catalog/rename_collection.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/exception_util.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/curop.h" #include "mongo/db/cursor_manager.h" #include "mongo/db/db_raii.h" @@ -201,8 +200,7 @@ std::vector<Document> CommonMongodProcessInterface::getIndexStats(OperationConte auto idxCatalog = collection->getIndexCatalog(); auto idx = idxCatalog->findIndexByName(opCtx, indexName, - IndexCatalog::InclusionPolicy::kReady | - IndexCatalog::InclusionPolicy::kUnfinished); + /* includeUnfinishedIndexes */ true); uassert(ErrorCodes::IndexNotFound, "Could not find entry in IndexCatalog for index " + indexName, idx); @@ -310,33 +308,34 @@ std::deque<BSONObj> CommonMongodProcessInterface::listCatalog(OperationContext* } boost::optional<BSONObj> CommonMongodProcessInterface::getCatalogEntry( - OperationContext* opCtx, - const NamespaceString& ns, - const boost::optional<UUID>& collUUID) const { - - // Perform an AutoGetCollection. This will verify that the collection still exists at the given - // read concern. If it doesn't and the aggregation has specified a UUID then this acquisition - // will fail. - AutoGetCollectionForRead coll{opCtx, ns}; - const auto& collPtr = coll.getCollection(); - checkCollectionUUIDMismatch(opCtx, ns, collPtr, collUUID); + OperationContext* opCtx, const NamespaceString& ns) const { + Lock::GlobalLock globalLock{opCtx, MODE_IS}; - if (!collPtr) { + auto rs = DurableCatalog::get(opCtx)->getRecordStore(); + if (!rs) { return boost::none; } - auto obj = DurableCatalog::get(opCtx)->getCatalogEntry(opCtx, collPtr->getCatalogId()); + auto cursor = rs->getCursor(opCtx); + while (auto record = cursor->next()) { + auto obj = record->data.toBson(); + if (NamespaceString{obj.getStringField("ns")} != ns) { + continue; + } - BSONObjBuilder builder; - builder.append("db", ns.db()); - builder.append("name", ns.coll()); - builder.append("type", "collection"); - if (auto shardName = getShardName(opCtx); !shardName.empty()) { - builder.append("shard", shardName); + BSONObjBuilder builder; + builder.append("db", ns.db()); + builder.append("name", ns.coll()); + builder.append("type", "collection"); + if (auto shardName = getShardName(opCtx); !shardName.empty()) { + builder.append("shard", shardName); + } + builder.appendElements(obj); + + return builder.obj(); } - builder.appendElements(obj); - return builder.obj(); + return boost::none; } void CommonMongodProcessInterface::appendLatencyStats(OperationContext* opCtx, @@ -605,8 +604,7 @@ bool CommonMongodProcessInterface::fieldsHaveSupportingUniqueIndex( return fieldPaths == std::set<FieldPath>{"_id"}; } - auto indexIterator = collection->getIndexCatalog()->getIndexIterator( - opCtx, IndexCatalog::InclusionPolicy::kReady); + auto indexIterator = collection->getIndexCatalog()->getIndexIterator(opCtx, false); while (indexIterator->more()) { const IndexCatalogEntry* entry = indexIterator->next(); if (supportsUniqueKey(expCtx, entry, fieldPaths)) { @@ -747,6 +745,59 @@ CommonMongodProcessInterface::ensureFieldsUniqueOrResolveDocumentKey( return {*fieldPaths, targetCollectionVersion}; } +write_ops::InsertCommandRequest CommonMongodProcessInterface::buildInsertOp( + const NamespaceString& nss, std::vector<BSONObj>&& objs, bool bypassDocValidation) { + write_ops::InsertCommandRequest insertOp(nss); + insertOp.setDocuments(std::move(objs)); + insertOp.setWriteCommandRequestBase([&] { + write_ops::WriteCommandRequestBase wcb; + wcb.setOrdered(false); + wcb.setBypassDocumentValidation(bypassDocValidation); + return wcb; + }()); + return insertOp; +} + +write_ops::UpdateCommandRequest CommonMongodProcessInterface::buildUpdateOp( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& nss, + BatchedObjects&& batch, + UpsertType upsert, + bool multi) { + write_ops::UpdateCommandRequest updateOp(nss); + updateOp.setUpdates([&] { + std::vector<write_ops::UpdateOpEntry> updateEntries; + for (auto&& obj : batch) { + updateEntries.push_back([&] { + write_ops::UpdateOpEntry entry; + auto&& [q, u, c] = obj; + entry.setQ(std::move(q)); + entry.setU(std::move(u)); + entry.setC(std::move(c)); + entry.setUpsert(upsert != UpsertType::kNone); + entry.setUpsertSupplied( + {{entry.getUpsert(), upsert == UpsertType::kInsertSuppliedDoc}}); + entry.setMulti(multi); + return entry; + }()); + } + return updateEntries; + }()); + updateOp.setWriteCommandRequestBase([&] { + write_ops::WriteCommandRequestBase wcb; + wcb.setOrdered(false); + wcb.setBypassDocumentValidation(expCtx->bypassDocumentValidation); + return wcb; + }()); + auto [constants, letParams] = + expCtx->variablesParseState.transitionalCompatibilitySerialize(expCtx->variables); + updateOp.setLegacyRuntimeConstants(std::move(constants)); + if (!letParams.isEmpty()) { + updateOp.setLet(std::move(letParams)); + } + return updateOp; +} + BSONObj CommonMongodProcessInterface::_convertRenameToInternalRename( OperationContext* opCtx, const BSONObj& renameCommandObj, diff --git a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h index 62edd6ca7d9..4a02ecce883 100644 --- a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h @@ -60,8 +60,7 @@ public: std::deque<BSONObj> listCatalog(OperationContext* opCtx) const final; boost::optional<BSONObj> getCatalogEntry(OperationContext* opCtx, - const NamespaceString& ns, - const boost::optional<UUID>& collUUID) const final; + const NamespaceString& ns) const final; void appendLatencyStats(OperationContext* opCtx, const NamespaceString& nss, @@ -143,6 +142,23 @@ protected: const Document& documentKey, MakePipelineOptions opts); + /** + * Builds an ordered insert op on namespace 'nss' and documents to be written 'objs'. + */ + write_ops::InsertCommandRequest buildInsertOp(const NamespaceString& nss, + std::vector<BSONObj>&& objs, + bool bypassDocValidation); + + /** + * Builds an ordered update op on namespace 'nss' with update entries contained in 'batch'. + */ + write_ops::UpdateCommandRequest buildUpdateOp( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& nss, + BatchedObjects&& batch, + UpsertType upsert, + bool multi); + BSONObj _reportCurrentOpForClient(OperationContext* opCtx, Client* client, CurrentOpTruncateMode truncateOps, diff --git a/src/mongo/db/pipeline/process_interface/common_process_interface.h b/src/mongo/db/pipeline/process_interface/common_process_interface.h index 55dc54837b1..513edd5a6f4 100644 --- a/src/mongo/db/pipeline/process_interface/common_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/common_process_interface.h @@ -32,7 +32,6 @@ #include <vector> #include "mongo/bson/bsonobj.h" -#include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/process_interface/mongo_process_interface.h" namespace mongo { @@ -47,70 +46,6 @@ public: virtual ~CommonProcessInterface() = default; /** - * Estimates the size of writes that will be executed on the current node. Note that this - * does not account for the full size of an update statement because in the case of local - * writes, we will not have to serialize to BSON and are therefore not subject to the 16MB - * BSONObj size limit. - */ - class LocalWriteSizeEstimator final : public WriteSizeEstimator { - public: - int estimateInsertHeaderSize( - const write_ops::InsertCommandRequest& insertReq) const override { - return 0; - } - - int estimateUpdateHeaderSize( - const write_ops::UpdateCommandRequest& insertReq) const override { - return 0; - } - - int estimateInsertSizeBytes(const BSONObj& insert) const override { - return insert.objsize(); - } - - int estimateUpdateSizeBytes(const BatchObject& batchObject, - UpsertType type) const override { - int size = std::get<write_ops::UpdateModification>(batchObject).objsize(); - if (auto vars = std::get<boost::optional<BSONObj>>(batchObject)) { - size += vars->objsize(); - } - return size; - } - }; - - /** - * Estimate the size of writes that will be sent to the replica set primary. - */ - class TargetPrimaryWriteSizeEstimator final : public WriteSizeEstimator { - public: - int estimateInsertHeaderSize( - const write_ops::InsertCommandRequest& insertReq) const override { - return write_ops::getInsertHeaderSizeEstimate(insertReq); - } - - int estimateUpdateHeaderSize( - const write_ops::UpdateCommandRequest& updateReq) const override { - return write_ops::getUpdateHeaderSizeEstimate(updateReq); - } - - int estimateInsertSizeBytes(const BSONObj& insert) const override { - return insert.objsize() + write_ops::kWriteCommandBSONArrayPerElementOverheadBytes; - } - - int estimateUpdateSizeBytes(const BatchObject& batchObject, - UpsertType type) const override { - return getUpdateSizeEstimate(std::get<BSONObj>(batchObject), - std::get<write_ops::UpdateModification>(batchObject), - std::get<boost::optional<BSONObj>>(batchObject), - type != UpsertType::kNone /* includeUpsertSupplied */, - boost::none /* collation */, - boost::none /* arrayFilters */, - BSONObj() /* hint*/) + - write_ops::kWriteCommandBSONArrayPerElementOverheadBytes; - } - }; - - /** * Returns true if the field names of 'keyPattern' are exactly those in 'uniqueKeyPaths', and * each of the elements of 'keyPattern' is numeric, i.e. not "text", "$**", or any other special * type of index. @@ -129,7 +64,6 @@ public: virtual std::vector<FieldPath> collectDocumentKeyFieldsActingAsRouter( OperationContext*, const NamespaceString&) const override; - virtual void updateClientOperationTime(OperationContext* opCtx) const final; boost::optional<ChunkVersion> refreshAndGetCollectionVersion( diff --git a/src/mongo/db/pipeline/process_interface/mongo_process_interface.h b/src/mongo/db/pipeline/process_interface/mongo_process_interface.h index 9a980594737..19477adf8c9 100644 --- a/src/mongo/db/pipeline/process_interface/mongo_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/mongo_process_interface.h @@ -78,13 +78,13 @@ class MongoProcessInterface { public: /** * Storage for a batch of BSON Objects to be updated in the write namespace. For each element - * in the batch we store a tuple of the following elements: + * in the batch we store a tuple of the folliwng elements: * 1. BSONObj - specifies the query that identifies a document in the to collection to be * updated. * 2. write_ops::UpdateModification - either the new document we want to upsert or insert into * the collection (i.e. a 'classic' replacement update), or the pipeline to run to compute * the new document. - * 3. boost::optional<BSONObj> - for pipeline-style updates, specifies variables that can be + * 3. boost::optional<BSONObj> - for pipeline-style updated, specifies variables that can be * referred to in the pipeline performing the custom update. */ using BatchObject = @@ -106,30 +106,6 @@ public: enum class CurrentOpBacktraceMode { kIncludeBacktrace, kExcludeBacktrace }; /** - * Interface which estimates the size of a given write operation. - */ - class WriteSizeEstimator { - public: - virtual ~WriteSizeEstimator() = default; - - /** - * Set of functions which estimate the entire size of a write command except for the array - * of write statements themselves. - */ - virtual int estimateInsertHeaderSize( - const write_ops::InsertCommandRequest& insertReq) const = 0; - virtual int estimateUpdateHeaderSize( - const write_ops::UpdateCommandRequest& updateReq) const = 0; - - /** - * Set of functions which estimate the size of a single write statement. - */ - virtual int estimateInsertSizeBytes(const BSONObj& insert) const = 0; - virtual int estimateUpdateSizeBytes(const BatchObject& batchObject, - UpsertType type) const = 0; - }; - - /** * Factory function to create MongoProcessInterface of the right type. The implementation will * be installed by a lib higher up in the link graph depending on the application type. */ @@ -151,12 +127,6 @@ public: virtual ~MongoProcessInterface(){}; /** - * Returns an instance of a 'WriteSizeEstimator' interface. - */ - virtual std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( - OperationContext* opCtx, const NamespaceString& ns) const = 0; - - /** * Creates a new TransactionHistoryIterator object. Only applicable in processes which support * locally traversing the oplog. */ @@ -172,20 +142,6 @@ public: virtual bool isSharded(OperationContext* opCtx, const NamespaceString& ns) = 0; /** - * TODO SERVER-79508 validate callers of this function remain correct. - * - * Returns false if the current request only handles parsing and validating queries. In other - * words, we are not executing queries. Examples include query analysis for queryable - * encryption, executing pipeline-style operations in the Update system, and creating a Query - * Shape. This function only returns false when the process interface is of type - * 'StubMongoProcessInterface'. - * - */ - virtual bool isExpectedToExecuteQueries() { - return true; - } - - /** * Advances the proxied write time associated with the client in ReplClientInfo to * be at least as high as the one tracked by the OperationTimeTracker associated with the * given operation context. @@ -193,30 +149,29 @@ public: virtual void updateClientOperationTime(OperationContext* opCtx) const = 0; /** - * Executes 'insertCommand' against 'ns' and returns an error Status if the insert fails. If - * 'targetEpoch' is set, throws ErrorCodes::StaleEpoch if the targeted collection does not have - * the same epoch or the epoch changes during the course of the insert. + * Inserts 'objs' into 'ns' and returns an error Status if the insert fails. If 'targetEpoch' is + * set, throws ErrorCodes::StaleEpoch if the targeted collection does not have the same epoch or + * the epoch changes during the course of the insert. */ virtual Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + std::vector<BSONObj>&& objs, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) = 0; /** - * Executes the updates described by 'updateCommand'. Returns an error Status if any of the - * updates fail, otherwise returns an 'UpdateResult' objects with the details of the update - * operation. If 'targetEpoch' is set, throws ErrorCodes::StaleEpoch if the targeted collection - * does not have the same epoch, or if the epoch changes during the update. + * Updates the documents matching 'queries' with the objects 'updates'. Returns an error Status + * if any of the updates fail, otherwise returns an 'UpdateResult' objects with the details of + * the update operation. If 'targetEpoch' is set, throws ErrorCodes::StaleEpoch if the targeted + * collection does not have the same epoch, or if the epoch changes during the update. */ - virtual StatusWith<UpdateResult> update( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, - const WriteConcernOptions& wc, - UpsertType upsert, - bool multi, - boost::optional<OID> targetEpoch) = 0; + virtual StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + BatchedObjects&& batch, + const WriteConcernOptions& wc, + UpsertType upsert, + bool multi, + boost::optional<OID> targetEpoch) = 0; /** * Returns index usage statistics for each index on collection 'ns' along with additional @@ -243,10 +198,8 @@ public: /** * Returns the catalog entry for the given namespace, if it exists. */ - virtual boost::optional<BSONObj> getCatalogEntry( - OperationContext* opCtx, - const NamespaceString& ns, - const boost::optional<UUID>& collUUID = boost::none) const = 0; + virtual boost::optional<BSONObj> getCatalogEntry(OperationContext* opCtx, + const NamespaceString& ns) const = 0; /** * Appends operation latency statistics for collection "nss" to "builder" diff --git a/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp b/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp index fb0890f5d61..6f17c7a0121 100644 --- a/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp @@ -99,12 +99,6 @@ bool supportsUniqueKey(const boost::intrusive_ptr<ExpressionContext>& expCtx, } // namespace -std::unique_ptr<MongoProcessInterface::WriteSizeEstimator> -MongosProcessInterface::getWriteSizeEstimator(OperationContext* opCtx, - const NamespaceString& ns) const { - return std::make_unique<TargetPrimaryWriteSizeEstimator>(); -} - std::unique_ptr<Pipeline, PipelineDeleter> MongosProcessInterface::attachCursorSourceToPipeline( Pipeline* ownedPipeline, ShardTargetingPolicy shardTargetingPolicy, @@ -181,15 +175,8 @@ boost::optional<Document> MongosProcessInterface::lookupSingleDocument( // single shard will be targeted here; however, in certain cases where only the _id // is present, we may need to scatter-gather the query to all shards in order to // find the document. - auto requests = - getVersionedRequestsForTargetedShards(expCtx->opCtx, - nss, - cm, - findCmd, - filterObj, - CollationSpec::kSimpleSpec, - boost::none /*letParameters*/, - boost::none /*runtimeConstants*/); + auto requests = getVersionedRequestsForTargetedShards( + expCtx->opCtx, nss, cm, findCmd, filterObj, CollationSpec::kSimpleSpec); // Dispatch the requests. The 'establishCursors' method conveniently prepares the // result into a vector of cursor responses for us. diff --git a/src/mongo/db/pipeline/process_interface/mongos_process_interface.h b/src/mongo/db/pipeline/process_interface/mongos_process_interface.h index c3740948f93..82eedfa6dec 100644 --- a/src/mongo/db/pipeline/process_interface/mongos_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/mongos_process_interface.h @@ -45,9 +45,6 @@ public: virtual ~MongosProcessInterface() = default; - std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( - OperationContext* opCtx, const NamespaceString& ns) const final; - boost::optional<Document> lookupSingleDocument( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& nss, @@ -72,7 +69,7 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + std::vector<BSONObj>&& objs, const WriteConcernOptions& wc, boost::optional<OID>) final { MONGO_UNREACHABLE; @@ -80,7 +77,7 @@ public: StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -106,8 +103,7 @@ public: } boost::optional<BSONObj> getCatalogEntry(OperationContext* opCtx, - const NamespaceString& ns, - const boost::optional<UUID>& collUUID) const final { + const NamespaceString& ns) const final { MONGO_UNREACHABLE; } diff --git a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp index 2a1ce64792a..682c0075340 100644 --- a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp @@ -36,7 +36,7 @@ #include "mongo/db/catalog/list_indexes.h" #include "mongo/db/catalog/rename_collection.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/exception_util.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/index_builds_coordinator.h" #include "mongo/db/pipeline/document_source_cursor.h" @@ -96,13 +96,13 @@ boost::optional<Document> NonShardServerProcessInterface::lookupSingleDocument( return lookedUpDocument; } -Status NonShardServerProcessInterface::insert( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, - const WriteConcernOptions& wc, - boost::optional<OID> targetEpoch) { - auto writeResults = write_ops_exec::performInserts(expCtx->opCtx, *insertCommand); +Status NonShardServerProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::vector<BSONObj>&& objs, + const WriteConcernOptions& wc, + boost::optional<OID> targetEpoch) { + auto writeResults = write_ops_exec::performInserts( + expCtx->opCtx, buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation)); // Need to check each result in the batch since the writes are unordered. for (const auto& result : writeResults.results) { @@ -116,12 +116,13 @@ Status NonShardServerProcessInterface::insert( StatusWith<MongoProcessInterface::UpdateResult> NonShardServerProcessInterface::update( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, boost::optional<OID> targetEpoch) { - auto writeResults = write_ops_exec::performUpdates(expCtx->opCtx, *updateCommand); + auto writeResults = write_ops_exec::performUpdates( + expCtx->opCtx, buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi)); // Need to check each result in the batch since the writes are unordered. UpdateResult updateResult; @@ -183,10 +184,9 @@ void NonShardServerProcessInterface::renameIfOptionsAndIndexesHaveNotChanged( RenameCollectionOptions options; options.dropTarget = renameCommandObj["dropTarget"].trueValue(); options.stayTemp = renameCommandObj["stayTemp"].trueValue(); - options.originalCollectionOptions = originalCollectionOptions; - options.originalIndexes = originalIndexes; // skip sharding validation on non sharded servers - doLocalRenameIfOptionsAndIndexesHaveNotChanged(opCtx, sourceNs, targetNs, options); + doLocalRenameIfOptionsAndIndexesHaveNotChanged( + opCtx, sourceNs, targetNs, options, originalIndexes, originalCollectionOptions); } void NonShardServerProcessInterface::createCollection(OperationContext* opCtx, @@ -205,7 +205,6 @@ BSONObj NonShardServerProcessInterface::preparePipelineAndExplain( Pipeline* ownedPipeline, ExplainOptions::Verbosity verbosity) { std::vector<Value> pipelineVec; auto firstStage = ownedPipeline->peekFront(); - auto opts = SerializationOptions{verbosity}; // If the pipeline already has a cursor explain with that one, otherwise attach a new one like // we would for a normal execution and explain that. if (firstStage && typeid(*firstStage) == typeid(DocumentSourceCursor)) { @@ -213,7 +212,7 @@ BSONObj NonShardServerProcessInterface::preparePipelineAndExplain( // extracted the necessary information and won't need it again. std::unique_ptr<Pipeline, PipelineDeleter> managedPipeline( ownedPipeline, PipelineDeleter(ownedPipeline->getContext()->opCtx)); - pipelineVec = managedPipeline->writeExplainOps(opts); + pipelineVec = managedPipeline->writeExplainOps(verbosity); ownedPipeline = nullptr; } else { auto pipelineWithCursor = attachCursorSourceToPipelineForLocalRead(ownedPipeline); @@ -222,7 +221,7 @@ BSONObj NonShardServerProcessInterface::preparePipelineAndExplain( while (pipelineWithCursor->getNext()) { } } - pipelineVec = pipelineWithCursor->writeExplainOps(opts); + pipelineVec = pipelineWithCursor->writeExplainOps(verbosity); } BSONArrayBuilder bab; for (auto&& stage : pipelineVec) { diff --git a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h index 9b96e83a1a8..ccbe90205c9 100644 --- a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h @@ -88,13 +88,13 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + std::vector<BSONObj>&& objs, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) override; StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp index 694038eff96..01db33c2337 100644 --- a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp @@ -35,6 +35,7 @@ #include "mongo/db/catalog/drop_collection.h" #include "mongo/db/catalog/rename_collection.h" #include "mongo/db/concurrency/d_concurrency.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/index_builds_coordinator.h" #include "mongo/db/logical_session_id_helpers.h" @@ -69,27 +70,26 @@ void ReplicaSetNodeProcessInterface::setReplicaSetNodeExecutor( replicaSetNodeExecutor(service) = std::move(executor); } -Status ReplicaSetNodeProcessInterface::insert( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, - const WriteConcernOptions& wc, - boost::optional<OID> targetEpoch) { +Status ReplicaSetNodeProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::vector<BSONObj>&& objs, + const WriteConcernOptions& wc, + boost::optional<OID> targetEpoch) { auto&& opCtx = expCtx->opCtx; if (_canWriteLocally(opCtx, ns)) { - return NonShardServerProcessInterface::insert( - expCtx, ns, std::move(insertCommand), wc, targetEpoch); + return NonShardServerProcessInterface::insert(expCtx, ns, std::move(objs), wc, targetEpoch); } - BatchedCommandRequest batchInsertCommand(std::move(insertCommand)); + BatchedCommandRequest insertCommand( + buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation)); - return _executeCommandOnPrimary(opCtx, ns, batchInsertCommand.toBSON()).getStatus(); + return _executeCommandOnPrimary(opCtx, ns, std::move(insertCommand.toBSON())).getStatus(); } StatusWith<MongoProcessInterface::UpdateResult> ReplicaSetNodeProcessInterface::update( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -97,11 +97,11 @@ StatusWith<MongoProcessInterface::UpdateResult> ReplicaSetNodeProcessInterface:: auto&& opCtx = expCtx->opCtx; if (_canWriteLocally(opCtx, ns)) { return NonShardServerProcessInterface::update( - expCtx, ns, std::move(updateCommand), wc, upsert, multi, targetEpoch); + expCtx, ns, std::move(batch), wc, upsert, multi, targetEpoch); } - BatchedCommandRequest batchUpdateCommand(std::move(updateCommand)); - auto result = _executeCommandOnPrimary(opCtx, ns, batchUpdateCommand.toBSON()); + BatchedCommandRequest updateCommand(buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi)); + auto result = _executeCommandOnPrimary(opCtx, ns, std::move(updateCommand.toBSON())); if (!result.isOK()) { return result.getStatus(); } diff --git a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h index 55b645c59aa..c61f654e844 100644 --- a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h @@ -43,15 +43,6 @@ class ReplicaSetNodeProcessInterface final : public NonShardServerProcessInterfa public: using NonShardServerProcessInterface::NonShardServerProcessInterface; - std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( - OperationContext* opCtx, const NamespaceString& ns) const override { - if (_canWriteLocally(opCtx, ns)) { - return std::make_unique<LocalWriteSizeEstimator>(); - } else { - return std::make_unique<TargetPrimaryWriteSizeEstimator>(); - } - } - static std::shared_ptr<executor::TaskExecutor> getReplicaSetNodeExecutor( ServiceContext* service); @@ -68,13 +59,12 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + std::vector<BSONObj>&& objs, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) final; - StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp index aa6914f88c3..69b5a111e2b 100644 --- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp @@ -29,6 +29,8 @@ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery +#include "mongo/platform/basic.h" + #include "mongo/db/pipeline/process_interface/shardsvr_process_interface.h" #include <fmt/format.h> @@ -49,7 +51,7 @@ #include "mongo/s/cluster_commands_helpers.h" #include "mongo/s/cluster_write.h" #include "mongo/s/query/document_source_merge_cursors.h" -#include "mongo/s/router_role.h" +#include "mongo/s/router.h" #include "mongo/s/stale_shard_version_helpers.h" namespace mongo { @@ -74,11 +76,14 @@ void ShardServerProcessInterface::checkRoutingInfoEpochOrThrow( catalogCache->invalidateShardOrEntireCollectionEntryForShardedCollection( nss, targetCollectionVersion, shardId); - const auto cm = uassertStatusOK(catalogCache->getCollectionRoutingInfo(expCtx->opCtx, nss)); - auto foundVersion = cm.isSharded() ? cm.getVersion() : ChunkVersion::UNSHARDED(); + const auto routingInfo = + uassertStatusOK(catalogCache->getCollectionRoutingInfo(expCtx->opCtx, nss)); + + const auto foundVersion = + routingInfo.isSharded() ? routingInfo.getVersion() : ChunkVersion::UNSHARDED(); - uassert(StaleEpochInfo(nss, targetCollectionVersion, foundVersion), - str::stream() << "Could not act as router for " << nss.ns() << ", received " + uassert(StaleEpochInfo(nss), + str::stream() << "Could not act as router for " << nss.ns() << ", wanted " << targetCollectionVersion.toString() << ", but found " << foundVersion.toString(), foundVersion.isSameCollection(targetCollectionVersion)); @@ -99,22 +104,20 @@ boost::optional<Document> ShardServerProcessInterface::lookupSingleDocument( return doLookupSingleDocument(expCtx, nss, collectionUUID, documentKey, std::move(opts)); } -Status ShardServerProcessInterface::insert( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, - const WriteConcernOptions& wc, - boost::optional<OID> targetEpoch) { +Status ShardServerProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::vector<BSONObj>&& objs, + const WriteConcernOptions& wc, + boost::optional<OID> targetEpoch) { BatchedCommandResponse response; BatchWriteExecStats stats; - BatchedCommandRequest batchInsertCommand(std::move(insertCommand)); + BatchedCommandRequest insertCommand( + buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation)); - const auto originalWC = expCtx->opCtx->getWriteConcern(); - ScopeGuard resetWCGuard([&] { expCtx->opCtx->setWriteConcern(originalWC); }); - expCtx->opCtx->setWriteConcern(wc); + insertCommand.setWriteConcern(wc.toBSON()); - cluster::write(expCtx->opCtx, batchInsertCommand, &stats, &response, targetEpoch); + cluster::write(expCtx->opCtx, insertCommand, &stats, &response, targetEpoch); return response.toStatus(); } @@ -122,7 +125,7 @@ Status ShardServerProcessInterface::insert( StatusWith<MongoProcessInterface::UpdateResult> ShardServerProcessInterface::update( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -130,13 +133,11 @@ StatusWith<MongoProcessInterface::UpdateResult> ShardServerProcessInterface::upd BatchedCommandResponse response; BatchWriteExecStats stats; - BatchedCommandRequest batchUpdateCommand(std::move(updateCommand)); + BatchedCommandRequest updateCommand(buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi)); - const auto originalWC = expCtx->opCtx->getWriteConcern(); - ScopeGuard resetWCGuard([&] { expCtx->opCtx->setWriteConcern(originalWC); }); - expCtx->opCtx->setWriteConcern(wc); + updateCommand.setWriteConcern(wc.toBSON()); - cluster::write(expCtx->opCtx, batchUpdateCommand, &stats, &response, targetEpoch); + cluster::write(expCtx->opCtx, updateCommand, &stats, &response, targetEpoch); if (auto status = response.toStatus(); status != Status::OK()) { return status; @@ -173,35 +174,29 @@ void ShardServerProcessInterface::renameIfOptionsAndIndexesHaveNotChanged( const NamespaceString& destinationNs, const BSONObj& originalCollectionOptions, const std::list<BSONObj>& originalIndexes) { - sharding::router::DBPrimaryRouter router(opCtx->getServiceContext(), destinationNs.db()); - router.route(opCtx, - "ShardServerProcessInterface::renameIfOptionsAndIndexesHaveNotChanged", - [&](OperationContext* opCtx, const CachedDatabaseInfo& cdb) { - auto newCmdObj = CommonMongodProcessInterface::_convertRenameToInternalRename( - opCtx, renameCommandObj, originalCollectionOptions, originalIndexes); - BSONObjBuilder newCmdWithWriteConcernBuilder(std::move(newCmdObj)); - newCmdWithWriteConcernBuilder.append(WriteConcernOptions::kWriteConcernField, - opCtx->getWriteConcern().toBSON()); - newCmdObj = newCmdWithWriteConcernBuilder.done(); - auto response = executeCommandAgainstDatabasePrimary( - opCtx, - // internalRenameIfOptionsAndIndexesMatch is adminOnly. - NamespaceString::kAdminDb, - cdb, - newCmdObj, - ReadPreferenceSetting(ReadPreference::PrimaryOnly), - Shard::RetryPolicy::kNoRetry); - uassertStatusOKWithContext(response.swResponse, - str::stream() << "failed while running command " - << newCmdObj); - auto result = response.swResponse.getValue().data; - uassertStatusOKWithContext(getStatusFromCommandResult(result), - str::stream() << "failed while running command " - << newCmdObj); - uassertStatusOKWithContext(getWriteConcernStatusFromCommandResult(result), - str::stream() << "failed while running command " - << newCmdObj); - }); + auto cachedDbInfo = + uassertStatusOK(Grid::get(opCtx)->catalogCache()->getDatabase(opCtx, destinationNs.db())); + auto newCmdObj = CommonMongodProcessInterface::_convertRenameToInternalRename( + opCtx, renameCommandObj, originalCollectionOptions, originalIndexes); + BSONObjBuilder newCmdWithWriteConcernBuilder(std::move(newCmdObj)); + newCmdWithWriteConcernBuilder.append(WriteConcernOptions::kWriteConcernField, + opCtx->getWriteConcern().toBSON()); + newCmdObj = newCmdWithWriteConcernBuilder.done(); + auto response = + executeCommandAgainstDatabasePrimary(opCtx, + // internalRenameIfOptionsAndIndexesMatch is adminOnly. + NamespaceString::kAdminDb, + std::move(cachedDbInfo), + newCmdObj, + ReadPreferenceSetting(ReadPreference::PrimaryOnly), + Shard::RetryPolicy::kNoRetry); + uassertStatusOKWithContext(response.swResponse, + str::stream() << "failed while running command " << newCmdObj); + auto result = response.swResponse.getValue().data; + uassertStatusOKWithContext(getStatusFromCommandResult(result), + str::stream() << "failed while running command " << newCmdObj); + uassertStatusOKWithContext(getWriteConcernStatusFromCommandResult(result), + str::stream() << "failed while running command " << newCmdObj); } BSONObj ShardServerProcessInterface::getCollectionOptions(OperationContext* opCtx, @@ -210,62 +205,58 @@ BSONObj ShardServerProcessInterface::getCollectionOptions(OperationContext* opCt return getCollectionOptionsLocally(opCtx, nss); } - sharding::router::DBPrimaryRouter router(opCtx->getServiceContext(), nss.db()); - return router.route( - opCtx, - "ShardServerProcessInterface::getCollectionOptions", - [&](OperationContext* opCtx, const CachedDatabaseInfo& cdb) { - const BSONObj filterObj = BSON("name" << nss.coll()); - const BSONObj cmdObj = BSON("listCollections" << 1 << "filter" << filterObj); - - const auto shard = uassertStatusOK( - Grid::get(opCtx)->shardRegistry()->getShard(opCtx, cdb->getPrimary())); - Shard::QueryResponse resultCollections; - - try { - resultCollections = uassertStatusOK(shard->runExhaustiveCursorCommand( - opCtx, - ReadPreferenceSetting(ReadPreference::PrimaryOnly), - nss.db().toString(), - appendDbVersionIfPresent(cmdObj, cdb), - Milliseconds(-1))); - } catch (const ExceptionFor<ErrorCodes::NamespaceNotFound>&) { - return BSONObj{}; - } + auto cachedDbInfo = + uassertStatusOK(Grid::get(opCtx)->catalogCache()->getDatabase(opCtx, nss.db())); + auto shard = uassertStatusOK( + Grid::get(opCtx)->shardRegistry()->getShard(opCtx, cachedDbInfo->getPrimary())); + + const BSONObj filterObj = BSON("name" << nss.coll()); + const BSONObj cmdObj = BSON("listCollections" << 1 << "filter" << filterObj); + + Shard::QueryResponse resultCollections; + try { + resultCollections = uassertStatusOK( + shard->runExhaustiveCursorCommand(opCtx, + ReadPreferenceSetting(ReadPreference::PrimaryOnly), + nss.db().toString(), + appendDbVersionIfPresent(cmdObj, cachedDbInfo), + Milliseconds(-1))); + } catch (const ExceptionFor<ErrorCodes::NamespaceNotFound>&) { + return BSONObj{}; + } - if (resultCollections.docs.empty()) { - return BSONObj{}; - } + if (resultCollections.docs.empty()) { + return BSONObj{}; + } - for (const BSONObj& bsonObj : resultCollections.docs) { - // Return first element which matches on name and has options. - const BSONElement nameElement = bsonObj["name"]; - if (!nameElement || nameElement.valueStringDataSafe() != nss.coll()) { - continue; - } - - const BSONElement optionsElement = bsonObj["options"]; - if (optionsElement) { - auto optionObj = optionsElement.Obj(); - - // If the BSON object has field 'info' and the BSON element 'info' has field - // 'uuid', then extract the uuid and add to the BSON object to be return. This - // will ensure that the BSON object is complaint with the BSON object returned - // for non-sharded namespace. - if (auto infoElement = bsonObj["info"]; infoElement && infoElement["uuid"]) { - return optionObj.addField(infoElement["uuid"]); - } - - return optionObj.getOwned(); - } - - tassert(5983900, - str::stream() << "Expected at most one collection with the name " << nss - << ": " << resultCollections.docs.size(), - resultCollections.docs.size() <= 1); + for (const BSONObj& bsonObj : resultCollections.docs) { + // Return first element which matches on name and has options. + const BSONElement nameElement = bsonObj["name"]; + if (!nameElement || nameElement.valueStringDataSafe() != nss.coll()) { + continue; + } + + const BSONElement optionsElement = bsonObj["options"]; + if (optionsElement) { + auto optionObj = optionsElement.Obj(); + + // If the BSON object has field 'info' and the BSON element 'info' has field 'uuid', + // then extract the uuid and add to the BSON object to be return. This will ensure that + // the BSON object is complaint with the BSON object returned for non-sharded namespace. + if (auto infoElement = bsonObj["info"]; infoElement && infoElement["uuid"]) { + return optionObj.addField(infoElement["uuid"]); } - return BSONObj{}; - }); + + return optionObj.getOwned(); + } + + tassert(5983900, + str::stream() << "Expected at most one collection with the name " << nss << ": " + << resultCollections.docs.size(), + resultCollections.docs.size() <= 1); + } + + return BSONObj{}; } std::list<BSONObj> ShardServerProcessInterface::getIndexSpecs(OperationContext* opCtx, @@ -273,58 +264,49 @@ std::list<BSONObj> ShardServerProcessInterface::getIndexSpecs(OperationContext* bool includeBuildUUIDs) { // Note that 'ns' must be an unsharded collection. The indexes for a sharded collection must be // read from a shard with a chunk instead of the primary shard. - sharding::router::DBPrimaryRouter router(opCtx->getServiceContext(), ns.db()); - return router.route(opCtx, - "ShardServerProcessInterface::getIndexSpecs", - [&](OperationContext* opCtx, const CachedDatabaseInfo& cdb) { - auto shard = - uassertStatusOK(Grid::get(opCtx)->shardRegistry()->getShard( - opCtx, cdb->getPrimary())); - auto cmdObj = BSON("listIndexes" << ns.coll()); - try { - auto indexes = uassertStatusOK(shard->runExhaustiveCursorCommand( - opCtx, - ReadPreferenceSetting(ReadPreference::PrimaryOnly), - ns.db().toString(), - appendDbVersionIfPresent(cmdObj, cdb), - Milliseconds(-1))); - return std::list<BSONObj>(indexes.docs.begin(), indexes.docs.end()); - } catch (ExceptionFor<ErrorCodes::NamespaceNotFound>&) { - return std::list<BSONObj>(); - } - }); + auto cachedDbInfo = + uassertStatusOK(Grid::get(opCtx)->catalogCache()->getDatabase(opCtx, ns.db())); + auto shard = uassertStatusOK( + Grid::get(opCtx)->shardRegistry()->getShard(opCtx, cachedDbInfo->getPrimary())); + auto cmdObj = BSON("listIndexes" << ns.coll()); + Shard::QueryResponse indexes; + try { + indexes = uassertStatusOK( + shard->runExhaustiveCursorCommand(opCtx, + ReadPreferenceSetting(ReadPreference::PrimaryOnly), + ns.db().toString(), + appendDbVersionIfPresent(cmdObj, cachedDbInfo), + Milliseconds(-1))); + } catch (ExceptionFor<ErrorCodes::NamespaceNotFound>&) { + return std::list<BSONObj>(); + } + return std::list<BSONObj>(indexes.docs.begin(), indexes.docs.end()); } void ShardServerProcessInterface::createCollection(OperationContext* opCtx, const std::string& dbName, const BSONObj& cmdObj) { - sharding::router::DBPrimaryRouter router(opCtx->getServiceContext(), dbName); - router.route(opCtx, - "ShardServerProcessInterface::createCollection", - [&](OperationContext* opCtx, const CachedDatabaseInfo& cdb) { - BSONObjBuilder finalCmdBuilder(cmdObj); - finalCmdBuilder.append(WriteConcernOptions::kWriteConcernField, - opCtx->getWriteConcern().toBSON()); - BSONObj finalCmdObj = finalCmdBuilder.obj(); - auto response = executeCommandAgainstDatabasePrimary( - opCtx, - dbName, - cdb, - finalCmdObj, - ReadPreferenceSetting(ReadPreference::PrimaryOnly), - Shard::RetryPolicy::kIdempotent); - uassertStatusOKWithContext(response.swResponse, - str::stream() << "failed while running command " - << finalCmdObj); - auto result = response.swResponse.getValue().data; - uassertStatusOKWithContext(getStatusFromCommandResult(result), - str::stream() << "failed while running command " - << finalCmdObj); - uassertStatusOKWithContext(getWriteConcernStatusFromCommandResult(result), - str::stream() - << "write concern failed while running command " - << finalCmdObj); - }); + auto cachedDbInfo = + uassertStatusOK(Grid::get(opCtx)->catalogCache()->getDatabase(opCtx, dbName)); + BSONObjBuilder finalCmdBuilder(cmdObj); + finalCmdBuilder.append(WriteConcernOptions::kWriteConcernField, + opCtx->getWriteConcern().toBSON()); + BSONObj finalCmdObj = finalCmdBuilder.obj(); + auto response = + executeCommandAgainstDatabasePrimary(opCtx, + dbName, + std::move(cachedDbInfo), + finalCmdObj, + ReadPreferenceSetting(ReadPreference::PrimaryOnly), + Shard::RetryPolicy::kIdempotent); + uassertStatusOKWithContext(response.swResponse, + str::stream() << "failed while running command " << finalCmdObj); + auto result = response.swResponse.getValue().data; + uassertStatusOKWithContext(getStatusFromCommandResult(result), + str::stream() << "failed while running command " << finalCmdObj); + uassertStatusOKWithContext(getWriteConcernStatusFromCommandResult(result), + str::stream() + << "write concern failed while running command " << finalCmdObj); } void ShardServerProcessInterface::createIndexesOnEmptyCollection( @@ -370,32 +352,28 @@ void ShardServerProcessInterface::dropCollection(OperationContext* opCtx, const NamespaceString& ns) { // Build and execute the dropCollection command against the primary shard of the given // database. - sharding::router::DBPrimaryRouter router(opCtx->getServiceContext(), ns.db()); - router.route( - opCtx, - "ShardServerProcessInterface::dropCollection", - [&](OperationContext* opCtx, const CachedDatabaseInfo& cdb) { - BSONObjBuilder newCmdBuilder; - newCmdBuilder.append("drop", ns.coll()); - newCmdBuilder.append(WriteConcernOptions::kWriteConcernField, - opCtx->getWriteConcern().toBSON()); - auto cmdObj = newCmdBuilder.done(); - auto response = executeCommandAgainstDatabasePrimary( - opCtx, - ns.db(), - cdb, - cmdObj, - ReadPreferenceSetting(ReadPreference::PrimaryOnly), - Shard::RetryPolicy::kIdempotent); - uassertStatusOKWithContext(response.swResponse, - str::stream() << "failed while running command " << cmdObj); - auto result = response.swResponse.getValue().data; - uassertStatusOKWithContext(getStatusFromCommandResult(result), - str::stream() << "failed while running command " << cmdObj); - uassertStatusOKWithContext( - getWriteConcernStatusFromCommandResult(result), - str::stream() << "write concern failed while running command " << cmdObj); - }); + auto cachedDbInfo = + uassertStatusOK(Grid::get(opCtx)->catalogCache()->getDatabase(opCtx, ns.db())); + BSONObjBuilder newCmdBuilder; + newCmdBuilder.append("drop", ns.coll()); + newCmdBuilder.append(WriteConcernOptions::kWriteConcernField, + opCtx->getWriteConcern().toBSON()); + auto cmdObj = newCmdBuilder.done(); + auto response = + executeCommandAgainstDatabasePrimary(opCtx, + ns.db(), + std::move(cachedDbInfo), + cmdObj, + ReadPreferenceSetting(ReadPreference::PrimaryOnly), + Shard::RetryPolicy::kIdempotent); + uassertStatusOKWithContext(response.swResponse, + str::stream() << "failed while running command " << cmdObj); + auto result = response.swResponse.getValue().data; + uassertStatusOKWithContext(getStatusFromCommandResult(result), + str::stream() << "failed while running command " << cmdObj); + uassertStatusOKWithContext(getWriteConcernStatusFromCommandResult(result), + str::stream() + << "write concern failed while running command " << cmdObj); } std::unique_ptr<Pipeline, PipelineDeleter> diff --git a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h index a08aa23777f..f6026f6ef3a 100644 --- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h @@ -53,11 +53,6 @@ public: const NamespaceString& nss, ChunkVersion targetCollectionVersion) const final; - std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( - OperationContext* opCtx, const NamespaceString& ns) const final { - return std::make_unique<TargetPrimaryWriteSizeEstimator>(); - } - std::vector<FieldPath> collectDocumentKeyFieldsActingAsRouter( OperationContext*, const NamespaceString&) const final { // We don't expect anyone to use this method on the shard itself (yet). This is currently @@ -76,15 +71,23 @@ public: const Document& documentKey, boost::optional<BSONObj> readConcern) final; + /** + * Inserts the documents 'objs' into the namespace 'ns' using the ClusterWriter for locking, + * routing, stale config handling, etc. + */ Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + std::vector<BSONObj>&& objs, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) final; + /** + * Replaces the documents matching 'queries' with 'updates' using the ClusterWriter for locking, + * routing, stale config handling, etc. + */ StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface_test.cpp b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface_test.cpp index d60af845ccc..5c3f7eebf97 100644 --- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface_test.cpp +++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface_test.cpp @@ -28,11 +28,9 @@ */ #include "mongo/db/concurrency/lock_state.h" -#include "mongo/db/cursor_id.h" #include "mongo/db/pipeline/document_source_out.h" #include "mongo/db/pipeline/document_source_queue.h" #include "mongo/db/pipeline/process_interface/shardsvr_process_interface.h" -#include "mongo/db/query/cursor_response.h" #include "mongo/s/query/sharded_agg_test_fixture.h" #include "mongo/unittest/unittest.h" diff --git a/src/mongo/db/pipeline/process_interface/standalone_process_interface.h b/src/mongo/db/pipeline/process_interface/standalone_process_interface.h index dc562b9089e..aceff8e6928 100644 --- a/src/mongo/db/pipeline/process_interface/standalone_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/standalone_process_interface.h @@ -41,11 +41,6 @@ public: StandaloneProcessInterface(std::shared_ptr<executor::TaskExecutor> exec) : NonShardServerProcessInterface(std::move(exec)) {} - std::unique_ptr<MongoProcessInterface::WriteSizeEstimator> getWriteSizeEstimator( - OperationContext* opCtx, const NamespaceString& ns) const final { - return std::make_unique<LocalWriteSizeEstimator>(); - } - virtual ~StandaloneProcessInterface() = default; }; diff --git a/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h b/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h index d69d5af7809..3fe1430ac72 100644 --- a/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h @@ -52,37 +52,6 @@ public: MONGO_UNREACHABLE; } - class StubWriteSizeEstimator final : public WriteSizeEstimator { - public: - int estimateInsertHeaderSize( - const write_ops::InsertCommandRequest& insertReq) const override { - return 0; - } - - int estimateUpdateHeaderSize( - const write_ops::UpdateCommandRequest& insertReq) const override { - return 0; - } - - int estimateInsertSizeBytes(const BSONObj& insert) const override { - MONGO_UNREACHABLE; - } - - int estimateUpdateSizeBytes(const BatchObject& batchObject, - UpsertType type) const override { - MONGO_UNREACHABLE; - } - }; - - std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( - OperationContext* opCtx, const NamespaceString& ns) const override { - return std::make_unique<StubWriteSizeEstimator>(); - } - - bool isExpectedToExecuteQueries() override { - return false; - } - bool isSharded(OperationContext* opCtx, const NamespaceString& ns) override { return false; } @@ -91,7 +60,7 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + std::vector<BSONObj>&& objs, const WriteConcernOptions& wc, boost::optional<OID>) override { MONGO_UNREACHABLE; @@ -99,7 +68,7 @@ public: StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + BatchedObjects&& batch, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -125,8 +94,7 @@ public: } boost::optional<BSONObj> getCatalogEntry(OperationContext* opCtx, - const NamespaceString& ns, - const boost::optional<UUID>& collUUID) const override { + const NamespaceString& ns) const override { MONGO_UNREACHABLE; } @@ -259,11 +227,11 @@ public: return BackupCursorState{UUID::gen(), boost::none, nullptr, {}}; } - void closeBackupCursor(OperationContext* opCtx, const UUID& backupId) override {} + void closeBackupCursor(OperationContext* opCtx, const UUID& backupId) final {} BackupCursorExtendState extendBackupCursor(OperationContext* opCtx, const UUID& backupId, - const Timestamp& extendTo) override { + const Timestamp& extendTo) final { return {{}}; } diff --git a/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp b/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp index cc1f696fcaa..a008cc92115 100644 --- a/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp +++ b/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp @@ -35,8 +35,6 @@ #include "mongo/db/pipeline/document_source_mock.h" #include "mongo/db/pipeline/sharded_agg_helpers.h" #include "mongo/db/s/config/initial_split_policy.h" -#include "mongo/idl/server_parameter_test_util.h" -#include "mongo/logv2/log.h" #include "mongo/s/query/sharded_agg_test_fixture.h" #include "mongo/unittest/unittest.h" @@ -49,6 +47,7 @@ const ShardId primaryShardId = ShardId("0"); TEST_F(ReshardingSplitPolicyTest, ShardKeyWithNonDottedFieldAndIdIsNotProjectedSucceeds) { auto shardKeyPattern = ShardKeyPattern(BSON("a" << 1)); + auto pipeline = Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), @@ -56,6 +55,7 @@ TEST_F(ReshardingSplitPolicyTest, ShardKeyWithNonDottedFieldAndIdIsNotProjectedS auto mockSource = DocumentSourceMock::createForTest({"{_id: 10, a: 15}", "{_id: 3, a: 5}"}, expCtx()); pipeline->addInitialSource(mockSource.get()); + // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); @@ -92,6 +92,7 @@ TEST_F(ReshardingSplitPolicyTest, ShardKeyWithIdFieldIsProjectedSucceeds) { TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithNonDottedHashedFieldSucceeds) { auto shardKeyPattern = ShardKeyPattern(BSON("a" << 1 << "b" << "hashed")); + auto pipeline = Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), @@ -99,6 +100,7 @@ TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithNonDottedHashedFieldSuccee auto mockSource = DocumentSourceMock::createForTest( {"{x: 1, b: 16, a: 15}", "{x: 2, b: 123, a: 5}"}, expCtx()); pipeline->addInitialSource(mockSource.get()); + // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); @@ -126,9 +128,9 @@ TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedFieldSucceeds) { // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("a.b" << 10 << "c" << 5)); + ASSERT_BSONOBJ_EQ(next.get().toBson(), BSON("a" << BSON("b" << 10) << "c" << 5)); next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("a.b" << 20 << "c" << 1)); + ASSERT_BSONOBJ_EQ(next.get().toBson(), BSON("a" << BSON("b" << 20) << "c" << 1)); ASSERT(!pipeline->getNext()); } @@ -147,11 +149,11 @@ TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedHashedFieldSucceeds) // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), - BSON("a.b" << 10 << "c" << 5 << "a.c" << -6548868637522515075LL)); + ASSERT_BSONOBJ_EQ(next.get().toBson(), + BSON("a" << BSON("b" << 10 << "c" << -6548868637522515075LL) << "c" << 5)); next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), - BSON("a.b" << 20 << "c" << 1 << "a.c" << 2598032665634823220LL)); + ASSERT_BSONOBJ_EQ(next.get().toBson(), + BSON("a" << BSON("b" << 20 << "c" << 2598032665634823220LL) << "c" << 1)); ASSERT(!pipeline->getNext()); } @@ -198,78 +200,5 @@ TEST_F(ReshardingSplitPolicyTest, SamplingSuceeds) { } } -TEST_F(ReshardingSplitPolicyTest, ShardKeyWithDottedPathAndIdIsNotProjectedSucceeds) { - auto shardKeyPattern = ShardKeyPattern(BSON("b" << 1)); - auto pipeline = - Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( - shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), - expCtx()); - auto mockSource = DocumentSourceMock::createForTest( - {"{_id: {a: 15}, b: 10}", "{_id: {a: 5}, b:1}"}, expCtx()); - pipeline->addInitialSource(mockSource.get()); - auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("b" << 1)); - next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("b" << 10)); - ASSERT(!pipeline->getNext()); -} - -TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedPathAndIdIsProjectedSucceeds) { - auto shardKeyPattern = ShardKeyPattern(BSON("_id.a" << 1 << "c" << 1)); - auto pipeline = - Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( - shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), - expCtx()); - auto mockSource = DocumentSourceMock::createForTest( - {"{_id: {a: 15}, c: 10}", "{_id: {a: 5}, c: 1}"}, expCtx()); - pipeline->addInitialSource(mockSource.get()); - auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("_id.a" << 5 << "c" << 1)); - next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("_id.a" << 15 << "c" << 10)); - ASSERT(!pipeline->getNext()); -} - -TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedHashedPathSucceeds) { - auto shardKeyPattern = ShardKeyPattern(BSON("_id.a" << 1 << "b" << 1 << "_id.b" - << "hashed")); - auto pipeline = - Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( - shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), - expCtx()); - auto mockSource = DocumentSourceMock::createForTest( - {"{x: 10, _id: {a: 20, b: 16}, b: 1}", "{x: 3, _id: {a: 10, b: 123}, b: 5}"}, expCtx()); - pipeline->addInitialSource(mockSource.get()); - - auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), - BSON("_id.a" << 10 << "b" << 5 << "_id.b" << -6548868637522515075LL)); - next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.value().toBson(), - BSON("_id.a" << 20 << "b" << 1 << "_id.b" << 2598032665634823220LL)); - ASSERT(!pipeline->getNext()); -} - -TEST_F(ReshardingSplitPolicyTest, ReshardingSucceedsWithLimitedMemoryForSortOperation) { - RAIIServerParameterControllerForTest sortMaxMemory{ - "internalQueryMaxBlockingSortMemoryUsageBytes", 100}; - auto shardKeyPattern = ShardKeyPattern(BSON("a" << 1)); - const NamespaceString ns("reshard", "foo"); - auto pipelineDocSource = - ReshardingSplitPolicy::makePipelineDocumentSource_forTest(operationContext(), - kTestAggregateNss, - shardKeyPattern, - 3 /*numInitialChunks*/, - 2 /*samplesPerChunk*/); - auto mockSource = DocumentSourceMock::createForTest( - {"{_id: 20, a: 4}", "{_id: 30, a: 3}", "{_id: 40, a: 2}", "{_id: 50, a: 1}"}, expCtx()); - pipelineDocSource->getPipeline_forTest()->addInitialSource(mockSource.get()); - auto next = pipelineDocSource->getNext(); - ASSERT_BSONOBJ_EQ(BSON("a" << 2), next.value()); - next = pipelineDocSource->getNext(); - ASSERT_BSONOBJ_EQ(BSON("a" << 4), next.value()); - ASSERT(!pipelineDocSource->getNext()); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/resume_token.cpp b/src/mongo/db/pipeline/resume_token.cpp index fe6468842ea..cc6d3631fd3 100644 --- a/src/mongo/db/pipeline/resume_token.cpp +++ b/src/mongo/db/pipeline/resume_token.cpp @@ -38,6 +38,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/exec/document_value/value_comparator.h" #include "mongo/db/pipeline/change_stream_helpers_legacy.h" +#include "mongo/db/pipeline/document_source_change_stream_gen.h" #include "mongo/db/storage/key_string.h" #include "mongo/util/hex.h" @@ -87,8 +88,7 @@ bool ResumeTokenData::operator==(const ResumeTokenData& other) const { return clusterTime == other.clusterTime && version == other.version && tokenType == other.tokenType && txnOpIndex == other.txnOpIndex && fromInvalidate == other.fromInvalidate && uuid == other.uuid && - (Value::compare(this->eventIdentifier, other.eventIdentifier, nullptr) == 0) && - fragmentNum == other.fragmentNum; + (Value::compare(this->eventIdentifier, other.eventIdentifier, nullptr) == 0); } std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) { @@ -102,11 +102,7 @@ std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) { out << ", fromInvalidate: " << static_cast<bool>(tokenData.fromInvalidate); } out << ", uuid: " << tokenData.uuid; - out << ", eventIdentifier: " << tokenData.eventIdentifier; - if (tokenData.version >= 2) { - out << ", fragmentNum: " << tokenData.fragmentNum; - } - out << "}"; + out << ", eventIdentifier: " << tokenData.eventIdentifier << "}"; return out; } @@ -167,14 +163,6 @@ ResumeToken::ResumeToken(const ResumeTokenData& data) { } data.eventIdentifier.addToBsonObj(&builder, ""); - if (data.fragmentNum) { - uassert(7182504, - str::stream() << "Tokens of version " << data.version - << " cannot have a fragmentNum", - data.version >= 2); - builder.appendNumber("", static_cast<long long>(*data.fragmentNum)); - } - auto keyObj = builder.obj(); KeyString::Builder encodedToken(KeyString::Version::V1, keyObj, Ordering::make(BSONObj())); _hexKeyString = hexblob::encode(encodedToken.getBuffer(), encodedToken.getSize()); @@ -297,38 +285,12 @@ ResumeTokenData ResumeToken::getData() const { "Resume Token eventIdentifier is not an object", result.eventIdentifier.getType() == BSONType::Object); - if (i.more() && result.version >= 2) { - auto fragmentNum = i.next(); - uassert(7182501, - "Resume token 'fragmentNum' must be a non-negative integer.", - fragmentNum.type() == BSONType::NumberInt && fragmentNum.numberInt() >= 0); - result.fragmentNum = fragmentNum.numberInt(); - } - uassert(40646, "invalid oversized resume token", !i.more()); return result; } -Document ResumeToken::toDocument(const SerializationOptions& options) const { - /* - * This is our default resume token for the representative query shape. - * We use a high water mark token, otherwise a resume event is expected when reparsing. - * When serializing the "_typeBits", we purposely avoid serializing with SerializationOptions, - * as this will result in mistakenly add '?undefined' to the Document. - * The serialization of the Document will typically exclude the "_typeBits" if they - * were unset, which is the case for "kDefaultToken". - */ - static const auto kDefaultToken = makeHighWaterMarkToken(Timestamp(), 0); - return Document{{kDataFieldName, - options.serializeLiteral(_hexKeyString, Value(kDefaultToken._hexKeyString))}, - {kTypeBitsFieldName, - options.literalPolicy != LiteralSerializationPolicy::kToDebugTypeString - ? options.serializeLiteral(_typeBits, kDefaultToken._typeBits) - : kDefaultToken._typeBits}}; -} - -BSONObj ResumeToken::toBSON(const SerializationOptions& options) const { - return toDocument(options).toBson(); +Document ResumeToken::toDocument() const { + return Document{{kDataFieldName, _hexKeyString}, {kTypeBitsFieldName, _typeBits}}; } ResumeToken ResumeToken::parse(const Document& resumeDoc) { diff --git a/src/mongo/db/pipeline/resume_token.h b/src/mongo/db/pipeline/resume_token.h index 74b2dd9ddef..a157c97fed9 100644 --- a/src/mongo/db/pipeline/resume_token.h +++ b/src/mongo/db/pipeline/resume_token.h @@ -36,7 +36,6 @@ #include "mongo/bson/timestamp.h" #include "mongo/db/exec/document_value/document.h" #include "mongo/db/exec/document_value/value.h" -#include "mongo/db/query/query_shape/serialization_options.h" #include "mongo/util/uuid.h" namespace mongo { @@ -108,9 +107,6 @@ struct ResumeTokenData { // The eventIdentifier can be either be a document key for CRUD operations, or a more // descriptive operation details for non-CRUD operations. Value eventIdentifier; - - // Index of the current fragment, for oversized events that have been split. - boost::optional<size_t> fragmentNum; }; std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData); @@ -166,18 +162,11 @@ public: */ explicit ResumeToken(const ResumeTokenData& resumeValue); - /** - * Convenience method to represent the ResumeToken as a Document. - * Provides support for specifying SerializationOptions, as this method is used to service the - * toBSON(). - */ - Document toDocument(const SerializationOptions& options = {}) const; + Document toDocument() const; - /** - * Serialization to BSONObj. Provides support for specifying SerializationOptions, - * as ResumeToken requires a "query_shape: custom" specification in its IDL uses. - */ - BSONObj toBSON(const SerializationOptions& options = {}) const; + BSONObj toBSON() const { + return toDocument().toBson(); + } ResumeTokenData getData() const; diff --git a/src/mongo/db/pipeline/resume_token.idl b/src/mongo/db/pipeline/resume_token.idl deleted file mode 100644 index ee20d401dd0..00000000000 --- a/src/mongo/db/pipeline/resume_token.idl +++ /dev/null @@ -1,46 +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. -# - -global: - cpp_namespace: "mongo" - cpp_includes: - - "mongo/db/pipeline/resume_token.h" - -imports: - - "mongo/idl/basic_types.idl" - -types: - # A resume token is an opaque document we return to the user that contains all the information - # needed to resume a stream where they left off. It also provides the ordering of streams - # from multiple shards. - resumeToken: - bson_serialization_type: object - description: An object representing a resume token for a change stream - cpp_type: ResumeToken - serializer: ResumeToken::toBSON - deserializer: ResumeToken::parse diff --git a/src/mongo/db/pipeline/resume_token_test.cpp b/src/mongo/db/pipeline/resume_token_test.cpp index 8005911d5a9..eed85344ddd 100644 --- a/src/mongo/db/pipeline/resume_token_test.cpp +++ b/src/mongo/db/pipeline/resume_token_test.cpp @@ -491,51 +491,5 @@ TEST(ResumeToken, StringEncodingSortsCorrectly) { {ts10_4, 2, 0, lower_uuid, higherEventIdentifer}); } -TEST(ResumeToken, FragmentNumRoundTripsThroughEncodingAndDecoding) { - ResumeTokenData resumeTokenDataIn{ - Timestamp(1000, 1), 2, 0, UUID::gen(), Value(Document{{"_id", 1}})}; - - auto resumeTokenDataFragmentNone = - ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()).getData(); - - ASSERT_EQ(resumeTokenDataIn, resumeTokenDataFragmentNone); - ASSERT_EQ(ResumeToken(resumeTokenDataIn).toBSON().objsize(), - ResumeToken(resumeTokenDataFragmentNone).toBSON().objsize()); - - resumeTokenDataIn.fragmentNum = 0ULL; - auto resumeTokenDataFragment0 = - ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()).getData(); - - ASSERT_EQ(resumeTokenDataIn, resumeTokenDataFragment0); - ASSERT_EQ(ResumeToken(resumeTokenDataIn).toBSON().objsize(), - ResumeToken(resumeTokenDataFragment0).toBSON().objsize()); - - resumeTokenDataIn.fragmentNum = 1ULL; - auto resumeTokenDataFragment1 = - ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()).getData(); - - ASSERT_EQ(resumeTokenDataIn, resumeTokenDataFragment1); - ASSERT_EQ(ResumeToken(resumeTokenDataIn).toBSON().objsize(), - ResumeToken(resumeTokenDataFragment1).toBSON().objsize()); -} - -TEST(ResumeToken, NegativeFragmentNumThrows) { - ResumeTokenData resumeTokenDataIn{ - Timestamp(1000, 1), 2, 0, UUID::gen(), Value(Document{{"_id", 1}})}; - - // Large 'ResumeTokenData::fragmentNum' value will be serialized as a negative integer. - resumeTokenDataIn.fragmentNum = std::numeric_limits<size_t>::max(); - auto resumeToken = ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()); - - ASSERT_THROWS_CODE(resumeToken.getData(), DBException, 7182501); -} - -TEST(ResumeToken, FragmentNumInV1Throws) { - ResumeTokenData resumeTokenDataV1{ - Timestamp(1000, 1), 1, 0, UUID::gen(), Value(Document{{"_id", 1}})}; - - resumeTokenDataV1.fragmentNum = 0ULL; - ASSERT_THROWS_CODE(ResumeToken(resumeTokenDataV1), DBException, 7182504); -} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/semantic_analysis.cpp b/src/mongo/db/pipeline/semantic_analysis.cpp index 04aa2de4541..c1613a6a85f 100644 --- a/src/mongo/db/pipeline/semantic_analysis.cpp +++ b/src/mongo/db/pipeline/semantic_analysis.cpp @@ -79,7 +79,7 @@ boost::optional<std::string> findRename(const StringMap<std::string>& renamedPat * maps the path to itself. */ StringMap<std::string> computeNamesAssumingAnyPathsNotRenamedAreUnmodified( - const StringMap<std::string>& renamedPaths, const OrderedPathSet& pathsOfInterest) { + const StringMap<std::string>& renamedPaths, const std::set<std::string>& pathsOfInterest) { StringMap<std::string> renameOut; for (auto&& ofInterest : pathsOfInterest) { if (auto name = findRename(renamedPaths, ofInterest)) { @@ -163,7 +163,7 @@ template <class Iterator> boost::optional<Iterator> lookForNestUnnestPattern( Iterator start, Iterator end, - OrderedPathSet pathsOfInterest, + std::set<std::string> pathsOfInterest, const Direction& traversalDir, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { auto replaceRootTransform = isReplaceRoot((*start).get()); @@ -252,7 +252,7 @@ template <class Iterator> std::pair<Iterator, StringMap<std::string>> multiStageRenamedPaths( Iterator start, Iterator end, - OrderedPathSet pathsOfInterest, + std::set<std::string> pathsOfInterest, const Direction& traversalDir, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none) { @@ -300,7 +300,7 @@ template <class Iterator> boost::optional<StringMap<std::string>> renamedPathsFullPipeline( Iterator start, Iterator end, - OrderedPathSet pathsOfInterest, + std::set<std::string> pathsOfInterest, const Direction& traversalDir, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { auto [itr, renameMap] = multiStageRenamedPaths( @@ -313,9 +313,9 @@ boost::optional<StringMap<std::string>> renamedPathsFullPipeline( } // namespace -OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies, - const OrderedPathSet& preservedPaths) { - OrderedPathSet modifiedDependencies; +std::set<std::string> extractModifiedDependencies(const std::set<std::string>& dependencies, + const std::set<std::string>& preservedPaths) { + std::set<std::string> modifiedDependencies; // The modified dependencies is *almost* the set difference 'dependencies' - 'preservedPaths', // except that if p in 'preservedPaths' is a "path prefix" of d in 'dependencies', then 'd' @@ -342,7 +342,7 @@ OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies, return modifiedDependencies; } -boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& pathsOfInterest, +boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>& pathsOfInterest, const DocumentSource& stage, const Direction& traversalDir) { auto modifiedPathsRet = stage.getModifiedPaths(); @@ -351,11 +351,19 @@ boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& paths case DocumentSource::GetModPathsReturn::Type::kAllPaths: return boost::none; case DocumentSource::GetModPathsReturn::Type::kFiniteSet: { - // Any overlap of the path means the path of interest is not preserved. For - // example, if the path of interest is "a.b", then a modified path of "a", - // "a.b", or "a.b.c" would all signal that "a.b" is not preserved. - if (!expression::areIndependent(modifiedPathsRet.paths, pathsOfInterest)) { - return boost::none; + for (auto&& modified : modifiedPathsRet.paths) { + for (auto&& ofInterest : pathsOfInterest) { + // Any overlap of the path means the path of interest is not preserved. For + // example, if the path of interest is "a.b", then a modified path of "a", + // "a.b", or "a.b.c" would all signal that "a.b" is not preserved. + if (ofInterest == modified || + expression::isPathPrefixOf(ofInterest, modified) || + expression::isPathPrefixOf(modified, ofInterest)) { + // This stage modifies at least one of the fields which the caller is + // interested in, bail out. + return boost::none; + } + } } // None of the paths of interest were modified, construct the result map, mapping @@ -393,7 +401,7 @@ boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& paths boost::optional<StringMap<std::string>> renamedPaths( const Pipeline::SourceContainer::const_iterator start, const Pipeline::SourceContainer::const_iterator end, - const OrderedPathSet& pathsOfInterest, + const std::set<std::string>& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { return renamedPathsFullPipeline( start, end, pathsOfInterest, Direction::kForward, additionalStageValidatorCallback); @@ -402,7 +410,7 @@ boost::optional<StringMap<std::string>> renamedPaths( boost::optional<StringMap<std::string>> renamedPaths( const Pipeline::SourceContainer::const_reverse_iterator start, const Pipeline::SourceContainer::const_reverse_iterator end, - const OrderedPathSet& pathsOfInterest, + const std::set<std::string>& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { return renamedPathsFullPipeline( start, end, pathsOfInterest, Direction::kBackward, additionalStageValidatorCallback); @@ -412,7 +420,7 @@ std::pair<Pipeline::SourceContainer::const_iterator, StringMap<std::string>> findLongestViablePrefixPreservingPaths( const Pipeline::SourceContainer::const_iterator start, const Pipeline::SourceContainer::const_iterator end, - const OrderedPathSet& pathsOfInterest, + const std::set<std::string>& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { return multiStageRenamedPaths( start, end, pathsOfInterest, Direction::kForward, additionalStageValidatorCallback); diff --git a/src/mongo/db/pipeline/semantic_analysis.h b/src/mongo/db/pipeline/semantic_analysis.h index befbddac923..1931800b9df 100644 --- a/src/mongo/db/pipeline/semantic_analysis.h +++ b/src/mongo/db/pipeline/semantic_analysis.h @@ -57,7 +57,7 @@ enum class Direction { kForward, kBackward }; * the pipeline, and direction is backward. Say nextStage preserves all the paths but renamed "a" to * "b"; we would return a mapping b-->a. */ -boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& pathsOfInterest, +boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>& pathsOfInterest, const DocumentSource& stage, const Direction& traversalDir); /** @@ -72,7 +72,7 @@ boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& paths boost::optional<StringMap<std::string>> renamedPaths( Pipeline::SourceContainer::const_iterator start, Pipeline::SourceContainer::const_iterator end, - const OrderedPathSet& pathsOfInterest, + const std::set<std::string>& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none); @@ -89,7 +89,7 @@ boost::optional<StringMap<std::string>> renamedPaths( boost::optional<StringMap<std::string>> renamedPaths( Pipeline::SourceContainer::const_reverse_iterator start, Pipeline::SourceContainer::const_reverse_iterator end, - const OrderedPathSet& pathsOfInterest, + const std::set<std::string>& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none); @@ -104,7 +104,7 @@ boost::optional<StringMap<std::string>> renamedPaths( std::pair<Pipeline::SourceContainer::const_iterator, StringMap<std::string>> findLongestViablePrefixPreservingPaths(Pipeline::SourceContainer::const_iterator start, Pipeline::SourceContainer::const_iterator end, - const OrderedPathSet& pathsOfInterest, + const std::set<std::string>& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none); @@ -115,9 +115,10 @@ findLongestViablePrefixPreservingPaths(Pipeline::SourceContainer::const_iterator * For example, extractModifiedDependencies({'a', 'b', 'c.d', 'e'}, {'a', 'b.c', c'}) returns * {'b', 'e'}, since 'b' and 'e' are not preserved (only 'b.c' is preserved). */ -OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies, - const OrderedPathSet& preservedPaths); +std::set<std::string> extractModifiedDependencies(const std::set<std::string>& dependencies, + const std::set<std::string>& preservedPaths); -bool pathSetContainsOverlappingPath(const OrderedPathSet& paths, const std::string& targetPath); +bool pathSetContainsOverlappingPath(const std::set<std::string>& paths, + const std::string& targetPath); } // namespace mongo::semantic_analysis diff --git a/src/mongo/db/pipeline/semantic_analysis_test.cpp b/src/mongo/db/pipeline/semantic_analysis_test.cpp index 0b6e4d224bf..ca95d9098a2 100644 --- a/src/mongo/db/pipeline/semantic_analysis_test.cpp +++ b/src/mongo/db/pipeline/semantic_analysis_test.cpp @@ -54,7 +54,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // Pretend this stage simply renames the "a" field to be "b", leaving the value of "a" the // same. This would be the equivalent of an {$addFields: {b: "$a"}}. - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"b", "a"}}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"b", "a"}}}; } }; @@ -132,7 +132,9 @@ public: : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{"e", "f", "g"}, {{"d", "c"}}}; + return {GetModPathsReturn::Type::kAllExcept, + std::set<std::string>{"e", "f", "g"}, + {{"d", "c"}}}; } }; @@ -194,7 +196,7 @@ public: : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{"f.g"}, {{"e", "c.d"}}}; + return {GetModPathsReturn::Type::kAllExcept, std::set<std::string>{"f.g"}, {{"e", "c.d"}}}; } }; @@ -293,7 +295,7 @@ public: : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{"c.d"}, {{"x.y", "a"}}}; + return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{"c.d"}, {{"x.y", "a"}}}; } }; @@ -384,7 +386,7 @@ public: ModifiesAllPaths(const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; } }; @@ -413,7 +415,7 @@ public: ModificationsUnknown(const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet{}, {}}; + return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>{}, {}}; } }; diff --git a/src/mongo/db/pipeline/sharded_agg_helpers.cpp b/src/mongo/db/pipeline/sharded_agg_helpers.cpp index 2fe2307b0a6..9a7b4df2242 100644 --- a/src/mongo/db/pipeline/sharded_agg_helpers.cpp +++ b/src/mongo/db/pipeline/sharded_agg_helpers.cpp @@ -61,15 +61,12 @@ #include "mongo/s/query/cluster_query_knobs_gen.h" #include "mongo/s/query/document_source_merge_cursors.h" #include "mongo/s/query/establish_cursors.h" -#include "mongo/s/router_role.h" +#include "mongo/s/router.h" #include "mongo/s/stale_exception.h" #include "mongo/s/transaction_router.h" #include "mongo/util/fail_point.h" #include "mongo/util/visit_helper.h" -#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery - - namespace mongo { namespace sharded_agg_helpers { namespace { @@ -81,7 +78,7 @@ MONGO_FAIL_POINT_DEFINE(shardedAggregateHangBeforeEstablishingShardCursors); * {aggregate: "myCollection", pipeline: [], ...}, * * produces the corresponding explain command: - * {explain: {aggregate: "myCollection", pipeline: [], ...}, $queryOptions: {...}, verbosity: ...} + * {explain: {aggregate: "myCollection", pipline: [], ...}, $queryOptions: {...}, verbosity: ...} */ Document wrapAggAsExplain(Document aggregateCommand, ExplainOptions::Verbosity verbosity) { MutableDocument explainCommandBuilder; @@ -103,13 +100,10 @@ Document wrapAggAsExplain(Document aggregateCommand, ExplainOptions::Verbosity v } /** - * Open a $changeStream cursor on the 'config.shards' collection to watch for new shards. The - * 'generateV2ResumeTokens' parameter requests the change stream cursor to generate v2 resume - * tokens. + * Open a $changeStream cursor on the 'config.shards' collection to watch for new shards. */ RemoteCursor openChangeStreamNewShardMonitor(const boost::intrusive_ptr<ExpressionContext>& expCtx, - Timestamp startMonitoringAtTime, - bool generateV2ResumeTokens) { + Timestamp startMonitoringAtTime) { const auto& configShard = Grid::get(expCtx->opCtx)->shardRegistry()->getConfigShard(); // Pipeline: {$changeStream: {startAtOperationTime: [now], allowToRunOnConfigDB: true}} AggregateCommandRequest aggReq( @@ -121,11 +115,9 @@ RemoteCursor openChangeStreamNewShardMonitor(const boost::intrusive_ptr<Expressi aggReq.setFromMongos(true); aggReq.setNeedsMerge(true); - // If on mongos, propagate the '$_generateV2ResumeTokens' value to the new shard monitor change - // stream request. // TODO SERVER-65370: from 6.1 onwards, we will default to v2 and this block should be removed. if (isMongos()) { - aggReq.setGenerateV2ResumeTokens(generateV2ResumeTokens); + aggReq.setGenerateV2ResumeTokens(expCtx->changeStreamTokenVersion == 2); } SimpleCursorOptions cursor; @@ -196,12 +188,11 @@ BSONObj genericTransformForShards(MutableDocument&& cmdForShards, std::vector<RemoteCursor> establishShardCursors(OperationContext* opCtx, std::shared_ptr<executor::TaskExecutor> executor, const NamespaceString& nss, - bool mustRunOnAllShards, + bool mustRunOnAll, boost::optional<ChunkManager>& cm, const std::set<ShardId>& shardIds, const BSONObj& cmdObj, - const ReadPreferenceSetting& readPref, - bool targetEveryShardServer) { + const ReadPreferenceSetting& readPref) { LOGV2_DEBUG(20904, 1, "Dispatching command {cmdObj} to establish cursors on shards", @@ -210,27 +201,9 @@ std::vector<RemoteCursor> establishShardCursors(OperationContext* opCtx, std::vector<std::pair<ShardId, BSONObj>> requests; // If we don't need to run on all shards, then we should always have a valid routing table. - invariant(cm || mustRunOnAllShards); - - if (targetEveryShardServer) { - uassert(7355703, - "Cannot target all hosts if the pipeline is not run on all shards.", - mustRunOnAllShards); - if (MONGO_unlikely(shardedAggregateHangBeforeEstablishingShardCursors.shouldFail())) { - LOGV2( - 7355704, - "shardedAggregateHangBeforeEstablishingShardCursors fail point enabled. Blocking " - "until fail point is disabled."); - while ( - MONGO_unlikely(shardedAggregateHangBeforeEstablishingShardCursors.shouldFail())) { - sleepsecs(1); - } - } - return establishCursorsOnAllHosts( - opCtx, std::move(executor), nss, shardIds, cmdObj, false, getDesiredRetryPolicy(opCtx)); - } + invariant(cm || mustRunOnAll); - if (mustRunOnAllShards) { + if (mustRunOnAll) { // The pipeline contains a stage which must be run on all shards. Skip versioning and // enqueue the raw command objects. for (const auto& shardId : shardIds) { @@ -441,7 +414,7 @@ void moveEligibleStreamingStagesBeforeSortOnShards(Pipeline* shardPipe, // Expected last stage on the shards to be a $sort. return; } - auto sortPaths = sortPattern.getFieldNames<OrderedPathSet>(); + auto sortPaths = sortPattern.getFieldNames<std::set<std::string>>(); auto firstMergeStage = mergePipe->getSources().cbegin(); std::function<bool(DocumentSource*)> distributedPlanLogicCallback = [](DocumentSource* stage) { return !static_cast<bool>(stage->distributedPlanLogic()); @@ -588,7 +561,7 @@ void limitFieldsSentFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe) } bool stageCanRunInParallel(const boost::intrusive_ptr<DocumentSource>& stage, - const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) { + const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) { if (stage->distributedPlanLogic()) { return stage->canRunInParallelBeforeWriteStage(nameOfShardKeyFieldsUponEntryToStage); } else { @@ -625,7 +598,7 @@ BSONObj buildNewKeyPattern(const ShardKeyPattern& shardKey, StringMap<std::strin } StringMap<std::string> computeShardKeyRenameMap(const Pipeline* mergePipeline, - OrderedPathSet&& pathsOfShardKey) { + std::set<std::string>&& pathsOfShardKey) { auto traversalStart = mergePipeline->getSources().crbegin(); auto traversalEnd = mergePipeline->getSources().crend(); const auto leadingGroup = @@ -653,7 +626,7 @@ StringMap<std::string> computeShardKeyRenameMap(const Pipeline* mergePipeline, * * Purposefully takes 'shardKeyPaths' by value so that it can be modified throughout. */ -bool anyStageModifiesShardKeyOrNeedsMerge(OrderedPathSet shardKeyPaths, +bool anyStageModifiesShardKeyOrNeedsMerge(std::set<std::string> shardKeyPaths, const Pipeline* mergePipeline) { const auto& stages = mergePipeline->getSources(); for (auto it = stages.crbegin(); it != stages.crend(); ++it) { @@ -680,7 +653,7 @@ boost::optional<ShardedExchangePolicy> walkPipelineBackwardsTrackingShardKey( OperationContext* opCtx, const Pipeline* mergePipeline, const ChunkManager& chunkManager) { const ShardKeyPattern& shardKey = chunkManager.getShardKeyPattern(); - OrderedPathSet shardKeyPaths; + std::set<std::string> shardKeyPaths; for (auto&& path : shardKey.getKeyPatternFields()) { shardKeyPaths.emplace(path->dottedField().toString()); } @@ -796,16 +769,10 @@ std::unique_ptr<Pipeline, PipelineDeleter> targetShardsAndAddMergeCursors( LiteParsedPipeline liteParsedPipeline(aggRequest); auto hasChangeStream = liteParsedPipeline.hasChangeStream(); - auto startsWithDocuments = liteParsedPipeline.startsWithDocuments(); auto shardDispatchResults = dispatchShardPipeline(aggregation_request_helper::serializeToCommandDoc(aggRequest), hasChangeStream, - startsWithDocuments, std::move(pipeline), - // Even if the overall operation is an explain, callers of this - // function always intend to actually execute a regular agg command - // and merge the results with $mergeCursors. - boost::none /*explain*/, shardTargetingPolicy, std::move(readConcern)); @@ -988,7 +955,6 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont const SplitPipeline& splitPipeline, const boost::optional<ShardedExchangePolicy> exchangeSpec, bool needsMerge, - boost::optional<ExplainOptions::Verbosity> explain, boost::optional<BSONObj> readConcern) { // Create the command for the shards. MutableDocument targetedCmd(serializedCommand); @@ -1020,22 +986,28 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont targetedCmd[AggregateCommandRequest::kExchangeFieldName] = exchangeSpec ? Value(exchangeSpec->exchangeSpec.toBSON()) : Value(); - auto shardCommand = genericTransformForShards( - std::move(targetedCmd), expCtx, explain, expCtx->getCollatorBSON(), std::move(readConcern)); + auto shardCommand = genericTransformForShards(std::move(targetedCmd), + expCtx, + expCtx->explain, + expCtx->getCollatorBSON(), + std::move(readConcern)); // Apply RW concern to the final shard command. return applyReadWriteConcern(expCtx->opCtx, - true, /* appendRC */ - !explain, /* appendWC */ + true, /* appendRC */ + !expCtx->explain, /* appendWC */ shardCommand); } +/** + * Targets shards for the pipeline and returns a struct with the remote cursors or results, and + * the pipeline that will need to be executed to merge the results from the remotes. If a stale + * shard version is encountered, refreshes the routing table and tries again. + */ DispatchShardPipelineResults dispatchShardPipeline( Document serializedCommand, bool hasChangeStream, - bool startsWithDocuments, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, - boost::optional<ExplainOptions::Verbosity> explain, ShardTargetingPolicy shardTargetingPolicy, boost::optional<BSONObj> readConcern) { auto expCtx = pipeline->getContext(); @@ -1079,17 +1051,15 @@ DispatchShardPipelineResults dispatchShardPipeline( : expCtx->getCollatorBSON(); // Determine whether we can run the entire aggregation on a single shard. - const bool mustRunOnAllShards = - checkIfMustRunOnAllShards(expCtx->ns, hasChangeStream, startsWithDocuments); + const bool mustRunOnAll = mustRunOnAllShards(expCtx->ns, hasChangeStream); std::set<ShardId> shardIds = getTargetedShards( - expCtx, mustRunOnAllShards, executionNsRoutingInfo, shardQuery, shardTargetingCollation); + expCtx, mustRunOnAll, executionNsRoutingInfo, shardQuery, shardTargetingCollation); - bool targetEveryShardServer = pipeline->needsAllShardServers(); // Don't need to split the pipeline if we are only targeting a single shard, unless: // - There is a stage that needs to be run on the primary shard and the single target shard // is not the primary. // - The pipeline contains one or more stages which must always merge on mongoS. - const bool needsSplit = (shardIds.size() > 1u || needsMongosMerge || targetEveryShardServer || + const bool needsSplit = (shardIds.size() > 1u || needsMongosMerge || (needsPrimaryShardMerge && executionNsRoutingInfo && *(shardIds.begin()) != executionNsRoutingInfo->dbPrimary())); @@ -1133,11 +1103,10 @@ DispatchShardPipelineResults dispatchShardPipeline( *splitPipelines, exchangeSpec, true /* needsMerge */, - explain, std::move(readConcern)) : createPassthroughCommandForShard(expCtx, serializedCommand, - explain, + expCtx->explain, pipeline.get(), expCtx->getCollatorBSON(), std::move(readConcern), @@ -1158,11 +1127,8 @@ DispatchShardPipelineResults dispatchShardPipeline( if (hasChangeStream) { Grid::get(opCtx)->shardRegistry()->reload(opCtx); // Rebuild the set of shards as the shard registry might have changed. - shardIds = getTargetedShards(expCtx, - mustRunOnAllShards, - executionNsRoutingInfo, - shardQuery, - shardTargetingCollation); + shardIds = getTargetedShards( + expCtx, mustRunOnAll, executionNsRoutingInfo, shardQuery, shardTargetingCollation); } // If there were no shards when we began execution, we wouldn't have run this aggregation in the @@ -1173,8 +1139,8 @@ DispatchShardPipelineResults dispatchShardPipeline( shardIds.size() > 0); // Explain does not produce a cursor, so instead we scatter-gather commands to the shards. - if (explain) { - if (mustRunOnAllShards) { + if (expCtx->explain) { + if (mustRunOnAll) { // Some stages (such as $currentOp) need to be broadcast to all shards, and // should not participate in the shard version protocol. shardResults = @@ -1188,7 +1154,7 @@ DispatchShardPipelineResults dispatchShardPipeline( // shards, and should participate in the shard version protocol. invariant(executionNsRoutingInfo); shardResults = - scatterGatherVersionedTargetByRoutingTable(expCtx, + scatterGatherVersionedTargetByRoutingTable(opCtx, expCtx->ns.db(), expCtx->ns, *executionNsRoutingInfo, @@ -1203,14 +1169,13 @@ DispatchShardPipelineResults dispatchShardPipeline( cursors = establishShardCursors(opCtx, expCtx->mongoProcessInterface->taskExecutor, expCtx->ns, - mustRunOnAllShards, + mustRunOnAll, executionNsRoutingInfo, shardIds, targetedCommand, - ReadPreferenceSetting::get(opCtx), - targetEveryShardServer); + ReadPreferenceSetting::get(opCtx)); - } catch (const ExceptionFor<ErrorCodes::StaleConfig>& e) { + } catch (const StaleConfigException& e) { // Check to see if the command failed because of a stale shard version or something // else. auto staleInfo = e.extraInfo<StaleConfigInfo>(); @@ -1239,12 +1204,7 @@ DispatchShardPipelineResults dispatchShardPipeline( // For $changeStream, we must open an extra cursor on the 'config.shards' collection, so // that we can monitor for the addition of new shards inline with real events. if (hasChangeStream && expCtx->ns.db() != ShardType::ConfigNS.db()) { - auto generateV2ResumeTokens = - serializedCommand[AggregateCommandRequest::kGenerateV2ResumeTokensFieldName]; - cursors.emplace_back(openChangeStreamNewShardMonitor( - expCtx, - shardRegistryReloadTime, - !generateV2ResumeTokens.missing() && generateV2ResumeTokens.getBool())); + cursors.emplace_back(openChangeStreamNewShardMonitor(expCtx, shardRegistryReloadTime)); } } @@ -1313,7 +1273,7 @@ AsyncResultsMergerParams buildArmParams(boost::intrusive_ptr<ExpressionContext> return armParams; } -// Anonymous namespace for helpers of partitionCursorsAndAddMergeCursors. +// Anonnymous namespace for helpers of partitionCursorsAndAddMergeCursors. namespace { /** * Given the owned cursors vector, partitions the cursors into either one or two vectors. If @@ -1435,11 +1395,9 @@ Status appendExplainResults(DispatchShardPipelineResults&& dispatchResults, MutableDocument pipelinesDoc; // We specify "queryPlanner" verbosity when building the output for "shardsPart" because // execution stats are reported by each shard individually. - auto opts = SerializationOptions{}; - opts.verbosity = boost::make_optional(ExplainOptions::Verbosity::kQueryPlanner); - pipelinesDoc.addField( - "shardsPart", - Value(dispatchResults.splitPipeline->shardsPipeline->writeExplainOps(opts))); + pipelinesDoc.addField("shardsPart", + Value(dispatchResults.splitPipeline->shardsPipeline->writeExplainOps( + ExplainOptions::Verbosity::kQueryPlanner))); if (dispatchResults.exchangeSpec) { BSONObjBuilder bob; dispatchResults.exchangeSpec->exchangeSpec.serialize(&bob); @@ -1448,7 +1406,7 @@ Status appendExplainResults(DispatchShardPipelineResults&& dispatchResults, } // We specify "queryPlanner" verbosity because execution stats are not currently // supported when building the output for "mergerPart". - auto explainOps = mergePipeline->writeExplainOps(opts); + auto explainOps = mergePipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); // No cursors to remote shards are established for an explain, and the $mergeCursors // aggregation stage which is normally built in addMergeCursorsSource() requires vectors of @@ -1476,11 +1434,11 @@ Status appendExplainResults(DispatchShardPipelineResults&& dispatchResults, BSONObjBuilder shardExplains(result->subobjStart("shards")); for (const auto& shardResult : dispatchResults.remoteExplainOutput) { + invariant(shardResult.shardHostAndPort); + uassertStatusOK(shardResult.swResponse.getStatus()); uassertStatusOK(getStatusFromCommandResult(shardResult.swResponse.getValue().data)); - invariant(shardResult.shardHostAndPort); - auto shardId = shardResult.shardId.toString(); const auto& data = shardResult.swResponse.getValue().data; BSONObjBuilder explain(shardExplains.subobjStart(shardId)); @@ -1528,13 +1486,10 @@ BSONObj targetShardsForExplain(Pipeline* ownedPipeline) { AggregateCommandRequest aggRequest(expCtx->ns, rawStages); LiteParsedPipeline liteParsedPipeline(aggRequest); auto hasChangeStream = liteParsedPipeline.hasChangeStream(); - auto startsWithDocuments = liteParsedPipeline.startsWithDocuments(); auto shardDispatchResults = dispatchShardPipeline(aggregation_request_helper::serializeToCommandDoc(aggRequest), hasChangeStream, - startsWithDocuments, - std::move(pipeline), - expCtx->explain); + std::move(pipeline)); BSONObjBuilder explainBuilder; auto appendStatus = appendExplainResults(std::move(shardDispatchResults), expCtx, &explainBuilder); @@ -1568,13 +1523,11 @@ Shard::RetryPolicy getDesiredRetryPolicy(OperationContext* opCtx) { return Shard::RetryPolicy::kIdempotent; } -bool checkIfMustRunOnAllShards(const NamespaceString& nss, - bool hasChangeStream, - bool startsWithDocuments) { +bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream) { // The following aggregations must be routed to all shards: // - Any collectionless aggregation, such as non-localOps $currentOp. // - Any aggregation which begins with a $changeStream stage. - return !startsWithDocuments && (nss.isCollectionlessAggregateNS() || hasChangeStream); + return nss.isCollectionlessAggregateNS() || hasChangeStream; } std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( @@ -1593,7 +1546,7 @@ std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( // which needs to actually get a cursor attached or not. const auto* firstStage = *hasFirstStage; invariant(!dynamic_cast<const DocumentSourceMergeCursors*>(firstStage)); - // Here we check the hostRequirement because there is at least one stage ($indexStats) which + // Here we check the hostRequirment because there is at least one stage ($indexStats) which // does not require input data, but is still expected to fan out and contact remote shards // nonetheless. if (auto constraints = firstStage->constraints(); !constraints.requiresInputDocSource && @@ -1630,14 +1583,11 @@ std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( [&](OperationContext* opCtx, const ChunkManager& cm) { auto pipelineToTarget = pipeline->clone(); - if (!cm.isSharded() && expCtx->ns != NamespaceString::kConfigsvrCollectionsNamespace) { + if (!cm.isSharded()) { // If the collection is unsharded and we are on the primary, we should be able to // do a local read. The primary may be moved right after the primary shard check, // but the local read path will do a db version check before it establishes a cursor // to catch this case and ensure we fail to read locally. - // There is the case where we are in config.collections (collection unsharded) and - // we want to broadcast to all shards. In this case we don't want to do a local read - // and we must target the config servers. try { auto expectUnshardedCollection( expCtx->mongoProcessInterface->expectUnshardedCollectionInScope( diff --git a/src/mongo/db/pipeline/sharded_agg_helpers.h b/src/mongo/db/pipeline/sharded_agg_helpers.h index c23ad646bbd..5f30f6f6a93 100644 --- a/src/mongo/db/pipeline/sharded_agg_helpers.h +++ b/src/mongo/db/pipeline/sharded_agg_helpers.h @@ -120,26 +120,11 @@ SplitPipeline splitPipeline(std::unique_ptr<Pipeline, PipelineDeleter> pipeline) * Targets shards for the pipeline and returns a struct with the remote cursors or results, and * the pipeline that will need to be executed to merge the results from the remotes. If a stale * shard version is encountered, refreshes the routing table and tries again. - * - * Although the 'pipeline' has an 'ExpressionContext' which indicates whether this operation is an - * explain (and if it is an explain what the verbosity is), the caller must explicitly indicate - * whether it wishes to dispatch a regular aggregate command or an explain command using the - * explicit 'explain' parameter. The reason for this is that in some contexts, the caller wishes to - * dispatch a regular agg command rather than an explain command even if the top-level operation is - * an explain. Consider the example of an explain that contains a stage like this: - * - * {$unionWith: {coll: "innerShardedColl", pipeline: <sub-pipeline>}} - * - * The explain works by first executing the inner and outer subpipelines in order to gather runtime - * statistics. While dispatching the inner pipeline, we must dispatch it not as an explain but as a - * regular agg command so that the runtime stats are accurate. */ DispatchShardPipelineResults dispatchShardPipeline( Document serializedCommand, bool hasChangeStream, - bool startsWithDocuments, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, - boost::optional<ExplainOptions::Verbosity> explain, ShardTargetingPolicy shardTargetingPolicy = ShardTargetingPolicy::kAllowed, boost::optional<BSONObj> readConcern = boost::none); @@ -157,7 +142,6 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont const SplitPipeline& splitPipeline, boost::optional<ShardedExchangePolicy> exchangeSpec, bool needsMerge, - boost::optional<ExplainOptions::Verbosity> explain, boost::optional<BSONObj> readConcern = boost::none); /** @@ -195,9 +179,7 @@ StatusWith<ChunkManager> getExecutionNsRoutingInfo(OperationContext* opCtx, /** * Returns true if an aggregation over 'nss' must run on all shards. */ -bool checkIfMustRunOnAllShards(const NamespaceString& nss, - bool hasChangeStream, - bool startsWithDocuments); +bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream); /** * Retrieves the desired retry policy based on whether the default writeConcern is set on 'opCtx'. @@ -220,11 +202,6 @@ std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( * beginning with that DocumentSourceMergeCursors stage. Note that one of the 'remote' cursors might * be this node itself. * - * Even if the ExpressionContext indicates that this operation is explain, this function still - * dispatches the pipeline as a non-explain, since it must open cursors on the remote nodes and - * merge them with a $mergeCursors. If the caller's intent is to dispatch an explain command, it - * must use a different helper. - * * Use the AggregateCommandRequest alternative for 'targetRequest' to explicitly specify command * options (e.g. read concern) to the shards when establishing remote cursors. Note that doing so * incurs the cost of parsing the pipeline. diff --git a/src/mongo/db/pipeline/sharded_union_test.cpp b/src/mongo/db/pipeline/sharded_union_test.cpp index 71336e4dfb9..79863fc7f14 100644 --- a/src/mongo/db/pipeline/sharded_union_test.cpp +++ b/src/mongo/db/pipeline/sharded_union_test.cpp @@ -35,7 +35,6 @@ #include "mongo/db/pipeline/document_source_queue.h" #include "mongo/db/pipeline/document_source_union_with.h" #include "mongo/db/pipeline/process_interface/shardsvr_process_interface.h" -#include "mongo/db/query/cursor_response.h" #include "mongo/db/repl/read_concern_args.h" #include "mongo/db/views/resolved_view.h" #include "mongo/s/query/sharded_agg_test_fixture.h" diff --git a/src/mongo/db/pipeline/skip_and_limit.cpp b/src/mongo/db/pipeline/skip_and_limit.cpp index 0f9f7d3aca7..e9e7e9772ce 100644 --- a/src/mongo/db/pipeline/skip_and_limit.cpp +++ b/src/mongo/db/pipeline/skip_and_limit.cpp @@ -82,21 +82,8 @@ Pipeline::SourceContainer::iterator eraseAndStich(Pipeline::SourceContainer::ite } // namespace -/** - * If there are any $limit stages that could be logically swapped forward to the position of the - * pipeline pointed to by 'itr' without changing the meaning of the query, removes these $limit - * stages from the Pipeline and returns the resulting limit. A single limit value is computed by - * taking the minimum after swapping each individual $limit stage forward. - * - * This method also implements the ability to swap a $limit before a $skip, by adding the value of - * the $skip to the value of the $limit. - * - * If shouldModifyPipeline is false, this method does not swap any stages but rather just returns - * the single limit value described above. - */ -boost::optional<long long> extractLimitForPushdownHelper(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container, - bool shouldModifyPipeline) { +boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container) { int64_t skipSum = 0; boost::optional<long long> minLimit; while (itr != container->end()) { @@ -117,11 +104,7 @@ boost::optional<long long> extractLimitForPushdownHelper(Pipeline::SourceContain minLimit = std::min(static_cast<long long>(safeSum), *minLimit); } - if (shouldModifyPipeline) { - itr = eraseAndStich(itr, container); - } else { - ++itr; - } + itr = eraseAndStich(itr, container); } else if (!nextStage->constraints().canSwapWithSkippingOrLimitingStage) { break; } else { @@ -132,16 +115,6 @@ boost::optional<long long> extractLimitForPushdownHelper(Pipeline::SourceContain return minLimit; } -boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container) { - return extractLimitForPushdownHelper(itr, container, true /* shouldModifyPipeline */); -} - -boost::optional<long long> getUserLimit(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container) { - return extractLimitForPushdownHelper(itr, container, false /* shouldModifyPipeline */); -} - boost::optional<long long> extractSkipForPushdown(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { boost::optional<long long> skipSum; diff --git a/src/mongo/db/pipeline/skip_and_limit.h b/src/mongo/db/pipeline/skip_and_limit.h index 07023afa1d9..2a3ecec5982 100644 --- a/src/mongo/db/pipeline/skip_and_limit.h +++ b/src/mongo/db/pipeline/skip_and_limit.h @@ -89,13 +89,6 @@ boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::it Pipeline::SourceContainer* container); /** - * This is similar to extractLimitForPushdown, except that it should be used when the caller does - * not want to modify the pipeline but still obtain the calculated limit value of the query. - */ -boost::optional<long long> getUserLimit(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container); - -/** * If there are any $skip stages that could be logically swapped forward to the position of the * pipeline pointed to by 'itr' without changing the meaning of the query, removes these $skip * stages from the Pipeline and returns the resulting skip. A single skip value is computed by diff --git a/src/mongo/db/pipeline/stage_constraints.h b/src/mongo/db/pipeline/stage_constraints.h index 456f3cfc65f..17537069d51 100644 --- a/src/mongo/db/pipeline/stage_constraints.h +++ b/src/mongo/db/pipeline/stage_constraints.h @@ -55,10 +55,11 @@ struct StageConstraints { enum class PositionRequirement { kNone, kFirst, - kLast, - // Stages with 'kCustom' requirement must also implement the 'validatePipelinePosition()' - // method which is called during pipeline validation. - kCustom + // User can specify this stage anywhere, as long as the system can move the stage to be + // first. If pipeline optimization is disabled, then the stage must be first prior to + // optimization. + kFirstAfterOptimization, + kLast }; /** @@ -77,10 +78,6 @@ struct StageConstraints { kAnyShard, // Indicates that the stage can only run on mongoS. kMongoS, - // Indicates that the stage should run on all data-bearing nodes, primary and seconday, for - // the participating shards. This is useful for stages like $currentOp which generate - // node-specific metadata. - kAllShardServers, }; /** @@ -96,15 +93,10 @@ struct StageConstraints { /** * A ChangeStreamRequirement determines whether a particular stage is itself a ChangeStream - * stage, whether it is allowed to exist in a $changeStream pipeline, or whether it can only - * exist in a change stream pipeline. + * stage, whether it is allowed to exist in a $changeStream pipeline, or whether it is + * denylisted from $changeStream. */ - enum class ChangeStreamRequirement { - kChangeStreamStage, // This stage is an actual change stream stage. - kAllowlist, // This stage is permitted in a change stream pipeline. - kDenylist, // This stage is banned from change stream pipelines. - kRequiresChangeStream // This stage is only allowed in a change stream pipeline. - }; + enum class ChangeStreamRequirement { kChangeStreamStage, kAllowlist, kDenylist }; /** * A FacetRequirement indicates whether this stage may be used within a $facet pipeline. @@ -194,8 +186,7 @@ struct StageConstraints { // shard, since it needs to be able to run on mongoS in a cluster. invariant(!(changeStreamRequirement == ChangeStreamRequirement::kAllowlist && (hostRequirement == HostTypeRequirement::kAnyShard || - hostRequirement == HostTypeRequirement::kPrimaryShard || - hostRequirement == HostTypeRequirement::kAllShardServers))); + hostRequirement == HostTypeRequirement::kPrimaryShard))); // A stage which is allowlisted for $changeStream cannot have a position requirement. invariant(!(changeStreamRequirement == ChangeStreamRequirement::kAllowlist && @@ -214,13 +205,6 @@ struct StageConstraints { if (diskRequirement == DiskUseRequirement::kWritesPersistentData) { invariant(!isAllowedInTransaction()); } - - tassert( - 7355706, - "Stage can only broadcast to all shard servers if it must be the first stage in the " - "pipeline.", - hostRequirement != HostTypeRequirement::kAllShardServers || - (requiredPosition == PositionRequirement::kFirst)); } /** @@ -265,13 +249,6 @@ struct StageConstraints { } /** - * True if this stage must run in a pipeline which starts with $changeStream. - */ - bool requiresChangeStream() const { - return changeStreamRequirement == ChangeStreamRequirement::kRequiresChangeStream; - } - - /** * Returns true if this stage is legal when the readConcern level is "snapshot" or when this * aggregation is being run within a multi-document transaction. */ @@ -359,22 +336,13 @@ struct StageConstraints { // documents because our implementation of $sample shuffles the order bool canSwapWithSkippingOrLimitingStage = false; - // If true, then any stage of kind 'DocumentSourceSingleDocumentTransformation' or $redact can - // be swapped ahead of this stage. - bool canSwapWithSingleDocTransformOrRedact = false; + // If true, then any stage of kind 'DocumentSourceSingleDocumentTransformation' can be swapped + // ahead of this stage. + bool canSwapWithSingleDocTransform = false; - // Indicates that a stage is allowed within a pipeline-style update. + // Indicates that a stage is allowed within a pipeline-stlye update. bool isAllowedWithinUpdatePipeline = false; - // Indicates that a stage requires idempotency guarantee and needs to check for existence of a - // field before performing a diff insert. - bool checkExistenceForDiffInsertOperations = false; - - // If true, then this stage may only appear in the pipeline once, though it can appear at an - // arbitrary position. It is not necessary to consider this for stages which have a strict - // PositionRequirement, since the presence of a second stage will violate that constraint. - bool canAppearOnlyOnceInPipeline = false; - // Indicates that a stage does not modify anything to do with a sort and can be done before a // following merge sort. bool preservesOrderAndMetadata = false; @@ -390,8 +358,6 @@ struct StageConstraints { isIndependentOfAnyCollection == other.isIndependentOfAnyCollection && canSwapWithMatch == other.canSwapWithMatch && canSwapWithSkippingOrLimitingStage == other.canSwapWithSkippingOrLimitingStage && - canSwapWithSingleDocTransformOrRedact == other.canSwapWithSingleDocTransformOrRedact && - canAppearOnlyOnceInPipeline == other.canAppearOnlyOnceInPipeline && isAllowedWithinUpdatePipeline == other.isAllowedWithinUpdatePipeline && unionRequirement == other.unionRequirement && preservesOrderAndMetadata == other.preservesOrderAndMetadata; diff --git a/src/mongo/db/pipeline/storage_stats_spec.idl b/src/mongo/db/pipeline/storage_stats_spec.idl index c0cc0a4ce6a..8052f09de51 100644 --- a/src/mongo/db/pipeline/storage_stats_spec.idl +++ b/src/mongo/db/pipeline/storage_stats_spec.idl @@ -36,23 +36,18 @@ structs: StorageStatsSpec: description: Represents the 'storageStats' argument to the $collStats stage. strict: false - query_shape_component: true fields: scale: description: A number to use as a scaling factor applied to reported metrics. type: safeInt optional: true validator: { gte: 1 } - query_shape: literal verbose: type: optionalBool default: false - query_shape: parameter waitForLock: type: optionalBool default: true - query_shape: parameter numericOnly: type: optionalBool default: false - query_shape: parameter diff --git a/src/mongo/db/pipeline/transformer_interface.h b/src/mongo/db/pipeline/transformer_interface.h index 6057a2e328c..6be98af2570 100644 --- a/src/mongo/db/pipeline/transformer_interface.h +++ b/src/mongo/db/pipeline/transformer_interface.h @@ -65,8 +65,8 @@ public: * Returns a document describing this transformation. For example, this function will return * {_id: 0, x: 1} for the stage parsed from {$project: {_id: 0, x: 1}}. */ - virtual Document serializeTransformation(boost::optional<ExplainOptions::Verbosity> explain, - const SerializationOptions& options = {}) const = 0; + virtual Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const = 0; /** * Method used by inclusion and add fields projecton executors to extract computed projections diff --git a/src/mongo/db/pipeline/variables.cpp b/src/mongo/db/pipeline/variables.cpp index 279b8630ce6..26793962efd 100644 --- a/src/mongo/db/pipeline/variables.cpp +++ b/src/mongo/db/pipeline/variables.cpp @@ -40,21 +40,6 @@ namespace mongo { -namespace { - -// We need to be careful when serializing values, e.g. to populate the 'let' parameter of a command -// to be sent over the wire. First, missing values should be serialied as $$REMOVE, otherwise they -// might be incorrectly omitted or serialized as empty objects ({}). Also, we should wrap values in -// $literal to avoid a scenario like the following: suppose we had a user-defined 'let' specified as -// {let: {a: {$literal: "$notAFieldName"}}}. On mongos, this will be evaluated to the string -// "$notAFieldName". When we serialize it again for the shard commands, it must appear as {$literal: -// "$notAFieldName"}, not simply "$notAFieldName", since the latter will be treated as a field name -// by mongods. -Value serializeValue(Value val) { - return val.missing() ? Value("$$REMOVE"_sd) : Value(DOC("$literal" << val)); -} -} // namespace - using namespace std::string_literals; constexpr Variables::Id Variables::kRootId; @@ -345,8 +330,7 @@ LegacyRuntimeConstants Variables::transitionalExtractRuntimeConstants() const { } Variables::Id VariablesParseState::defineVariable(StringData name) { - // Caller should have validated before hand by using - // variableValidation::validateNameForUserWrite. + // Caller should have validated before hand by using variableValidationvalidateNameForUserWrite. massert(17275, "Can't redefine a non-user-writable variable", Variables::kBuiltinVarNameToId.find(name) == Variables::kBuiltinVarNameToId.end()); @@ -390,9 +374,8 @@ std::set<Variables::Id> VariablesParseState::getDefinedVariableIDs() const { BSONObj VariablesParseState::serialize(const Variables& vars) const { auto bob = BSONObjBuilder{}; for (auto&& [var_name, id] : _variables) - if (vars.hasValue(id)) { - bob << var_name << serializeValue(vars.getValue(id)); - } + if (vars.hasValue(id)) + bob << var_name << Value(DOC("$literal" << vars.getValue(id))); // System variables have to be added separately since the variable IDs are reserved and not // allocated like normal variables, and so not present in '_variables'. @@ -404,9 +387,8 @@ std::pair<LegacyRuntimeConstants, BSONObj> VariablesParseState::transitionalComp const Variables& vars) const { auto bob = BSONObjBuilder{}; for (auto&& [var_name, id] : _variables) - if (vars.hasValue(id)) { - bob << var_name << serializeValue(vars.getValue(id)); - } + if (vars.hasValue(id)) + bob << var_name << Value(DOC("$literal" << vars.getValue(id))); return {vars.transitionalExtractRuntimeConstants(), bob.obj()}; } diff --git a/src/mongo/db/pipeline/variables.h b/src/mongo/db/pipeline/variables.h index d69d9d46831..d7a79c523b9 100644 --- a/src/mongo/db/pipeline/variables.h +++ b/src/mongo/db/pipeline/variables.h @@ -190,13 +190,6 @@ public: MONGO_UNREACHABLE_TASSERT(5858104); } - /** - * Return true if the passed-in variable ID belongs to a builtin variable. - */ - static auto isBuiltin(Variables::Id variable) { - return kIdToBuiltinVarName.find(variable) != kIdToBuiltinVarName.end(); - } - private: struct ValueAndState { ValueAndState() = default; diff --git a/src/mongo/db/pipeline/visitors/document_source_visitor.h b/src/mongo/db/pipeline/visitors/document_source_visitor.h index 9358b6267f9..a0158147e38 100644 --- a/src/mongo/db/pipeline/visitors/document_source_visitor.h +++ b/src/mongo/db/pipeline/visitors/document_source_visitor.h @@ -67,7 +67,6 @@ class DocumentSourceSingleDocumentTransformation; class DocumentSourceSkip; class DocumentSourceSort; class DocumentSourceTeeConsumer; -class DocumentSourceQueryStats; class DocumentSourceUnionWith; class DocumentSourceUnwind; @@ -126,7 +125,6 @@ public: virtual void visit(tree_walker::MaybeConstPtr<IsConst, DocumentSourceSkip> source) = 0; virtual void visit(tree_walker::MaybeConstPtr<IsConst, DocumentSourceSort> source) = 0; virtual void visit(tree_walker::MaybeConstPtr<IsConst, DocumentSourceTeeConsumer> source) = 0; - virtual void visit(tree_walker::MaybeConstPtr<IsConst, DocumentSourceQueryStats> source) = 0; virtual void visit(tree_walker::MaybeConstPtr<IsConst, DocumentSourceUnionWith> source) = 0; virtual void visit(tree_walker::MaybeConstPtr<IsConst, DocumentSourceUnwind> source) = 0; }; diff --git a/src/mongo/db/pipeline/visitors/document_source_walker.cpp b/src/mongo/db/pipeline/visitors/document_source_walker.cpp index 5f271814e1c..b0ea004cae9 100644 --- a/src/mongo/db/pipeline/visitors/document_source_walker.cpp +++ b/src/mongo/db/pipeline/visitors/document_source_walker.cpp @@ -54,7 +54,6 @@ #include "mongo/db/pipeline/document_source_operation_metrics.h" #include "mongo/db/pipeline/document_source_out.h" #include "mongo/db/pipeline/document_source_plan_cache_stats.h" -#include "mongo/db/pipeline/document_source_query_stats.h" #include "mongo/db/pipeline/document_source_queue.h" #include "mongo/db/pipeline/document_source_redact.h" #include "mongo/db/pipeline/document_source_sample.h" @@ -127,7 +126,6 @@ void DocumentSourceWalker::walk(const Pipeline& pipeline) { visitHelper<DocumentSourceSingleDocumentTransformation>(ds) || visitHelper<DocumentSourceSkip>(ds) || visitHelper<DocumentSourceSort>(ds) || visitHelper<DocumentSourceTeeConsumer>(ds) || - visitHelper<DocumentSourceQueryStats>(ds) || visitHelper<DocumentSourceUnionWith>(ds) || visitHelper<DocumentSourceUnwind>(ds) // TODO: uncomment after fixing dependency //&& visitHelper<DocumentSourceUpdateOnAddShard>(ds) diff --git a/src/mongo/db/pipeline/window_function/partition_iterator.cpp b/src/mongo/db/pipeline/window_function/partition_iterator.cpp index 83925b862b3..d8992aec28c 100644 --- a/src/mongo/db/pipeline/window_function/partition_iterator.cpp +++ b/src/mongo/db/pipeline/window_function/partition_iterator.cpp @@ -116,8 +116,8 @@ optional<Document> PartitionIterator::operator[](int index) { for (int i = _cache->getHighestIndex(); i < docDesired; i++) { // Pull in document from prior stage. getNextDocument(); - // Check whether the next document is available. - if (isPaused() || _state == IteratorState::kAwaitingAdvanceToNext || + // Check for EOF or the next partition. + if (_state == IteratorState::kAwaitingAdvanceToNext || _state == IteratorState::kAwaitingAdvanceToEOF) { return boost::none; } @@ -163,7 +163,6 @@ PartitionIterator::AdvanceResult PartitionIterator::advanceInternal() { // whether to pull from the prior stage. switch (_state) { case IteratorState::kNotInitialized: - case IteratorState::kPauseExecution: case IteratorState::kIntraPartition: // Pull in the next document and advance the pointer. getNextDocument(); @@ -302,13 +301,8 @@ optional<std::pair<int, int>> PartitionIterator::getEndpointsRangeBased( for (int i = start; (doc = (*this)[i]); ++i) { Value v = (*_sortExpr)->evaluate(*doc, &_expCtx->variables); if (!lessThan(v, threshold)) { - // This is the first doc we've scanned that crossed the threshold, - // so it's the first doc in the window (as long as it's the expected type). - if (hasExpectedType(v)) { - return i; - } else { - return boost::none; - } + // This is the first doc we've scanned that crossed the threshold. + return i; } } // We scanned every document in the partition, and none crossed the @@ -473,20 +467,17 @@ void PartitionIterator::getNextDocument() { return; } - if (getNextRes.isPaused()) { - _state = IteratorState::kPauseExecution; + if (!getNextRes.isAdvanced()) return; - } - tassert(7169100, "getNextResult must have advanced", getNextRes.isAdvanced()); auto doc = getNextRes.releaseDocument(); // Greedily populate the internal document cache to enable easier memory tracking versus // detecting the changing document size during execution of each function. - doc = doc.shred(); + doc.fillCache(); if (_partitionExpr) { - if (!_partitionComparator) { + if (_state == IteratorState::kNotInitialized) { _partitionComparator = std::make_unique<PartitionKeyComparator>(_expCtx, *_partitionExpr, doc); _nextPartitionDoc = std::move(doc); diff --git a/src/mongo/db/pipeline/window_function/partition_iterator.h b/src/mongo/db/pipeline/window_function/partition_iterator.h index 28e0e6a6242..128901834ee 100644 --- a/src/mongo/db/pipeline/window_function/partition_iterator.h +++ b/src/mongo/db/pipeline/window_function/partition_iterator.h @@ -77,13 +77,6 @@ public: return (*this)[0]; } - /** - * Returns true if iterator execution is paused. - */ - bool isPaused() { - return _state == IteratorState::kPauseExecution; - } - enum class AdvanceResult { kAdvanced, kNewPartition, @@ -294,9 +287,6 @@ private: enum class IteratorState { // Default state, no documents have been pulled into the cache. kNotInitialized, - // Input sources do not have a result to be processed yet, but there may be more results in - // the future. - kPauseExecution, // Iterating the current partition. We don't know where the current partition ends, or // whether it's the last partition. kIntraPartition, diff --git a/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp b/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp index 8b7a6beab76..cb6b5bcf1bf 100644 --- a/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp +++ b/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp @@ -499,20 +499,22 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForDocumentIteratorCache) { const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getApproximateSize(); - // Pull in the first document, and verify the reported size of the iterator is roughly the size - // of the document. + // Pull in the first document, and verify the reported size of the iterator is roughly double + // the size of the document. The size of the iterator is double the size of the document because + // we greedily fill the cache, so each internal document in memory stores two copies of + // largeStr. ASSERT_DOCUMENT_EQ(*_iter->current(), docs[0].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize + 500); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 2); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 + 500); // Pull in the second document. Both docs remain in the cache so the reported memory should // include both. advance(); ASSERT_DOCUMENT_EQ(*_iter->current(), docs[1].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 + 500); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 2 * 2); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 * 2 + 500); } TEST_F(PartitionIteratorTest, MemoryUsageAccountsForArraysInDocumentIteratorCache) { @@ -523,21 +525,21 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForArraysInDocumentIteratorCach const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getApproximateSize(); - // Pull in the first document, and verify the reported size of the iterator is roughly the size - // of the document. The reason we can't use EQ is that for memory tracking we call shred() so - // that the document cache will not increase when fields are accessed + // Pull in the first document, and verify the reported size of the iterator is roughly + // triple the size of the document. The reason for this is that 'largeStr' is cached twice; once + // for the 'arr' element and once for the nested 'subObj' element. ASSERT_DOCUMENT_EQ(*_iter->current(), docs[0].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize + 1024); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 3); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 3 + 1024); // Pull in the second document. Both docs remain in the cache so the reported memory should // include both. advance(); ASSERT_DOCUMENT_EQ(*_iter->current(), docs[1].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 2); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 + 1024); + ASSERT_GT(_iter->getApproximateSize(), (initialDocSize * 3) * 2); + ASSERT_LT(_iter->getApproximateSize(), (initialDocSize * 3) * 2 + 1024); } TEST_F(PartitionIteratorTest, MemoryUsageAccountsForNestedArraysInDocumentIteratorCache) { @@ -548,21 +550,21 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForNestedArraysInDocumentIterat const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getApproximateSize(); - // Pull in the first document, and verify the reported size of the iterator is roughly the size - // of the document. The reason we can't use EQ is that for memory tracking we call shred() so - // that the document cache will not increase when fields are accessed + // Pull in the first document, and verify the reported size of the iterator is roughly + // triple the size of the document. The reason for this is that 'largeStr' is cached twice; once + // for the 'arr' element and once for the nested 'subObj' element. ASSERT_DOCUMENT_EQ(*_iter->current(), docs[0].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize + 1024); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 3); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 3 + 1024); // Pull in the second document. Both docs remain in the cache so the reported memory should // include both. advance(); ASSERT_DOCUMENT_EQ(*_iter->current(), docs[1].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 2); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 + 1024); + ASSERT_GT(_iter->getApproximateSize(), (initialDocSize * 3) * 2); + ASSERT_LT(_iter->getApproximateSize(), (initialDocSize * 3) * 2 + 1024); } TEST_F(PartitionIteratorTest, MemoryUsageAccountsForNestedObjInDocumentIteratorCache) { @@ -573,12 +575,13 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForNestedObjInDocumentIteratorC const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getApproximateSize(); - // Pull in the first document, and verify the reported size. + // Pull in the first document, and verify the reported size. TODO SERVER-57011: The approximate + // size should not double count the nested strings. ASSERT_DOCUMENT_EQ(*_iter->current(), docs[0].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 3); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 4); } TEST_F(PartitionIteratorTest, MemoryUsageAccountsForReleasedDocuments) { @@ -589,27 +592,20 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForReleasedDocuments) { const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getApproximateSize(); - // Pull in the first document, and verify the reported size of the iterator is roughly the size - // of the document. + // Pull in the first document, and verify the reported size of the iterator is roughly double + // the size of the document. ASSERT_DOCUMENT_EQ(*accessor[0], docs[0].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize + 1024); - - // Read the field so that it is coppied into the cache. This will make the document bigger but - // shouldn't affect memory tracking. - auto iterSizeBeforeAccess = _iter->getApproximateSize(); - docs[0].getDocument()["a"]; - ASSERT_GT(docs[0].getDocument().getCurrentApproximateSize(), initialDocSize); - ASSERT_EQ(_iter->getApproximateSize(), iterSizeBeforeAccess); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 2); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 + 1024); // The accessor will have marked the first document as expired, and thus freed on the next call // to advance(). advance(); ASSERT_DOCUMENT_EQ(*_iter->current(), docs[1].getDocument()); - ASSERT_GT(_iter->getApproximateSize(), initialDocSize); - ASSERT_LT(_iter->getApproximateSize(), initialDocSize + 1024); + ASSERT_GT(_iter->getApproximateSize(), initialDocSize * 2); + ASSERT_LT(_iter->getApproximateSize(), initialDocSize * 2 + 1024); } TEST_F(PartitionIteratorTest, ManualPolicy) { diff --git a/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp b/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp index 7599842de57..62ec29b4d9c 100644 --- a/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp +++ b/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp @@ -30,7 +30,7 @@ #include "mongo/platform/basic.h" #include "mongo/db/catalog_raii.h" -#include "mongo/db/concurrency/exception_util.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/aggregation_mongod_context_fixture.h" #include "mongo/db/pipeline/window_function/spillable_cache.h" diff --git a/src/mongo/db/pipeline/window_function/window_bounds.cpp b/src/mongo/db/pipeline/window_function/window_bounds.cpp index 7ede49d0065..dd082135e02 100644 --- a/src/mongo/db/pipeline/window_function/window_bounds.cpp +++ b/src/mongo/db/pipeline/window_function/window_bounds.cpp @@ -63,19 +63,12 @@ WindowBounds::Bound<T> parseBound(ExpressionContext* expCtx, } template <class T> -Value serializeBound(const WindowBounds::Bound<T>& bound, - const SerializationOptions& opts, - const Value& representativeValue) { +Value serializeBound(const WindowBounds::Bound<T>& bound) { return stdx::visit( visit_helper::Overloaded{ - [&](const WindowBounds::Unbounded&) { return Value(WindowBounds::kValUnbounded); }, - [&](const WindowBounds::Current&) { return Value(WindowBounds::kValCurrent); }, - [&](const T& n) { - // If not "unbounded" or "current", n must be a literal constant - // The upper bound must be greater than the lower bound. We override the - // representative value to meet this constraint. - return opts.serializeLiteral(n, representativeValue); - }, + [](const WindowBounds::Unbounded&) { return Value(WindowBounds::kValUnbounded); }, + [](const WindowBounds::Current&) { return Value(WindowBounds::kValCurrent); }, + [](const T& n) { return Value(n); }, }, bound); } @@ -222,31 +215,22 @@ WindowBounds WindowBounds::parse(BSONObj args, uassert(5339902, "Range-based bounds require sortBy a single field", sortBy && sortBy->size() == 1); - const SortPattern::SortPatternPart& part = *sortBy->begin(); - uassert(8947400, - "Range-based bounds require a non-expression sortBy", - part.fieldPath && !part.expression); - uassert(8947401, "Range-based bounds require an ascending sortBy", part.isAscending); return bounds; } } -void WindowBounds::serialize(MutableDocument& args, const SerializationOptions& opts) const { +void WindowBounds::serialize(MutableDocument& args) const { stdx::visit( visit_helper::Overloaded{ [&](const DocumentBased& docBounds) { args[kArgDocuments] = Value{std::vector<Value>{ - serializeBound( - docBounds.lower, opts, /* representative value, if needed */ Value(0LL)), - serializeBound( - docBounds.upper, opts, /* representative value, if needed */ Value(1LL)), + serializeBound(docBounds.lower), + serializeBound(docBounds.upper), }}; }, [&](const RangeBased& rangeBounds) { args[kArgRange] = Value{std::vector<Value>{ - serializeBound( - rangeBounds.lower, opts, /* representative value, if needed */ Value(0LL)), - serializeBound( - rangeBounds.upper, opts, /* representative value, if needed */ Value(1LL)), + serializeBound(rangeBounds.lower), + serializeBound(rangeBounds.upper), }}; if (rangeBounds.unit) { args[kArgUnit] = Value{serializeTimeUnit(*rangeBounds.unit)}; diff --git a/src/mongo/db/pipeline/window_function/window_bounds.h b/src/mongo/db/pipeline/window_function/window_bounds.h index 90d0adf0371..6999f8fcdbc 100644 --- a/src/mongo/db/pipeline/window_function/window_bounds.h +++ b/src/mongo/db/pipeline/window_function/window_bounds.h @@ -121,7 +121,7 @@ struct WindowBounds { const boost::optional<SortPattern>& sortBy, ExpressionContext* expCtx); - void serialize(MutableDocument& args, const SerializationOptions& opts) const; + void serialize(MutableDocument& args) const; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/window_function/window_function_exec_first_last.h b/src/mongo/db/pipeline/window_function/window_function_exec_first_last.h index cf6dd3b2ed6..f64b77c28f8 100644 --- a/src/mongo/db/pipeline/window_function/window_function_exec_first_last.h +++ b/src/mongo/db/pipeline/window_function/window_function_exec_first_last.h @@ -51,28 +51,18 @@ protected: Value getFirst() { auto endpoints = _iter.getEndpoints(_bounds); - if (!endpoints) { + if (!endpoints) return _default; - } const Document doc = *(_iter)[endpoints->first]; - auto result = _input->evaluate(doc, &_input->getExpressionContext()->variables); - if (result.missing()) { - result = _default; - } - return result; + return _input->evaluate(doc, &_input->getExpressionContext()->variables); } Value getLast() { auto endpoints = _iter.getEndpoints(_bounds); - if (!endpoints) { + if (!endpoints) return _default; - } const Document doc = *(_iter)[endpoints->second]; - auto result = _input->evaluate(doc, &_input->getExpressionContext()->variables); - if (result.missing()) { - result = _default; - } - return result; + return _input->evaluate(doc, &_input->getExpressionContext()->variables); } void reset() final {} diff --git a/src/mongo/db/pipeline/window_function/window_function_expression.cpp b/src/mongo/db/pipeline/window_function/window_function_expression.cpp index a46bac72dfa..54f0552b59b 100644 --- a/src/mongo/db/pipeline/window_function/window_function_expression.cpp +++ b/src/mongo/db/pipeline/window_function/window_function_expression.cpp @@ -37,7 +37,6 @@ #include "mongo/db/pipeline/document_source_set_window_fields_gen.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/query/query_feature_flags_gen.h" -#include "mongo/db/stats/counters.h" #include "mongo/db/pipeline/window_function/partition_iterator.h" #include "mongo/db/pipeline/window_function/window_function_exec.h" @@ -153,7 +152,6 @@ intrusive_ptr<Expression> Expression::parse(BSONObj obj, assertLanguageFeatureIsAllowed( opCtx, exprName, allowedWithApi, AllowedWithClientType::kAny); - expCtx->incrementWindowAccumulatorExprCounter(exprName); return parser(obj, sortBy, expCtx); } @@ -189,7 +187,6 @@ void Expression::registerParser( AllowedWithApiStrict allowedWithApi) { invariant(parserMap.find(functionName) == parserMap.end()); ExpressionParserRegistration r{parser, requiredMinVersion, allowedWithApi}; - operatorCountersWindowAccumulatorExpressions.addCounter(functionName); parserMap.emplace(std::move(functionName), std::move(r)); } @@ -310,12 +307,12 @@ boost::intrusive_ptr<Expression> ExpressionFirstLast::parse( template <typename WindowFunctionN, typename AccumulatorNType> Value ExpressionN<WindowFunctionN, AccumulatorNType>::serialize( - const SerializationOptions& opts) const { + boost::optional<ExplainOptions::Verbosity> explain) const { auto acc = buildAccumulatorOnly(); - MutableDocument result(acc->serialize(nExpr, _input, opts)); + MutableDocument result(acc->serialize(nExpr, _input, static_cast<bool>(explain))); MutableDocument windowField; - _bounds.serialize(windowField, opts); + _bounds.serialize(windowField); result[kWindowArg] = windowField.freezeToValue(); return result.freezeToValue(); } diff --git a/src/mongo/db/pipeline/window_function/window_function_expression.h b/src/mongo/db/pipeline/window_function/window_function_expression.h index b14a81d1ae3..0d638ac1350 100644 --- a/src/mongo/db/pipeline/window_function/window_function_expression.h +++ b/src/mongo/db/pipeline/window_function/window_function_expression.h @@ -189,16 +189,17 @@ public: } }; - virtual Value serialize(const SerializationOptions& opts) const { + virtual Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument args; - args[_accumulatorName] = _input->serialize(opts); + args[_accumulatorName] = _input->serialize(static_cast<bool>(explain)); MutableDocument windowField; - _bounds.serialize(windowField, opts); + _bounds.serialize(windowField); args[kWindowArg] = windowField.freezeToValue(); return args.freezeToValue(); } + protected: ExpressionContext* _expCtx; std::string _accumulatorName; @@ -325,9 +326,9 @@ public: << " is not supported as a removable window function"); } - Value serialize(const SerializationOptions& opts) const final { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument args; - args.addField(_accumulatorName, Value(_input->serialize(opts))); + args.addField(_accumulatorName, Value(_input->serialize(static_cast<bool>(explain)))); return args.freezeToValue(); } }; @@ -443,7 +444,7 @@ public: << " is not supported with a removable window"); } - Value serialize(const SerializationOptions& opts) const final { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument args; args.addField(_accumulatorName, Value(Document())); return args.freezeToValue(); @@ -492,17 +493,15 @@ public: << " is not supported with a removable window"); } - Value serialize(const SerializationOptions& opts) const final { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument subObj; tassert(5433604, "ExpMovingAvg neither N nor alpha was set", _N || _alpha); if (_N) { - subObj[kNArg] = opts.serializeLiteral(_N.get()); + subObj[kNArg] = Value(_N.get()); } else { - // Alpha must be between zero and one (exclusive), so choose a legal representative - // value if applicable. - subObj[kAlphaArg] = opts.serializeLiteral(_alpha.get(), Value(0.1)); + subObj[kAlphaArg] = Value(_alpha.get()); } - subObj[kInputArg] = _input->serialize(opts); + subObj[kInputArg] = _input->serialize(static_cast<bool>(explain)); MutableDocument outerObj; outerObj[kAccName] = subObj.freezeToValue(); return outerObj.freezeToValue(); @@ -529,15 +528,15 @@ public: return _unit; } - Value serialize(const SerializationOptions& opts) const final { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument result; - result[_accumulatorName][kArgInput] = _input->serialize(opts); + result[_accumulatorName][kArgInput] = _input->serialize(static_cast<bool>(explain)); if (_unit) { result[_accumulatorName][kArgUnit] = Value(serializeTimeUnit(*_unit)); } MutableDocument windowField; - _bounds.serialize(windowField, opts); + _bounds.serialize(windowField); result[kWindowArg] = windowField.freezeToValue(); return result.freezeToValue(); } @@ -556,7 +555,7 @@ protected: case TimeUnit::year: case TimeUnit::quarter: case TimeUnit::month: - uasserted(5490710, "unit must be 'week' or smaller"); + uasserted(5490704, "unit must be 'week' or smaller"); // Only these time units are allowed. case TimeUnit::week: case TimeUnit::day: @@ -804,16 +803,16 @@ public: } boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final { - MONGO_UNREACHABLE_TASSERT(5490704); + MONGO_UNREACHABLE_TASSERT(5490701); } std::unique_ptr<WindowFunctionState> buildRemovable() const final { - MONGO_UNREACHABLE_TASSERT(5490705); + MONGO_UNREACHABLE_TASSERT(5490702); } - Value serialize(const SerializationOptions& opts) const final { + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument args; - args.addField(_accumulatorName, Value(_input->serialize(opts))); + args.addField(_accumulatorName, Value(_input->serialize(static_cast<bool>(explain)))); return args.freezeToValue(); } }; @@ -852,11 +851,11 @@ public: } boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final { - MONGO_UNREACHABLE_TASSERT(5490706); + MONGO_UNREACHABLE_TASSERT(5490701); } std::unique_ptr<WindowFunctionState> buildRemovable() const final { - MONGO_UNREACHABLE_TASSERT(5490707); + MONGO_UNREACHABLE_TASSERT(5490702); } }; @@ -874,11 +873,11 @@ public: } boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final { - MONGO_UNREACHABLE_TASSERT(5490708); + MONGO_UNREACHABLE_TASSERT(5490701); } std::unique_ptr<WindowFunctionState> buildRemovable() const final { - MONGO_UNREACHABLE_TASSERT(5490709); + MONGO_UNREACHABLE_TASSERT(5490702); } }; @@ -905,7 +904,7 @@ public: nExpr(std::move(nExpr)), sortPattern(std::move(sortPattern)) {} - Value serialize(const SerializationOptions& opts) const final; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final; diff --git a/src/mongo/db/pipeline/window_function/window_function_push.h b/src/mongo/db/pipeline/window_function/window_function_push.h index 932bbcb29dd..4ed5b083996 100644 --- a/src/mongo/db/pipeline/window_function/window_function_push.h +++ b/src/mongo/db/pipeline/window_function/window_function_push.h @@ -48,9 +48,6 @@ public: } void add(Value value) override { - if (value.missing()) { - return; - } _memUsageBytes += value.getApproximateSize(); _values.push_back(std::move(value)); } @@ -59,10 +56,6 @@ public: * This should only remove the first/lowest element in the window. */ void remove(Value value) override { - if (value.missing()) { - return; - } - tassert(5423801, "Can't remove from an empty WindowFunctionPush", _values.size() != 0); auto valToRemove = _values.front(); tassert( diff --git a/src/mongo/db/pipeline/window_function/window_function_shift.cpp b/src/mongo/db/pipeline/window_function/window_function_shift.cpp index 0ae15c4e3f0..c74424e74e7 100644 --- a/src/mongo/db/pipeline/window_function/window_function_shift.cpp +++ b/src/mongo/db/pipeline/window_function/window_function_shift.cpp @@ -119,12 +119,12 @@ boost::intrusive_ptr<Expression> ExpressionShift::parse(BSONObj obj, return shiftExpr; } -Value ExpressionShift::serialize(const SerializationOptions& opts) const { +Value ExpressionShift::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument args; - args.addField(kByArg, opts.serializeLiteral(_offset)); - args.addField(kOutputArg, _input->serialize(opts)); - args.addField(kDefaultArg, - opts.serializeLiteral(_defaultVal.get_value_or(mongo::Value(BSONNULL)))); + args.addField(kByArg, Value(_offset)); + args.addField(kOutputArg, _input->serialize(static_cast<bool>(explain))); + args.addField(kDefaultArg, _defaultVal.get_value_or(mongo::Value(BSONNULL))); + MutableDocument windowFun; windowFun.addField(_accumulatorName, args.freezeToValue()); return windowFun.freezeToValue(); diff --git a/src/mongo/db/pipeline/window_function/window_function_shift.h b/src/mongo/db/pipeline/window_function/window_function_shift.h index 649e90ce7e4..99a45dd3f84 100644 --- a/src/mongo/db/pipeline/window_function/window_function_shift.h +++ b/src/mongo/db/pipeline/window_function/window_function_shift.h @@ -68,7 +68,7 @@ public: MONGO_UNREACHABLE_TASSERT(5424302); } - Value serialize(const SerializationOptions& opts) const final; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; private: static boost::intrusive_ptr<Expression> parseShiftArgs(BSONObj obj, diff --git a/src/mongo/db/pipeline/window_function/window_function_stddev.h b/src/mongo/db/pipeline/window_function/window_function_stddev.h index 14aa1fa1260..5b4aa45b075 100644 --- a/src/mongo/db/pipeline/window_function/window_function_stddev.h +++ b/src/mongo/db/pipeline/window_function/window_function_stddev.h @@ -63,7 +63,7 @@ public: if (_nonfiniteValueCount > 0) return Value(BSONNULL); const long long adjustedCount = _isSamp ? _count - 1 : _count; - if (adjustedCount <= 0) + if (adjustedCount == 0) return getDefault(); double squaredDifferences = _m2->getValue(false).coerceToDouble(); if (squaredDifferences < 0 || (!_isSamp && _count == 1)) { diff --git a/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h b/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h index 64d3319efb9..fbf77c59fa2 100644 --- a/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h +++ b/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h @@ -56,17 +56,17 @@ public: explicit WindowFunctionTopBottomN(ExpressionContext* const expCtx, SortPattern sp, long long n) : WindowFunctionState(expCtx), _acc(expCtx, std::move(sp), true) { _acc.startNewGroup(Value(n)); - updateMemUsage(); + _memUsageBytes = sizeof(*this); } void add(Value value) final { _acc.process(value, false); - updateMemUsage(); + _memUsageBytes = _acc.getMemUsage(); } void remove(Value value) final { _acc.remove(value); - updateMemUsage(); + _memUsageBytes = _acc.getMemUsage(); } Value getValue() const final { @@ -75,14 +75,10 @@ public: void reset() final { _acc.reset(); - updateMemUsage(); + _memUsageBytes = _acc.getMemUsage(); } private: - void updateMemUsage() { - _memUsageBytes = sizeof(*this) + _acc.getMemUsage(); - } - AccumulatorTopBottomN<sense, single> _acc; }; |
