summaryrefslogtreecommitdiff
path: root/src/mongo/db/pipeline
diff options
context:
space:
mode:
authorLucas de Castro Borges <lucas@gnuabordo.com.br>2025-02-14 14:26:38 -0300
committerLucas de Castro Borges <lucas@gnuabordo.com.br>2025-02-14 14:26:38 -0300
commit294bc6ecabf14c09c9bc8644704921dcf97cb44e (patch)
tree279b1e0bab53901a1647ac63c1c724f0f789a663 /src/mongo/db/pipeline
parent70be7c27a251621187a1de533462ae2bb1e3bd39 (diff)
parent1e917fd798aa25b7066d4b414b51184f13d5a092 (diff)
Update upstream source from tag 'upstream/6.0.10'debian/6.0.10-1
Update to upstream version '6.0.10' with Debian dir 2d176fa254eee97b139f712fec5709641335a8c3
Diffstat (limited to 'src/mongo/db/pipeline')
-rw-r--r--src/mongo/db/pipeline/SConscript36
-rw-r--r--src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp6
-rw-r--r--src/mongo/db/pipeline/abt/field_map_builder.h2
-rw-r--r--src/mongo/db/pipeline/accumulation_statement.cpp4
-rw-r--r--src/mongo/db/pipeline/accumulator_js_reduce.cpp82
-rw-r--r--src/mongo/db/pipeline/accumulator_js_test.cpp33
-rw-r--r--src/mongo/db/pipeline/accumulator_multi.cpp9
-rw-r--r--src/mongo/db/pipeline/aggregate_command.idl8
-rw-r--r--src/mongo/db/pipeline/change_stream_event_transform.cpp12
-rw-r--r--src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp335
-rw-r--r--src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h16
-rw-r--r--src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp44
-rw-r--r--src/mongo/db/pipeline/change_stream_helpers.cpp53
-rw-r--r--src/mongo/db/pipeline/change_stream_helpers.h47
-rw-r--r--src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp9
-rw-r--r--src/mongo/db/pipeline/change_stream_split_event_helpers.cpp131
-rw-r--r--src/mongo/db/pipeline/change_stream_split_event_helpers.h68
-rw-r--r--src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp158
-rw-r--r--src/mongo/db/pipeline/dependencies.cpp66
-rw-r--r--src/mongo/db/pipeline/dependencies.h39
-rw-r--r--src/mongo/db/pipeline/dependencies_test.cpp14
-rw-r--r--src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp42
-rw-r--r--src/mongo/db/pipeline/document_path_support.cpp11
-rw-r--r--src/mongo/db/pipeline/document_path_support.h9
-rw-r--r--src/mongo/db/pipeline/document_path_support_test.cpp33
-rw-r--r--src/mongo/db/pipeline/document_source.h22
-rw-r--r--src/mongo/db/pipeline/document_source_bucket_auto.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream.cpp46
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream.h17
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp13
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp13
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h3
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp3
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp42
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_check_resumability.h9
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp6
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h2
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp3
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h2
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp3
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp236
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_split_large_event.h123
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_transform.cpp10
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_check_resume_token_test.cpp11
-rw-r--r--src/mongo/db/pipeline/document_source_coll_stats.cpp57
-rw-r--r--src/mongo/db/pipeline/document_source_coll_stats.h4
-rw-r--r--src/mongo/db/pipeline/document_source_coll_stats.idl7
-rw-r--r--src/mongo/db/pipeline/document_source_densify.cpp9
-rw-r--r--src/mongo/db/pipeline/document_source_documents.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_documents.h24
-rw-r--r--src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_geo_near.h13
-rw-r--r--src/mongo/db/pipeline/document_source_geo_near_cursor.cpp3
-rw-r--r--src/mongo/db/pipeline/document_source_graph_lookup.cpp3
-rw-r--r--src/mongo/db/pipeline/document_source_graph_lookup.h4
-rw-r--r--src/mongo/db/pipeline/document_source_group.cpp846
-rw-r--r--src/mongo/db/pipeline/document_source_group.h253
-rw-r--r--src/mongo/db/pipeline/document_source_group_base.cpp811
-rw-r--r--src/mongo/db/pipeline/document_source_group_base.h266
-rw-r--r--src/mongo/db/pipeline/document_source_group_test.cpp340
-rw-r--r--src/mongo/db/pipeline/document_source_index_stats.cpp5
-rw-r--r--src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp163
-rw-r--r--src/mongo/db/pipeline/document_source_internal_all_collection_stats.h127
-rw-r--r--src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl47
-rw-r--r--src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h4
-rw-r--r--src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp499
-rw-r--r--src/mongo/db/pipeline/document_source_internal_unpack_bucket.h56
-rw-r--r--src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp132
-rw-r--r--src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp43
-rw-r--r--src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp238
-rw-r--r--src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp34
-rw-r--r--src/mongo/db/pipeline/document_source_lookup.cpp11
-rw-r--r--src/mongo/db/pipeline/document_source_match.cpp15
-rw-r--r--src/mongo/db/pipeline/document_source_match.h6
-rw-r--r--src/mongo/db/pipeline/document_source_merge.cpp281
-rw-r--r--src/mongo/db/pipeline/document_source_merge.h33
-rw-r--r--src/mongo/db/pipeline/document_source_mock.h2
-rw-r--r--src/mongo/db/pipeline/document_source_out.cpp5
-rw-r--r--src/mongo/db/pipeline/document_source_out.h12
-rw-r--r--src/mongo/db/pipeline/document_source_queue.h2
-rw-r--r--src/mongo/db/pipeline/document_source_redact.h10
-rw-r--r--src/mongo/db/pipeline/document_source_redact_test.cpp21
-rw-r--r--src/mongo/db/pipeline/document_source_replace_root.h2
-rw-r--r--src/mongo/db/pipeline/document_source_sample.cpp4
-rw-r--r--src/mongo/db/pipeline/document_source_sequential_document_cache.cpp3
-rw-r--r--src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h1
-rw-r--r--src/mongo/db/pipeline/document_source_set_window_fields.cpp6
-rw-r--r--src/mongo/db/pipeline/document_source_set_window_fields.h2
-rw-r--r--src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp129
-rw-r--r--src/mongo/db/pipeline/document_source_sharded_data_distribution.h73
-rw-r--r--src/mongo/db/pipeline/document_source_sort.cpp22
-rw-r--r--src/mongo/db/pipeline/document_source_sort.h4
-rw-r--r--src/mongo/db/pipeline/document_source_streaming_group.cpp279
-rw-r--r--src/mongo/db/pipeline/document_source_streaming_group.h126
-rw-r--r--src/mongo/db/pipeline/document_source_unwind.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_writer.h93
-rw-r--r--src/mongo/db/pipeline/expression.cpp497
-rw-r--r--src/mongo/db/pipeline/expression.h115
-rw-r--r--src/mongo/db/pipeline/expression_context.cpp16
-rw-r--r--src/mongo/db/pipeline/expression_context.h71
-rw-r--r--src/mongo/db/pipeline/expression_date_test.cpp4
-rw-r--r--src/mongo/db/pipeline/expression_js_emit.cpp13
-rw-r--r--src/mongo/db/pipeline/expression_test.cpp70
-rw-r--r--src/mongo/db/pipeline/field_path.cpp7
-rw-r--r--src/mongo/db/pipeline/group_from_first_document_transformation.cpp87
-rw-r--r--src/mongo/db/pipeline/group_from_first_document_transformation.h92
-rw-r--r--src/mongo/db/pipeline/javascript_execution.h8
-rw-r--r--src/mongo/db/pipeline/lite_parsed_document_source.h14
-rw-r--r--src/mongo/db/pipeline/lite_parsed_pipeline.cpp16
-rw-r--r--src/mongo/db/pipeline/lite_parsed_pipeline.h17
-rw-r--r--src/mongo/db/pipeline/lookup_set_cache_test.cpp6
-rw-r--r--src/mongo/db/pipeline/map_reduce_options.idl42
-rw-r--r--src/mongo/db/pipeline/memory_usage_tracker.h23
-rw-r--r--src/mongo/db/pipeline/memory_usage_tracker_test.cpp26
-rw-r--r--src/mongo/db/pipeline/monotonic_expression.cpp60
-rw-r--r--src/mongo/db/pipeline/monotonic_expression.h70
-rw-r--r--src/mongo/db/pipeline/monotonic_expression_test.cpp149
-rw-r--r--src/mongo/db/pipeline/pipeline.cpp83
-rw-r--r--src/mongo/db/pipeline/pipeline.h10
-rw-r--r--src/mongo/db/pipeline/pipeline_d.cpp40
-rw-r--r--src/mongo/db/pipeline/pipeline_test.cpp81
-rw-r--r--src/mongo/db/pipeline/plan_executor_pipeline.cpp31
-rw-r--r--src/mongo/db/pipeline/plan_executor_pipeline.h5
-rw-r--r--src/mongo/db/pipeline/process_interface/SConscript1
-rw-r--r--src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp61
-rw-r--r--src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h17
-rw-r--r--src/mongo/db/pipeline/process_interface/common_process_interface.h66
-rw-r--r--src/mongo/db/pipeline/process_interface/mongo_process_interface.h63
-rw-r--r--src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp17
-rw-r--r--src/mongo/db/pipeline/process_interface/mongos_process_interface.h7
-rw-r--r--src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp21
-rw-r--r--src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h4
-rw-r--r--src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp28
-rw-r--r--src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h14
-rw-r--r--src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp42
-rw-r--r--src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h17
-rw-r--r--src/mongo/db/pipeline/process_interface/standalone_process_interface.h5
-rw-r--r--src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h35
-rw-r--r--src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp91
-rw-r--r--src/mongo/db/pipeline/resume_token.cpp26
-rw-r--r--src/mongo/db/pipeline/resume_token.h3
-rw-r--r--src/mongo/db/pipeline/resume_token_test.cpp46
-rw-r--r--src/mongo/db/pipeline/semantic_analysis.cpp40
-rw-r--r--src/mongo/db/pipeline/semantic_analysis.h15
-rw-r--r--src/mongo/db/pipeline/semantic_analysis_test.cpp14
-rw-r--r--src/mongo/db/pipeline/sharded_agg_helpers.cpp64
-rw-r--r--src/mongo/db/pipeline/sharded_agg_helpers.h23
-rw-r--r--src/mongo/db/pipeline/skip_and_limit.cpp33
-rw-r--r--src/mongo/db/pipeline/skip_and_limit.h7
-rw-r--r--src/mongo/db/pipeline/stage_constraints.h34
-rw-r--r--src/mongo/db/pipeline/window_function/partition_iterator.cpp21
-rw-r--r--src/mongo/db/pipeline/window_function/partition_iterator.h10
-rw-r--r--src/mongo/db/pipeline/window_function/partition_iterator_test.cpp10
-rw-r--r--src/mongo/db/pipeline/window_function/spillable_cache_test.cpp2
-rw-r--r--src/mongo/db/pipeline/window_function/window_function_expression.cpp3
-rw-r--r--src/mongo/db/pipeline/window_function/window_function_expression.h14
-rw-r--r--src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h12
158 files changed, 6914 insertions, 2576 deletions
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript
index 16e39fc4832..a1c0634027e 100644
--- a/src/mongo/db/pipeline/SConscript
+++ b/src/mongo/db/pipeline/SConscript
@@ -104,6 +104,7 @@ env.Library(
'expression_trigonometric.cpp',
'javascript_execution.cpp',
'make_js_function.cpp',
+ 'monotonic_expression.cpp',
'variables.cpp',
],
LIBDEPS=[
@@ -165,6 +166,7 @@ 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',
@@ -183,8 +185,9 @@ env.Library(
],
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/exec/sort_executor',
- '$BUILD_DIR/mongo/db/index/key_generator'
- ]
+ '$BUILD_DIR/mongo/db/index/index_access_method',
+ '$BUILD_DIR/mongo/idl/idl_parser',
+ ],
)
env.Library(
@@ -287,7 +290,9 @@ 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',
@@ -314,13 +319,16 @@ 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',
@@ -349,7 +357,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/key_generator',
+ '$BUILD_DIR/mongo/db/index/index_access_method',
'$BUILD_DIR/mongo/db/logical_session_cache',
'$BUILD_DIR/mongo/db/logical_session_id_helpers',
'$BUILD_DIR/mongo/db/matcher/expressions',
@@ -377,6 +385,7 @@ 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',
@@ -391,6 +400,7 @@ 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',
@@ -420,6 +430,7 @@ 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',
@@ -429,6 +440,7 @@ 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',
],
@@ -439,6 +451,7 @@ 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',
],
)
@@ -461,6 +474,7 @@ 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',
@@ -493,6 +507,7 @@ 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',
@@ -521,6 +536,17 @@ 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",
@@ -542,12 +568,14 @@ 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_count_test.cpp',
@@ -563,7 +591,6 @@ 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',
@@ -619,6 +646,7 @@ 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 582f3263bfb..1d136a098ce 100644
--- a/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp
+++ b/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp
@@ -179,7 +179,7 @@ private:
}
void processProjectedPaths(const projection_executor::InclusionNode& node) {
- std::set<std::string> preservedPaths;
+ OrderedPathSet preservedPaths;
node.reportProjectedPaths(&preservedPaths);
for (const std::string& preservedPathStr : preservedPaths) {
@@ -194,7 +194,7 @@ private:
void processComputedPaths(const projection_executor::InclusionNode& node,
const std::string& rootProjection,
const bool isAddingFields) {
- std::set<std::string> computedPaths;
+ OrderedPathSet computedPaths;
StringMap<std::string> renamedPaths;
node.reportComputedPaths(&computedPaths, &renamedPaths);
@@ -268,7 +268,7 @@ private:
}
void visitExclusionNode(const projection_executor::ExclusionNode& node) {
- std::set<std::string> preservedPaths;
+ OrderedPathSet preservedPaths;
node.reportProjectedPaths(&preservedPaths);
for (const std::string& preservedPathStr : preservedPaths) {
diff --git a/src/mongo/db/pipeline/abt/field_map_builder.h b/src/mongo/db/pipeline/abt/field_map_builder.h
index af593d38cd3..4eedf150644 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;
- std::set<std::string> _childPaths;
+ OrderedPathSet _childPaths;
};
class FieldMapBuilder {
diff --git a/src/mongo/db/pipeline/accumulation_statement.cpp b/src/mongo/db/pipeline/accumulation_statement.cpp
index d5afb1f83d0..cb5efcf81ec 100644
--- a/src/mongo/db/pipeline/accumulation_statement.cpp
+++ b/src/mongo/db/pipeline/accumulation_statement.cpp
@@ -38,6 +38,7 @@
#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"
@@ -63,6 +64,7 @@ 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) {
@@ -118,6 +120,8 @@ 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_js_reduce.cpp b/src/mongo/db/pipeline/accumulator_js_reduce.cpp
index 71075978d6d..78058d5913d 100644
--- a/src/mongo/db/pipeline/accumulator_js_reduce.cpp
+++ b/src/mongo/db/pipeline/accumulator_js_reduce.cpp
@@ -32,6 +32,7 @@
#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 {
@@ -120,47 +121,52 @@ Value AccumulatorInternalJsReduce::getValue(bool toBeMerged) {
if (_values.size() < 1) {
return Value{};
}
-
- 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;
+ 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;
}
- 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);
+ 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);
+ }
}
}
diff --git a/src/mongo/db/pipeline/accumulator_js_test.cpp b/src/mongo/db/pipeline/accumulator_js_test.cpp
index 953b4ce475a..2a44e8e967c 100644
--- a/src/mongo/db/pipeline/accumulator_js_test.cpp
+++ b/src/mongo/db/pipeline/accumulator_js_test.cpp
@@ -38,6 +38,7 @@
#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 {
@@ -197,6 +198,38 @@ 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 2f6971e1f22..57cf06f0090 100644
--- a/src/mongo/db/pipeline/accumulator_multi.cpp
+++ b/src/mongo/db/pipeline/accumulator_multi.cpp
@@ -671,12 +671,6 @@ void AccumulatorTopBottomN<sense, single>::_processValue(const Value& val) {
}
}
- // 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);
@@ -698,9 +692,6 @@ void AccumulatorTopBottomN<sense, single>::remove(const Value& val) {
auto it = _map->lower_bound(keyOutPair.first);
_map->erase(it);
- // 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/aggregate_command.idl b/src/mongo/db/pipeline/aggregate_command.idl
index b84513a30e6..0a4ae1f7ec4 100644
--- a/src/mongo/db/pipeline/aggregate_command.idl
+++ b/src/mongo/db/pipeline/aggregate_command.idl
@@ -156,6 +156,14 @@ 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
# 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/change_stream_event_transform.cpp b/src/mongo/db/pipeline/change_stream_event_transform.cpp
index 9e8a7d8ac19..f8b5052ecbe 100644
--- a/src/mongo/db/pipeline/change_stream_event_transform.cpp
+++ b/src/mongo/db/pipeline/change_stream_event_transform.cpp
@@ -31,6 +31,8 @@
#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"
@@ -43,6 +45,7 @@
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);
@@ -79,8 +82,7 @@ ChangeStreamEventTransformation::ChangeStreamEventTransformation(
const DocumentSourceChangeStreamSpec& spec)
: _changeStreamSpec(spec), _expCtx(expCtx) {
// Extract the resume token from the spec and store it.
- _resumeToken =
- DocumentSourceChangeStream::resolveResumeTokenFromSpec(_expCtx, _changeStreamSpec);
+ _resumeToken = resolveResumeToken(_expCtx, _changeStreamSpec);
// Determine whether the user requested a point-in-time pre-image, which will affect this
// stage's output.
@@ -107,8 +109,10 @@ ResumeTokenData ChangeStreamEventTransformation::makeResumeToken(Value tsVal,
// If we have a resume token, we need to match the version with which it was generated until we
// have surpassed it, at which point we can begin generating tokens with our default version.
- auto version = (clusterTime > _resumeToken.clusterTime) ? _expCtx->changeStreamTokenVersion
- : _resumeToken.version;
+ // If we have been explicitly instructed to ignore the client's token version, skip this check.
+ auto version = (clusterTime > _resumeToken.clusterTime || _expCtx->ignoreTokenVersionOnResume)
+ ? _expCtx->changeStreamTokenVersion
+ : _resumeToken.version;
// Construct and return the final resume token.
return {clusterTime, version, txnOpIndex, uuid, operationType, documentKey, opDescription};
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 2b16d4dacda..78e945a4ae8 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/write_conflict_exception.h"
+#include "mongo/db/concurrency/exception_util.h"
#include "mongo/db/db_raii.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/pipeline/change_stream_preimage_gen.h"
@@ -58,18 +58,6 @@ 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) {
@@ -108,18 +96,48 @@ 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.
*
- * 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()).
+ * 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.
*
* +-------------------------+
* | config.system.preimages |
@@ -134,267 +152,96 @@ RecordId toRecordId(ChangeStreamPreImageId id) {
* | applyIndex: 0 | | applyIndex: 0 | | applyIndex: 0 | | applyIndex: 1 |
* +-------------------+ +-------------------+ +-------------------+ +-------------------+
*/
-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();
-
+size_t deleteExpiredChangeStreamPreImages(OperationContext* opCtx,
+ Date_t currentTimeForTimeBasedExpiration) {
// Acquire intent-exclusive lock on the pre-images collection. Early exit if the collection
// doesn't exist.
- AutoGetCollection autoColl(
- opCtx.get(), NamespaceString::kChangeStreamPreImagesNamespace, MODE_IX);
+ AutoGetCollection autoColl(opCtx, NamespaceString::kChangeStreamPreImagesNamespace, MODE_IX);
const auto& preImagesColl = autoColl.getCollection();
if (!preImagesColl) {
- return;
+ return 0;
}
// Do not run the job on secondaries.
- if (!repl::ReplicationCoordinator::get(opCtx.get())
- ->canAcceptWritesForDatabase(opCtx.get(), NamespaceString::kAdminDb)) {
- return;
+ if (!repl::ReplicationCoordinator::get(opCtx)->canAcceptWritesForDatabase(
+ opCtx, NamespaceString::kAdminDb)) {
+ return 0;
}
- // Get the timestamp of the ealiest oplog entry.
+ // Get the timestamp of the earliest oplog entry.
const auto currentEarliestOplogEntryTs =
- repl::StorageInterface::get(client->getServiceContext())
- ->getEarliestOplogTimestamp(opCtx.get());
+ repl::StorageInterface::get(opCtx->getServiceContext())->getEarliestOplogTimestamp(opCtx);
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();
- ChangeStreamExpiredPreImageIterator expiredPreImages(
- opCtx.get(),
- &preImagesColl,
- currentEarliestOplogEntryTs,
- ::mongo::preImageRemoverInternal::getPreImageExpirationTime(
- opCtx.get(), currentTimeForTimeBasedExpiration));
-
- for (const auto& collectionRange : expiredPreImages) {
+ boost::optional<UUID> currentCollectionUUID = boost::none;
+ while ((currentCollectionUUID =
+ findNextCollectionUUID(opCtx, &preImagesColl, currentCollectionUUID))) {
writeConflictRetry(
- opCtx.get(),
+ opCtx,
"ChangeStreamExpiredPreImagesRemover",
NamespaceString::kChangeStreamPreImagesNamespace.ns(),
[&] {
auto params = std::make_unique<DeleteStageParams>();
params->isMulti = true;
- boost::optional<std::unique_ptr<BatchedDeleteStageBatchParams>> batchParams;
+ std::unique_ptr<BatchedDeleteStageBatchParams> batchedDeleteParams;
if (isBatchedRemoval) {
- batchParams = std::make_unique<BatchedDeleteStageBatchParams>();
+ batchedDeleteParams = 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.get(),
+ opCtx,
&preImagesColl,
std::move(params),
PlanYieldPolicy::YieldPolicy::YIELD_AUTO,
InternalPlanner::Direction::FORWARD,
- RecordIdBound(collectionRange.first),
- RecordIdBound(collectionRange.second),
+ std::move(minRecordId),
+ std::move(maxRecordId),
CollectionScanParams::ScanBoundInclusion::kIncludeBothStartAndEndRecords,
- std::move(batchParams));
+ std::move(batchedDeleteParams),
+ filterPtr,
+ shouldReturnEofOnFilterMismatch);
numberOfRemovals += exec->executeDelete();
});
}
-
- 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());
- }
+ return numberOfRemovals;
}
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.
@@ -409,7 +256,7 @@ void performExpiredChangeStreamPreImagesRemovalPass(Client* client) {
currentTimeForTimeBasedExpiration = currentTimeElem.Date();
}
});
- deleteExpiredChangeStreamPreImages(client, currentTimeForTimeBasedExpiration);
+ deleteExpiredChangeStreamPreImages(opCtx.get(), 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 0ddd491991f..19315825043 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,22 +35,6 @@
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 ec49c8453d3..b2562a1db44 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,16 +61,6 @@ 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) {
@@ -107,39 +97,5 @@ 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_helpers.cpp b/src/mongo/db/pipeline/change_stream_helpers.cpp
new file mode 100644
index 00000000000..720646d32ea
--- /dev/null
+++ b/src/mongo/db/pipeline/change_stream_helpers.cpp
@@ -0,0 +1,53 @@
+/**
+ * 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
new file mode 100644
index 00000000000..7adc4f92f8f
--- /dev/null
+++ b/src/mongo/db/pipeline/change_stream_helpers.h
@@ -0,0 +1,47 @@
+/**
+ * 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 ab0ade1e517..f7703354560 100644
--- a/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp
+++ b/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp
@@ -29,6 +29,8 @@
#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"
@@ -888,9 +890,12 @@ 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: '"
- << nsElem.regex() << "', options: '" << nsElem.regexFlags() << "'}}";
+ << "{$regexMatch: {input: " << exprDbOrCollName << ", regex: '" << regex
+ << "', options: '" << nsElem.regexFlags() << "'}}";
// Finally, wrap the regex in a $let which defines the '$$oplogField' variable.
const std::string exprRewrittenPredicate = str::stream()
diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp b/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp
new file mode 100644
index 00000000000..19fba5afea5
--- /dev/null
+++ b/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp
@@ -0,0 +1,131 @@
+/**
+ * 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
new file mode 100644
index 00000000000..ee39b52dead
--- /dev/null
+++ b/src/mongo/db/pipeline/change_stream_split_event_helpers.h
@@ -0,0 +1,68 @@
+/**
+ * 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
new file mode 100644
index 00000000000..aea1ca6fce8
--- /dev/null
+++ b/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp
@@ -0,0 +1,158 @@
+/**
+ * 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/dependencies.cpp b/src/mongo/db/pipeline/dependencies.cpp
index d2a5563c7c7..4480ec8cf6b 100644
--- a/src/mongo/db/pipeline/dependencies.cpp
+++ b/src/mongo/db/pipeline/dependencies.cpp
@@ -37,11 +37,32 @@
namespace mongo {
-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;
+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;
}
BSONObj DepsTracker::toProjectionWithoutMetadata(
@@ -59,35 +80,16 @@ BSONObj DepsTracker::toProjectionWithoutMetadata(
return bb.obj();
}
- // 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.
+ // 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}.
bool idSpecified = false;
- std::string last;
- for (const auto& field : sortedFields()) {
- if (str::startsWith(field, "_id") && (field.size() == 3 || field[3] == '.')) {
+ 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] == '.')) {
idSpecified = true;
}
-
- 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);
- }
+ bb.append(path, 1);
}
if (!idSpecified) {
@@ -109,7 +111,7 @@ void DepsTracker::setNeedsMetadata(DocumentMetadataFields::MetaType type, bool r
}
// Returns true if the lhs value should sort before the rhs, false otherwise.
-bool PathPrefixComparator::operator()(const std::string& lhs, const std::string& rhs) const {
+bool PathComparator::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 3c892de8181..963584ba148 100644
--- a/src/mongo/db/pipeline/dependencies.h
+++ b/src/mongo/db/pipeline/dependencies.h
@@ -39,6 +39,21 @@
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 {
@@ -104,6 +119,16 @@ 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
@@ -185,11 +210,11 @@ struct DepsTracker {
}
/**
- * Return fieldpaths ordered such that a parent is immediately before its children.
+ * Return names of needed fields in dotted notation. A custom comparator orders the fields
+ * such that a parent is immediately before its children.
*/
- std::list<std::string> sortedFields() const;
+ OrderedPathSet fields;
- 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.
@@ -207,12 +232,4 @@ 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 938130b91bd..75451b258b8 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>
-set<string> arrayToSet(const char* (&array)[ArrayLen]) {
- set<string> out;
+OrderedPathSet arrayToSet(const char* (&array)[ArrayLen]) {
+ OrderedPathSet out;
for (size_t i = 0; i < ArrayLen; i++)
out.insert(array[i]);
return out;
@@ -306,19 +306,17 @@ TEST(DependenciesToProjectionTest, SortFieldPaths) {
"b.a"
"b.aa"
"b.🌲d"};
- DepsTracker deps;
- deps.fields = arrayToSet(array);
+ auto fields = arrayToSet(array);
// our custom sort will restore the ordering above
- std::list<std::string> fieldPathSorted = deps.sortedFields();
- auto itr = fieldPathSorted.begin();
- for (unsigned long i = 0; i < fieldPathSorted.size(); i++) {
+ auto itr = fields.begin();
+ for (unsigned long i = 0; i < fields.size(); i++) {
ASSERT_EQ(*itr, array[i]);
++itr;
}
}
TEST(DependenciesToProjectionTest, PathLessThan) {
- auto lessThan = PathPrefixComparator();
+ auto lessThan = PathComparator();
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 069a7e2f0b2..ac8924a13ed 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.h"
+#include "mongo/s/router_role.h"
namespace mongo {
namespace {
@@ -53,10 +53,14 @@ 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, std::move(pipeline));
+ auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand,
+ hasChangeStream,
+ startsWithDocuments,
+ std::move(pipeline),
+ boost::none /*explain*/);
ASSERT_EQ(results.remoteCursors.size(), 1UL);
ASSERT(!results.splitPipeline);
});
@@ -84,10 +88,14 @@ 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, std::move(pipeline));
+ auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand,
+ hasChangeStream,
+ startsWithDocuments,
+ std::move(pipeline),
+ boost::none /*explain*/);
ASSERT_EQ(results.remoteCursors.size(), 2UL);
ASSERT(bool(results.splitPipeline));
});
@@ -118,10 +126,14 @@ 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, std::move(pipeline));
+ auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand,
+ hasChangeStream,
+ startsWithDocuments,
+ std::move(pipeline),
+ boost::none /*explain*/);
ASSERT_EQ(results.remoteCursors.size(), 2UL);
ASSERT(bool(results.splitPipeline));
});
@@ -163,9 +175,14 @@ 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, std::move(pipeline)),
+ ASSERT_THROWS_CODE(sharded_agg_helpers::dispatchShardPipeline(serializedCommand,
+ hasChangeStream,
+ startsWithDocuments,
+ std::move(pipeline),
+ boost::none /*explain*/),
AssertionException,
ErrorCodes::StaleConfig);
});
@@ -197,6 +214,7 @@ 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);
@@ -204,7 +222,11 @@ TEST_F(DispatchShardPipelineTest, WrappedDispatchDoesRetryOnStaleConfigError) {
"dispatch shard pipeline"_sd,
[&](OperationContext* opCtx, const ChunkManager& cm) {
return sharded_agg_helpers::dispatchShardPipeline(
- serializedCommand, hasChangeStream, pipeline->clone());
+ serializedCommand,
+ hasChangeStream,
+ startsWithDocuments,
+ pipeline->clone(),
+ boost::none /*explain*/);
});
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 a57d0496a1d..eeb0831b55d 100644
--- a/src/mongo/db/pipeline/document_path_support.cpp
+++ b/src/mongo/db/pipeline/document_path_support.cpp
@@ -136,19 +136,18 @@ StatusWith<Value> extractElementAlongNonArrayPath(const Document& doc, const Fie
return curValue;
}
-BSONObj documentToBsonWithPaths(const Document& input, const std::set<std::string>& paths) {
- BSONObjBuilder outputBuilder;
+void documentToBsonWithPaths(const Document& input,
+ const OrderedPathSet& paths,
+ BSONObjBuilder* builder) {
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 (!outputBuilder.hasField(prefix)) {
+ if (!builder->hasField(prefix)) {
// Avoid adding the same prefix twice.
- input.getField(prefix).addToBsonObj(&outputBuilder, prefix);
+ input.getField(prefix).addToBsonObj(builder, 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 5d9f0a1cb6b..b1c127af0e4 100644
--- a/src/mongo/db/pipeline/document_path_support.h
+++ b/src/mongo/db/pipeline/document_path_support.h
@@ -63,7 +63,14 @@ StatusWith<Value> extractElementAlongNonArrayPath(const Document& doc, const Fie
/**
* Extracts 'paths' from the input document and returns a BSON object containing only those paths.
*/
-BSONObj documentToBsonWithPaths(const Document&, const std::set<std::string>& 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>();
+}
/**
* 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 5e966e08a75..3df55d54e90 100644
--- a/src/mongo/db/pipeline/document_path_support_test.cpp
+++ b/src/mongo/db/pipeline/document_path_support_test.cpp
@@ -42,6 +42,15 @@
#include "mongo/db/pipeline/field_path.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 document_path_support {
@@ -351,8 +360,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, std::set<std::string>{}));
+ ASSERT_BSONOBJ_EQ(expected,
+ document_path_support::documentToBsonWithPaths(input, OrderedPathSet{}));
}
TEST(DocumentToBsonWithPathsTest, ShouldExtractEntireArrayFromPrefixOfDottedField) {
@@ -361,6 +370,26 @@ 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.h b/src/mongo/db/pipeline/document_source.h
index ee3977aed18..a7188df709a 100644
--- a/src/mongo/db/pipeline/document_source.h
+++ b/src/mongo/db/pipeline/document_source.h
@@ -381,6 +381,16 @@ 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.
*
@@ -601,9 +611,7 @@ public:
kAllExcept,
};
- GetModPathsReturn(Type type,
- std::set<std::string>&& paths,
- StringMap<std::string>&& renames)
+ GetModPathsReturn(Type type, OrderedPathSet&& paths, StringMap<std::string>&& renames)
: type(type), paths(std::move(paths)), renames(std::move(renames)) {}
std::set<std::string> getNewNames() {
@@ -649,11 +657,11 @@ public:
return true;
}
// Cannot hit.
- MONGO_UNREACHABLE_TASSERT(6434901);
+ MONGO_UNREACHABLE_TASSERT(6434902);
}
Type type;
- std::set<std::string> paths;
+ OrderedPathSet 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
@@ -676,7 +684,7 @@ public:
* See GetModPathsReturn above for the possible return values and what they mean.
*/
virtual GetModPathsReturn getModifiedPaths() const {
- return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet{}, {}};
}
/**
@@ -714,7 +722,7 @@ public:
* parallel since it will preserve the shard key.
*/
virtual bool canRunInParallelBeforeWriteStage(
- const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const {
+ const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const {
return false;
}
diff --git a/src/mongo/db/pipeline/document_source_bucket_auto.cpp b/src/mongo/db/pipeline/document_source_bucket_auto.cpp
index 10300e9c334..b0349125a98 100644
--- a/src/mongo/db/pipeline/document_source_bucket_auto.cpp
+++ b/src/mongo/db/pipeline/document_source_bucket_auto.cpp
@@ -221,7 +221,7 @@ void DocumentSourceBucketAuto::initializeBucketIteration() {
auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx);
metricsCollector.incrementKeysSorted(_sorter->numSorted());
- metricsCollector.incrementSorterSpills(_sorter->numSpills());
+ metricsCollector.incrementSorterSpills(_sorter->stats().spilledRanges());
_sorter.reset();
diff --git a/src/mongo/db/pipeline/document_source_change_stream.cpp b/src/mongo/db/pipeline/document_source_change_stream.cpp
index 4fc0041cc0e..3f326a1959f 100644
--- a/src/mongo/db/pipeline/document_source_change_stream.cpp
+++ b/src/mongo/db/pipeline/document_source_change_stream.cpp
@@ -37,6 +37,7 @@
#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"
@@ -47,6 +48,7 @@
#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"
@@ -226,23 +228,6 @@ 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
@@ -272,6 +257,13 @@ 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));
+ }
+
// Save a copy of the spec on the expression context. Used when building the oplog filter.
expCtx->changeStreamSpec = spec;
@@ -287,15 +279,8 @@ 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 = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec);
+ auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec);
// Unfold the $changeStream into its constituent stages and add them to the pipeline.
stages.push_back(DocumentSourceChangeStreamOplogMatch::create(expCtx, spec));
@@ -310,11 +295,9 @@ 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));
- // 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));
- }
+ // 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 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,7 +307,6 @@ 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
@@ -444,7 +426,7 @@ void DocumentSourceChangeStream::assertIsLegalSpecification(
!spec.getResumeAfter() || !spec.getStartAfter());
auto resumeToken = (spec.getResumeAfter() || spec.getStartAfter())
- ? resolveResumeTokenFromSpec(expCtx, spec)
+ ? change_stream::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 60013e64444..044623335ed 100644
--- a/src/mongo/db/pipeline/document_source_change_stream.h
+++ b/src/mongo/db/pipeline/document_source_change_stream.h
@@ -243,9 +243,13 @@ public:
// Default regex for collections match which prohibits system collections.
static constexpr StringData kRegexAllCollections = R"((?!(\$|system\.)))"_sd;
- // Regex matching all regular collections plus certain system collections.
+
+ // 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.
static constexpr StringData kRegexAllCollectionsShowSystemEvents =
- R"((?!(\$|system\.(?!(js$)))))"_sd;
+ R"((?!(\$|system\.(?!(js$|buckets\.)))))"_sd;
static constexpr StringData kRegexAllDBs = R"(^(?!(admin|config|local)\.)[^.]+)"_sd;
static constexpr StringData kRegexCmdColl = R"(\$cmd$)"_sd;
@@ -283,15 +287,6 @@ 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_add_post_image.cpp b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp
index 051922556a5..aa663dd131f 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,6 +48,8 @@ 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];
@@ -97,12 +99,11 @@ 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, "
- "delete and replace events, but the post-image was not found for event: "
- << output.peek().toString(),
- postImageDoc || _fullDocumentMode != FullDocumentModeEnum::kRequired);
+ 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);
// Even if no post-image was found, we have to populate the 'fullDocument' field.
output[kFullDocumentFieldName] = (postImageDoc ? Value(*postImageDoc) : Value(BSONNULL));
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 97b252c9829..42b0cc20bfb 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: "
- << input.getDocument().toString(),
+ << makePreImageNotFoundErrorMsg(input.getDocument()),
_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: "
- << input.getDocument().toString(),
+ << makePreImageNotFoundErrorMsg(input.getDocument()),
preImageDoc ||
_fullDocumentBeforeChangeMode != FullDocumentBeforeChangeModeEnum::kRequired);
@@ -165,4 +165,13 @@ Value DocumentSourceChangeStreamAddPreImage::serialize(
DocumentSourceChangeStreamAddPreImageSpec(_fullDocumentBeforeChangeMode).toBSON()}});
}
+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();
+}
+
} // 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 cc735fc135e..e04ac9a30ef 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,6 +63,9 @@ 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) {
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 8121fedb3e7..4e85815b049 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,6 +31,7 @@
#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"
@@ -71,7 +72,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 = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec);
+ auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec);
return new DocumentSourceChangeStreamCheckInvalidate(
expCtx, boost::make_optional(resumeToken.fromInvalidate, std::move(resumeToken)));
}
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 3861b21693d..e7e05dcd756 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,6 +30,7 @@
#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"
@@ -47,16 +48,15 @@ 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).
+// 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.
DocumentSourceChangeStreamCheckResumability::ResumeStatus
DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken(
- const intrusive_ptr<ExpressionContext>& expCtx,
- const Document& documentFromResumedStream,
- const ResumeTokenData& tokenDataFromClient) {
+ const Document& eventFromResumedStream, const ResumeTokenData& tokenDataFromClient) {
// Parse the stream doc into comprehensible ResumeTokenData.
auto tokenDataFromResumedStream =
- ResumeToken::parse(documentFromResumedStream["_id"].getDocument()).getData();
+ ResumeToken::parse(eventFromResumedStream.metadata().getSortKey().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,21 +97,25 @@ 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 all the fields match exactly, then we have found the token.
+ // 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 (ValueComparator::kInstance.evaluate(tokenDataFromResumedStream.eventIdentifier ==
tokenDataFromClient.eventIdentifier)) {
- return ResumeStatus::kFoundToken;
+ 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;
}
// At this point, we know that the tokens differ only by eventIdentifier. The status we return
@@ -130,7 +134,7 @@ DocumentSourceChangeStreamCheckResumability::DocumentSourceChangeStreamCheckResu
intrusive_ptr<DocumentSourceChangeStreamCheckResumability>
DocumentSourceChangeStreamCheckResumability::create(const intrusive_ptr<ExpressionContext>& expCtx,
const DocumentSourceChangeStreamSpec& spec) {
- auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec);
+ auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec);
return new DocumentSourceChangeStreamCheckResumability(expCtx, std::move(resumeToken));
}
@@ -178,15 +182,21 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamCheckResumability::doGet
// Determine whether the current event sorts before, equal to or after the resume token.
_resumeStatus =
DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken(
- pExpCtx, nextInput.getDocument(), _tokenFromClient);
+ 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;
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 a290d59cacd..59946310ab1 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,7 +68,8 @@ 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.
+ 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.
};
const char* getSourceName() const override;
@@ -98,10 +99,8 @@ public:
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const DocumentSourceChangeStreamSpec& spec);
- static ResumeStatus compareAgainstClientResumeToken(
- const boost::intrusive_ptr<ExpressionContext>& expCtx,
- const Document& documentFromResumedStream,
- const ResumeTokenData& tokenDataFromClient);
+ static ResumeStatus compareAgainstClientResumeToken(const Document& eventFromResumedStream,
+ const ResumeTokenData& tokenDataFromClient);
protected:
/**
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 15fcb30d58f..420f7313161 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,6 +31,7 @@
#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"
@@ -45,7 +46,7 @@ boost::intrusive_ptr<DocumentSourceChangeStreamEnsureResumeTokenPresent>
DocumentSourceChangeStreamEnsureResumeTokenPresent::create(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const DocumentSourceChangeStreamSpec& spec) {
- auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec);
+ auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec);
tassert(5666902,
"Expected non-high-water-mark resume token",
!ResumeToken::isHighWaterMarkToken(resumeToken));
@@ -137,9 +138,10 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamEnsureResumeTokenPresent
const DocumentSource::GetNextResult nextInput =
Document::fromBsonWithMetaData(extraInfo->getStartAfterInvalidateEvent());
+
_resumeStatus =
DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken(
- pExpCtx, nextInput.getDocument(), _tokenFromClient);
+ nextInput.getDocument(), _tokenFromClient);
// This exception should always contain the client-provided resume token.
tassert(5779201,
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 3f24ed446ea..6fcabf4c0b6 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
@@ -47,7 +47,7 @@ public:
GetModPathsReturn getModifiedPaths() const final {
// This stage neither modifies nor renames any field.
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
static boost::intrusive_ptr<DocumentSourceChangeStreamEnsureResumeTokenPresent> create(
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 0f32b20aa47..b75009c2577 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
@@ -228,7 +228,8 @@ BSONObj DocumentSourceChangeStreamHandleTopologyChange::createUpdatedCommandForN
Document{shardCommand},
splitPipelines,
boost::none, /* exhangeSpec */
- true /* needsMerge */);
+ true /* needsMerge */,
+ boost::none /* explain */);
}
BSONObj DocumentSourceChangeStreamHandleTopologyChange::replaceResumeTokenInCommand(
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 c5d5a16fd93..4c07368aff9 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
@@ -67,7 +67,7 @@ public:
GetModPathsReturn getModifiedPaths() const final {
// This stage neither modifies nor renames any field.
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
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 c11cb0a1aeb..658861a9a3f 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,6 +31,7 @@
#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 {
@@ -104,7 +105,7 @@ DocumentSourceChangeStreamOplogMatch::DocumentSourceChangeStreamOplogMatch(
boost::intrusive_ptr<DocumentSourceChangeStreamOplogMatch>
DocumentSourceChangeStreamOplogMatch::create(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const DocumentSourceChangeStreamSpec& spec) {
- auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec);
+ auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec);
return make_intrusive<DocumentSourceChangeStreamOplogMatch>(resumeToken.clusterTime, expCtx);
}
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
new file mode 100644
index 00000000000..b2ac5353325
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp
@@ -0,0 +1,236 @@
+/**
+ * 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_ensure_resume_token_present.h"
+#include "mongo/db/pipeline/document_source_change_stream_handle_topology_change.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(
+ boost::optional<ExplainOptions::Verbosity> explain) 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;
+}
+
+namespace {
+// During pipeline optimization, the split stage must move ahead of these change stream stages.
+static const std::set<StringData> kStagesToMoveAheadOf = {
+ DocumentSourceChangeStreamEnsureResumeTokenPresent::kStageName,
+ DocumentSourceChangeStreamHandleTopologyChange::kStageName};
+} // namespace
+
+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 kStagesToMoveAheadOf.count(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)); --it) {
+ std::swap(*it, *std::prev(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 kStagesToMoveAheadOf.count(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
new file mode 100644
index 00000000000..1dfab7b85a1
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h
@@ -0,0 +1,123 @@
+/**
+ * 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;
+
+ // This stage does not reference any user or system variables.
+ void addVariableRefs(std::set<Variables::Id>* refs) const {}
+
+ Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final;
+
+ 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_transform.cpp b/src/mongo/db/pipeline/document_source_change_stream_transform.cpp
index 766150a6e54..7434190a609 100644
--- a/src/mongo/db/pipeline/document_source_change_stream_transform.cpp
+++ b/src/mongo/db/pipeline/document_source_change_stream_transform.cpp
@@ -33,6 +33,7 @@
#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"
@@ -63,6 +64,10 @@ 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));
}
@@ -74,8 +79,7 @@ DocumentSourceChangeStreamTransform::DocumentSourceChangeStreamTransform(
_isIndependentOfAnyCollection(expCtx->ns.isCollectionlessAggregateNS()) {
// Extract the resume token or high-water-mark from the spec.
- auto tokenData =
- DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, _changeStreamSpec);
+ auto tokenData = change_stream::resolveResumeTokenFromSpec(expCtx, _changeStreamSpec);
// Set the initialPostBatchResumeToken on the expression context.
expCtx->initialPostBatchResumeToken = ResumeToken(tokenData).toBSON();
@@ -118,7 +122,7 @@ DepsTracker::State DocumentSourceChangeStreamTransform::getDependencies(DepsTrac
DocumentSource::GetModPathsReturn DocumentSourceChangeStreamTransform::getModifiedPaths() const {
// All paths are modified.
- return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}};
+ return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}};
}
DocumentSource::GetNextResult DocumentSourceChangeStreamTransform::doGetNext() {
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 05bfcff4df7..ae65c696d65 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
@@ -157,7 +157,7 @@ DepsTracker::State DocumentSourceChangeStreamUnwindTransaction::getDependencies(
DocumentSource::GetModPathsReturn DocumentSourceChangeStreamUnwindTransaction::getModifiedPaths()
const {
- return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}};
+ return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}};
}
DocumentSource::GetNextResult DocumentSourceChangeStreamUnwindTransaction::doGetNext() {
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 28603f10a30..2d5eaf32521 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,10 +206,6 @@ 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.
@@ -228,6 +224,7 @@ 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:
@@ -483,8 +480,12 @@ TEST_F(CheckResumeTokenTest, ShouldFailIfTokenHasWrongNamespace) {
Timestamp resumeTimestamp(100, 1);
auto resumeTokenUUID = UUID::gen();
- auto checkResumeToken = createDSEnsureResumeTokenPresent(resumeTimestamp, "1", resumeTokenUUID);
auto otherUUID = UUID::gen();
+ ASSERT_NE(resumeTokenUUID, otherUUID);
+ if (resumeTokenUUID > otherUUID) {
+ std::swap(resumeTokenUUID, otherUUID);
+ }
+ auto checkResumeToken = createDSEnsureResumeTokenPresent(resumeTimestamp, "1", resumeTokenUUID);
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 c00f5fcd44d..2881425d5e4 100644
--- a/src/mongo/db/pipeline/document_source_coll_stats.cpp
+++ b/src/mongo/db/pipeline/document_source_coll_stats.cpp
@@ -71,18 +71,15 @@ intrusive_ptr<DocumentSource> DocumentSourceCollStats::createFromBson(
return make_intrusive<DocumentSourceCollStats>(pExpCtx, std::move(spec));
}
-DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() {
- if (_finished) {
- return GetNextResult::makeEOF();
- }
-
- _finished = true;
-
+BSONObj DocumentSourceCollStats::makeStatsForNs(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const NamespaceString& nss,
+ const DocumentSourceCollStatsSpec& spec) {
BSONObjBuilder builder;
- builder.append("ns", pExpCtx->ns.ns());
+ builder.append("ns", nss.ns());
- auto shardName = pExpCtx->mongoProcessInterface->getShardName(pExpCtx->opCtx);
+ auto shardName = expCtx->mongoProcessInterface->getShardName(expCtx->opCtx);
if (!shardName.empty()) {
builder.append("shard", shardName);
@@ -91,33 +88,45 @@ DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() {
builder.append("host", getHostNameCachedAndPort());
builder.appendDate("localTime", jsTime());
- if (auto latencyStatsSpec = _collStatsSpec.getLatencyStats()) {
- pExpCtx->mongoProcessInterface->appendLatencyStats(
- pExpCtx->opCtx, pExpCtx->ns, latencyStatsSpec->getHistograms(), &builder);
+ 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 storageStats = _collStatsSpec.getStorageStats()) {
+ if (auto storageStats = spec.getStorageStats()) {
// If the storageStats field exists, it must have been validated as an object when parsing.
BSONObjBuilder storageBuilder(builder.subobjStart("storageStats"));
- uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendStorageStats(
- pExpCtx->opCtx, pExpCtx->ns, *storageStats, &storageBuilder),
+ uassertStatusOKWithContext(expCtx->mongoProcessInterface->appendStorageStats(
+ expCtx->opCtx, nss, *storageStats, &storageBuilder),
"Unable to retrieve storageStats in $collStats stage");
storageBuilder.doneFast();
}
- if (_collStatsSpec.getCount()) {
- uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendRecordCount(
- pExpCtx->opCtx, pExpCtx->ns, &builder),
- "Unable to retrieve count in $collStats stage");
+ if (spec.getCount()) {
+ uassertStatusOKWithContext(
+ expCtx->mongoProcessInterface->appendRecordCount(expCtx->opCtx, nss, &builder),
+ "Unable to retrieve count in $collStats stage");
}
- if (_collStatsSpec.getQueryExecStats()) {
- uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendQueryExecStats(
- pExpCtx->opCtx, pExpCtx->ns, &builder),
- "Unable to retrieve queryExecStats in $collStats stage");
+ if (spec.getQueryExecStats()) {
+ uassertStatusOKWithContext(
+ expCtx->mongoProcessInterface->appendQueryExecStats(expCtx->opCtx, nss, &builder),
+ "Unable to retrieve queryExecStats in $collStats stage");
}
+ return builder.obj();
+}
+
+DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() {
+ if (_finished) {
+ return GetNextResult::makeEOF();
+ }
+
+ _finished = true;
- return {Document(builder.obj())};
+ return {Document(makeStatsForNs(pExpCtx, pExpCtx->ns, _collStatsSpec))};
}
Value DocumentSourceCollStats::serialize(boost::optional<ExplainOptions::Verbosity> explain) const {
diff --git a/src/mongo/db/pipeline/document_source_coll_stats.h b/src/mongo/db/pipeline/document_source_coll_stats.h
index 72c6923ccdc..074683367e1 100644
--- a/src/mongo/db/pipeline/document_source_coll_stats.h
+++ b/src/mongo/db/pipeline/document_source_coll_stats.h
@@ -85,6 +85,10 @@ 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)) {}
diff --git a/src/mongo/db/pipeline/document_source_coll_stats.idl b/src/mongo/db/pipeline/document_source_coll_stats.idl
index 060c20d835a..264a4004a5c 100644
--- a/src/mongo/db/pipeline/document_source_coll_stats.idl
+++ b/src/mongo/db/pipeline/document_source_coll_stats.idl
@@ -39,7 +39,7 @@ structs:
LatencyStatsSpec:
description: Represents the 'latencyStats' argument to the $collStats stage.
strict: true
- fields:
+ fields:
histograms:
description: Adds latency histogram information to the embedded documents in latencyStats if true.
type: optionalBool
@@ -67,3 +67,8 @@ structs:
validator:
callback: validateObjectIsEmpty
optional: true
+ $_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
diff --git a/src/mongo/db/pipeline/document_source_densify.cpp b/src/mongo/db/pipeline/document_source_densify.cpp
index 740ef193431..ab43e159692 100644
--- a/src/mongo/db/pipeline/document_source_densify.cpp
+++ b/src/mongo/db/pipeline/document_source_densify.cpp
@@ -335,10 +335,19 @@ 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(),
diff --git a/src/mongo/db/pipeline/document_source_documents.cpp b/src/mongo/db/pipeline/document_source_documents.cpp
index 5d4e02c71ea..934116a7c27 100644
--- a/src/mongo/db/pipeline/document_source_documents.cpp
+++ b/src/mongo/db/pipeline/document_source_documents.cpp
@@ -44,7 +44,7 @@ namespace mongo {
using boost::intrusive_ptr;
REGISTER_DOCUMENT_SOURCE(documents,
- LiteParsedDocumentSourceDefault::parse,
+ DocumentSourceDocuments::LiteParsed::parse,
DocumentSourceDocuments::createFromBson,
AllowedWithApiStrict::kAlways);
diff --git a/src/mongo/db/pipeline/document_source_documents.h b/src/mongo/db/pipeline/document_source_documents.h
index a6ab70e0c59..6048ed551bb 100644
--- a/src/mongo/db/pipeline/document_source_documents.h
+++ b/src/mongo/db/pipeline/document_source_documents.h
@@ -35,6 +35,30 @@
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 isDocuments() const final {
+ return true;
+ }
+
+ bool allowedToPassthroughFromMongos() const final {
+ return false;
+ }
+};
static constexpr StringData kStageName = "$documents"_sd;
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 2fc1208e2b9..13229ba6d3e 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
@@ -199,7 +199,7 @@ DepsTracker::State DocumentSourceFindAndModifyImageLookup::getDependencies(
}
DocumentSource::GetModPathsReturn DocumentSourceFindAndModifyImageLookup::getModifiedPaths() const {
- return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}};
+ return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}};
}
DocumentSource::GetNextResult DocumentSourceFindAndModifyImageLookup::doGetNext() {
diff --git a/src/mongo/db/pipeline/document_source_geo_near.h b/src/mongo/db/pipeline/document_source_geo_near.h
index 83ddd02bb88..2b012285560 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::kFirstAfterOptimization,
+ PositionRequirement::kCustom,
HostTypeRequirement::kAnyShard,
DiskUseRequirement::kNoDiskUse,
FacetRequirement::kNotAllowed,
@@ -60,6 +60,17 @@ 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.
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 27e72886225..8de9f300ab9 100644
--- a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp
+++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp
@@ -77,9 +77,6 @@ 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_graph_lookup.cpp b/src/mongo/db/pipeline/document_source_graph_lookup.cpp
index 417e92df4b0..8c1076d3d76 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 {
- std::set<std::string> modifiedPaths{_as.fullPath()};
+ OrderedPathSet modifiedPaths{_as.fullPath()};
if (_unwind) {
auto pathsModifiedByUnwind = _unwind.get()->getModifiedPaths();
invariant(pathsModifiedByUnwind.type == GetModPathsReturn::Type::kFiniteSet);
@@ -623,6 +623,7 @@ 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.
diff --git a/src/mongo/db/pipeline/document_source_graph_lookup.h b/src/mongo/db/pipeline/document_source_graph_lookup.h
index bc0146f1829..a4ad79c8111 100644
--- a/src/mongo/db/pipeline/document_source_graph_lookup.h
+++ b/src/mongo/db/pipeline/document_source_graph_lookup.h
@@ -132,6 +132,10 @@ 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;
};
diff --git a/src/mongo/db/pipeline/document_source_group.cpp b/src/mongo/db/pipeline/document_source_group.cpp
index 3f718360838..f437debe890 100644
--- a/src/mongo/db/pipeline/document_source_group.cpp
+++ b/src/mongo/db/pipeline/document_source_group.cpp
@@ -45,82 +45,6 @@
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,
@@ -132,449 +56,70 @@ const char* DocumentSourceGroup::getSourceName() const {
return kStageName.rawData();
}
-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,
+boost::intrusive_ptr<DocumentSourceGroup> DocumentSourceGroup::create(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
const boost::intrusive_ptr<Expression>& groupByExpression,
std::vector<AccumulationStatement> accumulationStatements,
boost::optional<size_t> maxMemoryUsageBytes) {
- intrusive_ptr<DocumentSourceGroup> groupStage(
- new DocumentSourceGroup(expCtx, maxMemoryUsageBytes));
+ boost::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 intrusive_ptr<ExpressionContext>& expCtx,
+DocumentSourceGroup::DocumentSourceGroup(const boost::intrusive_ptr<ExpressionContext>& expCtx,
boost::optional<size_t> maxMemoryUsageBytes)
- : 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) {}
+ : DocumentSourceGroupBase(kStageName, expCtx, maxMemoryUsageBytes), _groupsReady(false) {}
-void DocumentSourceGroup::addAccumulator(AccumulationStatement accumulationStatement) {
- _accumulatedFields.push_back(accumulationStatement);
+boost::intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBson(
+ BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) {
+ return createFromBsonWithMaxMemoryUsage(std::move(elem), expCtx, boost::none);
}
-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);
- }
+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
-
-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);
+DocumentSource::GetNextResult DocumentSourceGroup::doGetNext() {
+ if (!_groupsReady) {
+ const auto initializationResult = performBlockingGroup();
+ if (initializationResult.isPaused()) {
+ return initializationResult;
}
- } else {
- _idExpressions.push_back(idExpression);
- }
-}
-
-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]);
+ invariant(initializationResult.isEOF());
}
- return ExpressionObject::create(_idExpressions[0]->getExpressionContext(),
- std::move(fieldsAndExprs));
-}
-
-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);
- }
+ auto result = getNextReadyGroup();
+ if (result.isEOF()) {
+ dispose();
}
- groupStage->_sbeCompatible = expCtx->sbeGroupCompatible && expCtx->sbeCompatible;
-
- uassert(
- 15955, "a group specification must include an _id", !groupStage->_idExpressions.empty());
- return groupStage;
+ return result;
}
-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() {
+DocumentSource::GetNextResult DocumentSourceGroup::performBlockingGroup() {
GetNextResult input = pSource->getNext();
- return initializeSelf(input);
+ return performBlockingGroupSelf(input);
}
-// This separate NOINLINE function is used here to decrease stack utilization of initialize() and
-// prevent stack overflows.
-MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initializeSelf(
+// This separate NOINLINE function is used here to decrease stack utilization of
+// performBlockingGroup() and prevent stack overflows.
+MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::performBlockingGroupSelf(
GetNextResult input) {
- const size_t numAccumulators = _accumulatedFields.size();
+ setExecutionStarted();
// Barring any pausing, this loop exhausts 'pSource' and populates '_groups'.
for (; input.isAdvanced(); input = pSource->getNext()) {
if (shouldSpillWithAttemptToSaveMemory()) {
- _sortedFiles.push_back(spill());
+ spill();
}
// We release the result document here so that it does not outlive the end of this loop
@@ -582,58 +127,7 @@ MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initi
auto rootDocument = input.releaseDocument();
Value id = computeId(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());
- }
- }
+ processDocument(id, rootDocument);
}
switch (input.getStatus()) {
@@ -644,280 +138,14 @@ MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initi
return input; // Propagate pause.
}
case DocumentSource::GetNextResult::ReturnStatus::kEOF: {
- // 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();
- }
-
+ readyGroups();
// This must happen last so that, unless control gets here, we will re-enter
// initialization after getting a GetNextResult::ResultState::kPauseExecution.
- _initialized = true;
+ _groupsReady = 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 edffed6758b..a1f8e9b2b9a 100644
--- a/src/mongo/db/pipeline/document_source_group.h
+++ b/src/mongo/db/pipeline/document_source_group.h
@@ -32,75 +32,19 @@
#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/memory_usage_tracker.h"
-#include "mongo/db/pipeline/transformer_interface.h"
-#include "mongo/db/sorter/sorter.h"
+#include "mongo/db/pipeline/document_source_group_base.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.
+ * This class represents hash based group implementation that stores all groups until source is
+ * depleted and only then starts outputing documents.
*/
-class GroupFromFirstDocumentTransformation final : public TransformerInterface {
+class DocumentSourceGroup final : public DocumentSourceGroupBase {
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,
@@ -118,199 +62,40 @@ public:
*/
static boost::intrusive_ptr<DocumentSource> createFromBson(
BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx);
-
- 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;
- }
-
- 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;
- }
+ static boost::intrusive_ptr<DocumentSource> createFromBsonWithMaxMemoryUsage(
+ BSONElement elem,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ boost::optional<size_t> maxMemoryUsageBytes);
protected:
GetNextResult doGetNext() final;
- void doDispose() final;
+
+ bool isSpecFieldReserved(StringData) final {
+ return false;
+ }
private:
explicit DocumentSourceGroup(const boost::intrusive_ptr<ExpressionContext>& expCtx,
boost::optional<size_t> maxMemoryUsageBytes = boost::none);
/**
- * 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.
+ * Before returning anything, this source must prepare itself. performBlockingGroup() exhausts
+ * the previous source before
+ * returning. The '_groupsReady' boolean indicates that performBlockingGroup() 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 initialize();
+ GetNextResult performBlockingGroup();
/**
- * Initializes this $group after any children are potentially initialized see initialize() for
+ * Initializes this $group after any children are initialized. See performBlockingGroup() for
* more details.
*/
- 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;
+ GetNextResult performBlockingGroupSelf(GetNextResult input);
- bool _sbeCompatible;
+ bool _groupsReady;
};
} // namespace mongo
diff --git a/src/mongo/db/pipeline/document_source_group_base.cpp b/src/mongo/db/pipeline/document_source_group_base.cpp
new file mode 100644
index 00000000000..0268cd57127
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_group_base.cpp
@@ -0,0 +1,811 @@
+/**
+ * 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(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) {
+ insides["$doingMerge"] = Value(true);
+ }
+
+ serializeAdditionalFields(insides, explain);
+
+ MutableDocument out;
+ out[getSourceName()] = insides.freezeToValue();
+
+ 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 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
new file mode 100644
index 00000000000..76d099fef78
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_group_base.h
@@ -0,0 +1,266 @@
+/**
+ * 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(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final;
+ 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;
+ }
+
+ 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, boost::optional<ExplainOptions::Verbosity> explain) 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 d3ed11f2031..9a621cffdcd 100644
--- a/src/mongo/db/pipeline/document_source_group_test.cpp
+++ b/src/mongo/db/pipeline/document_source_group_test.cpp
@@ -48,6 +48,7 @@
#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"
@@ -252,32 +253,65 @@ BSONObj toBson(const intrusive_ptr<DocumentSource>& source) {
return arr[0].getDocument().toBson();
}
+enum class GroupStageType { Default, Streaming };
+
class Base : public ServiceContextTest {
public:
- Base()
+ Base(GroupStageType groupStageType = GroupStageType::Default)
: _opCtx(makeOperationContext()),
_ctx(new ExpressionContextForTest(_opCtx.get(),
AggregateCommandRequest(NamespaceString(ns), {}))),
- _tempDir("DocumentSourceGroupTest") {}
+ _tempDir("DocumentSourceGroupTest"),
+ _groupStageType(groupStageType) {}
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("$group" << spec);
+ BSONObj namedSpec = BSON(getStageName() << 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 = DocumentSourceGroup::createFromBson(specElement, expressionContext);
+ _group = createFromBson(specElement, expressionContext);
assertRoundTrips(_group, expressionContext);
}
- DocumentSourceGroup* group() {
- return static_cast<DocumentSourceGroup*>(_group.get());
+ DocumentSourceGroupBase* group() {
+ return static_cast<DocumentSourceGroupBase*>(_group.get());
}
/** Assert that iterator state accessors consistently report the source is exhausted. */
void assertEOF(const intrusive_ptr<DocumentSource>& source) const {
@@ -299,8 +333,7 @@ private:
// $const operators may be introduced in the first serialization.
BSONObj spec = toBson(group);
BSONElement specElement = spec.firstElement();
- intrusive_ptr<DocumentSource> generated =
- DocumentSourceGroup::createFromBson(specElement, expCtx);
+ intrusive_ptr<DocumentSource> generated = createFromBson(specElement, expCtx);
ASSERT_BSONOBJ_EQ(spec, toBson(generated));
}
std::unique_ptr<QueryTestServiceContext> _queryServiceContext;
@@ -308,6 +341,7 @@ private:
intrusive_ptr<ExpressionContextForTest> _ctx;
intrusive_ptr<DocumentSource> _group;
TempDir _tempDir;
+ GroupStageType _groupStageType;
};
class ParseErrorBase : public Base {
@@ -355,10 +389,9 @@ class IdConstantBase : public ExpressionBase {
class NonObject : public Base {
public:
void _doTest() final {
- BSONObj spec = BSON("$group"
- << "foo");
+ BSONObj spec = BSON(getStageName() << "foo");
BSONElement specElement = spec.firstElement();
- ASSERT_THROWS(DocumentSourceGroup::createFromBson(specElement, ctx()), AssertionException);
+ ASSERT_THROWS(createFromBson(specElement, ctx()), AssertionException);
}
};
@@ -557,8 +590,10 @@ typedef map<Value, Document, ValueCmp> IdMap;
class CheckResultsBase : public Base {
public:
+ CheckResultsBase(GroupStageType groupStageType = GroupStageType::Default)
+ : Base(groupStageType) {}
virtual ~CheckResultsBase() {}
- void _doTest() {
+ void _doTest() override {
runSharded(false);
runSharded(true);
}
@@ -571,7 +606,7 @@ public:
if (sharded) {
sink = createMerger();
// Serialize and re-parse the shard stage.
- createGroup(toBson(group())["$group"].Obj(), true);
+ createGroup(toBson(group())[group()->getSourceName()].Obj(), true);
group()->setSource(source.get());
sink->setSource(group());
}
@@ -871,6 +906,277 @@ 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") {}
@@ -911,6 +1217,14 @@ 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 e87b614e59a..139fdde3bca 100644
--- a/src/mongo/db/pipeline/document_source_index_stats.cpp
+++ b/src/mongo/db/pipeline/document_source_index_stats.cpp
@@ -51,7 +51,10 @@ const char* DocumentSourceIndexStats::getSourceName() const {
DocumentSource::GetNextResult DocumentSourceIndexStats::doGetNext() {
if (_indexStats.empty()) {
_indexStats = pExpCtx->mongoProcessInterface->getIndexStats(
- pExpCtx->opCtx, pExpCtx->ns, _processName, pExpCtx->fromMongos);
+ pExpCtx->opCtx,
+ pExpCtx->ns,
+ _processName,
+ serverGlobalParams.clusterRole != ClusterRole::None);
_indexStatsIter = _indexStats.cbegin();
}
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
new file mode 100644
index 00000000000..6c9693e0443
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp
@@ -0,0 +1,163 @@
+/**
+ * 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, boost::optional<ExplainOptions::Verbosity> explain) const {
+ if (explain) {
+ BSONObjBuilder bob;
+ _internalAllCollectionStatsSpec.serialize(&bob);
+ if (_absorbedMatch) {
+ bob.append("match", _absorbedMatch->getQuery());
+ }
+ auto doc = Document{{getSourceName(), bob.obj()}};
+ array.push_back(Value(doc));
+ } else {
+ array.push_back(serialize(explain));
+ 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(
+ boost::optional<ExplainOptions::Verbosity> explain) const {
+ return Value(Document{{getSourceName(), _internalAllCollectionStatsSpec.toBSON()}});
+}
+} // 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
new file mode 100644
index 00000000000..38324fb51ad
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h
@@ -0,0 +1,127 @@
+/**
+ * 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(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final;
+
+ 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,
+ boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final;
+
+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
new file mode 100644
index 00000000000..61fc5d02f03
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl
@@ -0,0 +1,47 @@
+# 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
+ fields:
+ stats:
+ description: Specification for a $collStats stage.
+ type: DocumentSourceCollStatsSpec
+ optional: true
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 1083d58bb7f..b1a24787224 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,9 +76,7 @@ public:
}
DocumentSource::GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kFiniteSet,
- std::set<std::string>{_distanceField.fullPath()},
- {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{_distanceField.fullPath()}, {}};
}
boost::optional<DistributedPlanLogic> distributedPlanLogic() override {
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 272d7a1a80c..1c0dce7ef34 100644
--- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp
+++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp
@@ -29,6 +29,7 @@
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
+#include "mongo/db/pipeline/document_source_sequential_document_cache.h"
#include <algorithm>
#include <iterator>
@@ -55,6 +56,7 @@
#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"
@@ -243,9 +245,39 @@ 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,
@@ -255,14 +287,20 @@ 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) {
@@ -287,8 +325,8 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF
field.find('.') == std::string::npos);
bucketSpec.addIncludeExcludeField(field);
}
- unpackerBehavior = fieldName == kInclude ? BucketUnpacker::Behavior::kInclude
- : BucketUnpacker::Behavior::kExclude;
+ bucketSpec.setBehavior(fieldName == kInclude ? BucketSpec::Behavior::kInclude
+ : BucketSpec::Behavior::kExclude);
hasIncludeExclude = true;
} else if (fieldName == kAssumeNoMixedSchemaData) {
uassert(6067202,
@@ -350,6 +388,24 @@ 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()
@@ -364,11 +420,12 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF
"The $_internalUnpackBucket stage requires a bucketMaxSpanSeconds parameter",
hasBucketMaxSpanSeconds);
- return make_intrusive<DocumentSourceInternalUnpackBucket>(
- expCtx,
- BucketUnpacker{std::move(bucketSpec), unpackerBehavior},
- bucketMaxSpanSeconds,
- assumeClean);
+ return make_intrusive<DocumentSourceInternalUnpackBucket>(expCtx,
+ BucketUnpacker{std::move(bucketSpec)},
+ bucketMaxSpanSeconds,
+ eventFilterBson,
+ wholeBucketFilterBson,
+ assumeClean);
}
boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createFromBsonExternal(
@@ -414,26 +471,23 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF
hasTimeField);
return make_intrusive<DocumentSourceInternalUnpackBucket>(
- expCtx,
- BucketUnpacker{std::move(bucketSpec), BucketUnpacker::Behavior::kExclude},
- 3600,
- assumeClean);
+ expCtx, BucketUnpacker{std::move(bucketSpec)}, 3600, assumeClean);
}
void DocumentSourceInternalUnpackBucket::serializeToArray(
std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const {
MutableDocument out;
auto behavior =
- _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kInclude ? kInclude : kExclude;
+ _bucketUnpacker.behavior() == BucketSpec::Behavior::kInclude ? kInclude : kExclude;
const auto& spec = _bucketUnpacker.bucketSpec();
std::vector<Value> fields;
for (auto&& field : spec.fieldSet()) {
fields.emplace_back(field);
}
if (((_bucketUnpacker.includeMetaField() &&
- _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kInclude) ||
+ _bucketUnpacker.behavior() == BucketSpec::Behavior::kInclude) ||
(!_bucketUnpacker.includeMetaField() &&
- _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kExclude && spec.metaField())) &&
+ _bucketUnpacker.behavior() == BucketSpec::Behavior::kExclude && spec.metaField())) &&
std::find(spec.computedMetaProjFields().cbegin(),
spec.computedMetaProjFields().cend(),
*spec.metaField()) == spec.computedMetaProjFields().cend())
@@ -448,6 +502,14 @@ void DocumentSourceInternalUnpackBucket::serializeToArray(
if (_assumeNoMixedSchemaData)
out.addField(kAssumeNoMixedSchemaData, 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, Value{true});
+ }
+
if (!spec.computedMetaProjFields().empty())
out.addField("computedMetaProjFields", Value{[&] {
std::vector<Value> compFields;
@@ -465,6 +527,13 @@ void DocumentSourceInternalUnpackBucket::serializeToArray(
out.addField(kIncludeMaxTimeAsMetadata, Value{_bucketUnpacker.includeMaxTimeAsMetadata()});
}
+ if (_wholeBucketFilter) {
+ out.addField(kWholeBucketFilter, Value{_wholeBucketFilter->serialize()});
+ }
+ if (_eventFilter) {
+ out.addField(kEventFilter, Value{_eventFilter->serialize()});
+ }
+
if (!explain) {
array.push_back(Value(DOC(getSourceName() << out.freeze())));
if (_sampleSize) {
@@ -480,25 +549,58 @@ void DocumentSourceInternalUnpackBucket::serializeToArray(
}
}
+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 (_bucketUnpacker.hasNext()) {
- return _bucketUnpacker.getNext();
+ if (auto measure = getNextMatchingMeasure()) {
+ return GetNextResult(std::move(*measure));
}
auto nextResult = pSource->getNext();
- if (nextResult.isAdvanced()) {
+ while (nextResult.isAdvanced()) {
auto bucket = nextResult.getDocument().toBson();
- _bucketUnpacker.reset(std::move(bucket));
+ auto bucketMatchedQuery = _wholeBucketFilter && _wholeBucketFilter->matchesBSON(bucket);
+ _bucketUnpacker.reset(std::move(bucket), bucketMatchedQuery);
+
uassert(5346509,
str::stream() << "A bucket with _id "
<< _bucketUnpacker.bucket()[timeseries::kBucketIdFieldName].toString()
<< " contains an empty data region",
_bucketUnpacker.hasNext());
- return _bucketUnpacker.getNext();
+ if (auto measure = getNextMatchingMeasure()) {
+ return GetNextResult(std::move(*measure));
+ }
+ nextResult = pSource->getNext();
}
return nextResult;
@@ -510,7 +612,7 @@ bool DocumentSourceInternalUnpackBucket::pushDownComputedMetaProjection(
if (std::next(itr) == container->end()) {
return nextStageWasRemoved;
}
- if (!_bucketUnpacker.bucketSpec().metaField()) {
+ if (!_bucketUnpacker.getMetaField() || !_bucketUnpacker.includeMetaField()) {
return nextStageWasRemoved;
}
@@ -562,9 +664,8 @@ void DocumentSourceInternalUnpackBucket::internalizeProject(const BSONObj& proje
// Update '_bucketUnpacker' state with the new fields and behavior.
auto spec = _bucketUnpacker.bucketSpec();
spec.setFieldSet(fields);
- _bucketUnpacker.setBucketSpecAndBehavior(std::move(spec),
- isInclusion ? BucketUnpacker::Behavior::kInclude
- : BucketUnpacker::Behavior::kExclude);
+ spec.setBehavior(isInclusion ? BucketSpec::Behavior::kInclude : BucketSpec::Behavior::kExclude);
+ _bucketUnpacker.setBucketSpec(std::move(spec));
}
std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProjectToInternalize(
@@ -576,7 +677,8 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje
// Check for a viable inclusion $project after the $_internalUnpackBucket.
auto [existingProj, isInclusion] = getIncludeExcludeProjectAndType(std::next(itr)->get());
- if (isInclusion && !existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) {
+ if (!_eventFilter && isInclusion && !existingProj.isEmpty() &&
+ canInternalizeProjectObj(existingProj)) {
container->erase(std::next(itr));
return {existingProj, isInclusion};
}
@@ -584,8 +686,7 @@ 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.
- Pipeline::SourceContainer restOfPipeline(std::next(itr), container->end());
- auto deps = Pipeline::getDependenciesForContainer(pExpCtx, restOfPipeline, boost::none);
+ auto deps = getRestPipelineDependencies(itr, container, true /* includeEventFilter */);
if (auto dependencyProj =
deps.toProjectionWithoutMetadata(DepsTracker::TruncateToRootLevel::yes);
!dependencyProj.isEmpty()) {
@@ -593,7 +694,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje
}
// Check for a viable exclusion $project after the $_internalUnpackBucket.
- if (!existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) {
+ if (!_eventFilter && !existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) {
container->erase(std::next(itr));
return {existingProj, isInclusion};
}
@@ -601,8 +702,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje
return {BSONObj{}, false};
}
-std::unique_ptr<MatchExpression>
-DocumentSourceInternalUnpackBucket::createPredicatesOnBucketLevelField(
+BucketSpec::BucketPredicate DocumentSourceInternalUnpackBucket::createPredicatesOnBucketLevelField(
const MatchExpression* matchExpr) const {
return BucketSpec::createPredicatesOnBucketLevelField(
matchExpr,
@@ -631,58 +731,102 @@ 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();
- if (idFields.size() != 1 || !_bucketUnpacker.bucketSpec().metaField().has_value()) {
+
+ // 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) {
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();
- if (idPath.getPathLength() < 2 ||
- idPath.getFieldName(1) != _bucketUnpacker.bucketSpec().metaField().get()) {
+ // {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) {
return {};
}
- bool suitable = true;
- std::vector<AccumulationStatement> accumulationStatements;
+ std::vector<AccumulationStatement> accumulationStatementsBucket;
for (const AccumulationStatement& stmt : groupPtr->getAccumulatedFields()) {
- const auto op = stmt.expr.name;
- const bool isMin = op == "$min";
- const bool isMax = op == "$max";
+ 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 {};
+ }
- // Rewrite is valid only for min and max aggregates.
- if (!isMin && !isMax) {
- suitable = false;
- break;
+ 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 {};
}
- 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;
- }
+ // 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);
+
+ // 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 {};
+ }
+ // 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);
+ }
+ } else {
// Update aggregates to reference the control field.
- std::ostringstream os;
- if (isMin) {
+ const auto op = stmt.expr.name;
+ if (op == "$min") {
os << timeseries::kControlMinFieldNamePrefix;
- } else {
+ } else if (op == "$max") {
os << timeseries::kControlMaxFieldNamePrefix;
+ } else {
+ MONGO_UNREACHABLE;
}
for (size_t index = 1; index < path.getPathLength(); index++) {
@@ -691,45 +835,43 @@ DocumentSourceInternalUnpackBucket::rewriteGroupByMinMax(Pipeline::SourceContain
}
os << path.getFieldName(index);
}
-
- const auto& newExpr = ExpressionFieldPath::createPathFromString(
- pExpCtx.get(), os.str(), pExpCtx->variablesParseState);
-
- AccumulationExpression accExpr = stmt.expr;
- accExpr.argument = newExpr;
- accumulationStatements.emplace_back(stmt.fieldName, std::move(accExpr));
}
- }
- 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(
+ // Re-create the accumulator using the bucket-level paths.
+ const auto& newExpr = ExpressionFieldPath::createPathFromString(
pExpCtx.get(), os.str(), pExpCtx->variablesParseState);
- auto newGroup = DocumentSourceGroup::create(pExpCtx,
- std::move(exprId1),
- std::move(accumulationStatements),
- groupPtr->getMaxMemoryUsageBytes());
-
- // 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 group stage.
- return {true, itr};
- } else {
- // Give chance of the previous stage to optimize against group stage.
- return {true, std::prev(itr)};
- }
+ AccumulationExpression accExpr = stmt.expr;
+ accExpr.argument = newExpr;
+ accumulationStatementsBucket.emplace_back(stmt.fieldName, std::move(accExpr));
}
- return {};
+ // 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);
+ }
+ auto exprIdBucket = ExpressionFieldPath::createPathFromString(
+ pExpCtx.get(), os.str(), pExpCtx->variablesParseState);
+
+ auto newGroup = DocumentSourceGroup::create(pExpCtx,
+ std::move(exprIdBucket),
+ std::move(accumulationStatementsBucket),
+ 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);
+
+ 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)};
+ }
}
bool DocumentSourceInternalUnpackBucket::haveComputedMetaField() const {
@@ -738,6 +880,68 @@ 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,
@@ -980,6 +1184,27 @@ 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;
+}
+
+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);
@@ -993,7 +1218,8 @@ 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())) {
+ if (auto sortPtr = dynamic_cast<DocumentSourceSort*>(std::next(itr)->get());
+ sortPtr && !_eventFilter) {
if (auto metaField = _bucketUnpacker.bucketSpec().metaField();
metaField && !haveComputedMetaField) {
if (checkMetadataSortReorder(sortPtr->getSortKeyPattern(), metaField.get())) {
@@ -1024,7 +1250,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi
}
// Attempt to push geoNear on the metaField past $_internalUnpackBucket.
- if (auto nextNear = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get())) {
+ if (auto nextNear = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get());
+ nextNear && !_eventFilter) {
// 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();
@@ -1073,8 +1300,19 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi
// the first stage, because it expects to use a special DocumentSouceGeoNearCursor plan.
nextStage->optimizeAt(std::next(itr), container);
}
+ auto cacheFound = findSequentialDocumentCache(itr, container->end());
+ if (cacheFound) {
+ // optimizeAt() is responsible for reordering stages, and optimize() is responsible for
+ // simplifying individual stages. $sequentialCache's optimizeAt() places the stage where
+ // it can cache as big a prefix of the pipeline as possible. To do so correctly, it
+ // needs to look at dependencies: a stage that depends on a let-variable cannot be
+ // cached. But optimize() can inline variables. Therefore, we want to avoid calling
+ // optimize() before $sequentialCache has a chance to run optimizeAt().
+ return Pipeline::optimizeAtEndOfPipeline(itr, container);
+ } else {
+ Pipeline::optimizeEndOfPipeline(itr, container);
+ }
- Pipeline::optimizeEndOfPipeline(itr, container);
if (std::next(itr) == container->end()) {
return container->end();
} else {
@@ -1083,7 +1321,8 @@ 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) {
@@ -1094,13 +1333,12 @@ 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 = Pipeline::getDependenciesForContainer(
- pExpCtx, Pipeline::SourceContainer{std::next(itr), container->end()}, boost::none);
+ auto deps = getRestPipelineDependencies(itr, container, true /* includeEventFilter */);
if (deps.hasNoRequirements()) {
- _bucketUnpacker.setBucketSpecAndBehavior({_bucketUnpacker.bucketSpec().timeField(),
- _bucketUnpacker.bucketSpec().metaField(),
- {}},
- BucketUnpacker::Behavior::kInclude);
+ _bucketUnpacker.setBucketSpec({_bucketUnpacker.bucketSpec().timeField(),
+ _bucketUnpacker.bucketSpec().metaField(),
+ {},
+ BucketSpec::Behavior::kInclude});
// Keep going for next optimization.
}
@@ -1117,31 +1355,72 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi
// Attempt to optimize last-point type queries.
if (feature_flags::gfeatureFlagLastPointQuery.isEnabled(
serverGlobalParams.featureCompatibility) &&
- !_triedLastpointRewrite && optimizeLastpoint(itr, container)) {
+ !_triedLastpointRewrite && !_eventFilter && 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 predicates on the control field.
- if (auto nextMatch = dynamic_cast<DocumentSourceMatch*>(std::next(itr)->get());
- nextMatch && !_triedBucketLevelFieldsPredicatesPushdown) {
- _triedBucketLevelFieldsPredicatesPushdown = true;
+ // Attempt to map predicates on bucketed fields to the predicates on the control field.
+ if (auto nextMatch = dynamic_cast<DocumentSourceMatch*>(std::next(itr)->get())) {
+
+ // 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));
+ }
- if (auto match = createPredicatesOnBucketLevelField(nextMatch->getMatchExpression())) {
+ // 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) {
BSONObjBuilder bob;
- match->serialize(&bob);
+ predicates.loosePredicate->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 (!haveComputedMetaField) {
+ if (!_eventFilter && !haveComputedMetaField) {
if (auto [metaProject, deleteRemainder] = extractProjectForPushDown(std::next(itr)->get());
!metaProject.isEmpty()) {
container->insert(itr,
@@ -1160,7 +1439,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 (pushDownComputedMetaProjection(itr, container)) {
+ if (!_eventFilter && 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));
}
@@ -1179,6 +1458,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi
}
}
+ enableStreamingGroupIfPossible(itr, container);
+
return container->end();
}
@@ -1187,8 +1468,8 @@ DocumentSource::GetModPathsReturn DocumentSourceInternalUnpackBucket::getModifie
StringMap<std::string> renames;
renames.emplace(*_bucketUnpacker.bucketSpec().metaField(),
timeseries::kBucketMetaFieldName);
- return {GetModPathsReturn::Type::kAllExcept, std::set<std::string>{}, std::move(renames)};
+ return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{}, std::move(renames)};
}
- return {GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}};
}
} // 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 df3bf93bff4..4d20a22f107 100644
--- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h
+++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h
@@ -47,9 +47,12 @@ 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);
@@ -61,6 +64,13 @@ 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();
}
@@ -95,6 +105,8 @@ public:
UnionRequirement::kAllowed,
ChangeStreamRequirement::kDenylist};
constraints.canSwapWithMatch = true;
+ // The user cannot specify multiple $unpackBucket stages in the pipeline.
+ constraints.canAppearOnlyOnceInPipeline = true;
return constraints;
}
@@ -155,7 +167,7 @@ public:
/**
* Convenience wrapper around BucketSpec::createPredicatesOnBucketLevelField().
*/
- std::unique_ptr<MatchExpression> createPredicatesOnBucketLevelField(
+ BucketSpec::BucketPredicate createPredicatesOnBucketLevelField(
const MatchExpression* matchExpr) const;
/**
@@ -209,13 +221,20 @@ public:
/**
* Helper method which checks if we can avoid unpacking if we have a group stage with min/max
- * aggregates. If a rewrite is possible, 'container' is modified, and we returns result value
- * for 'doOptimizeAt'.
+ * 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.
*/
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.
@@ -240,23 +259,48 @@ 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;
// 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;
- // 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;
+ // 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;
+
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 ffe291dcbd0..63d3b4a0b23 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,10 +55,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -76,10 +77,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -97,10 +99,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -118,10 +121,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -139,11 +143,12 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -161,7 +166,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate);
+ ASSERT(predicate.loosePredicate);
auto expected = fromjson(
"{$or: ["
" {$or: ["
@@ -185,7 +190,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
" ]}}"
" ]}"
"]}");
- ASSERT_BSONOBJ_EQ(predicate->serialize(true), expected);
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), expected);
+ ASSERT_FALSE(predicate.tightPredicate);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -208,10 +214,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -234,10 +241,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -260,10 +268,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -286,10 +295,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -312,11 +322,12 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -334,13 +345,14 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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,
@@ -358,7 +370,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate == nullptr);
+ ASSERT(predicate.loosePredicate == nullptr);
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -376,7 +389,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true),
fromjson("{$or: ["
" {'control.max.b': {$_internalExprGt: 1}},"
" {$expr: {$ne: ["
@@ -384,6 +397,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
" {$type: [ \"$control.max.b\" ]}"
" ]}}"
"]}"));
+ ASSERT_FALSE(predicate.tightPredicate);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -402,7 +416,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true),
fromjson("{$and: [ {$or: [ {'control.max.b': {$_internalExprGte: 2}},"
"{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]},"
"{$type: [ \"$control.max.b\" ]} ]}} ]},"
@@ -412,6 +426,7 @@ 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,
@@ -429,8 +444,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate);
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT(predicate.loosePredicate);
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true),
fromjson("{$or: ["
" {$or: ["
" {'control.max.b': {$_internalExprGt: 1}},"
@@ -447,6 +462,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
" ]}}"
" ]}"
"]}"));
+ ASSERT_FALSE(predicate.tightPredicate);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -464,7 +480,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate == nullptr);
+ ASSERT(predicate.loosePredicate == nullptr);
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -485,7 +502,8 @@ 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 == nullptr);
+ ASSERT(predicate.loosePredicate == nullptr);
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -504,7 +522,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true),
fromjson("{$or: ["
" {$or: ["
" {'control.max.b': {$_internalExprGte: 2}},"
@@ -530,6 +548,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
" ]}"
" ]}"
"]}"));
+ ASSERT_FALSE(predicate.tightPredicate);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -541,19 +560,21 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
ASSERT_EQ(pipeline->getSources().size(), 2U);
pipeline->optimizePipeline();
- ASSERT_EQ(pipeline->getSources().size(), 3U);
+ ASSERT_EQ(pipeline->getSources().size(), 2U);
// To get the optimized $match from the pipeline, we have to serialize with explain.
auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(stages.size(), 3U);
+ ASSERT_EQ(stages.size(), 2U);
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(), unpackBucketObj);
- ASSERT_BSONOBJ_EQ(stages[2].getDocument().toBson(),
- fromjson("{$match: {$and: [{b: {$gt: 1}}, {a: {$not: {$eq: 5}}}]}}"));
+ ASSERT_BSONOBJ_EQ(
+ stages[1].getDocument().toBson(),
+ fromjson(
+ "{$_internalUnpackBucket: {exclude: [], timeField: 'time', bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { $and: [ { b: { $gt: 1 } }, { a: { $not: { $eq: 5 } } } ] }}}"));
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -566,10 +587,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
ASSERT_EQ(pipeline->getSources().size(), 2U);
pipeline->optimizePipeline();
- ASSERT_EQ(pipeline->getSources().size(), 3U);
+ ASSERT_EQ(pipeline->getSources().size(), 2U);
auto stages = pipeline->serializeToBson();
- ASSERT_EQ(stages.size(), 3U);
+ ASSERT_EQ(stages.size(), 2U);
ASSERT_BSONOBJ_EQ(
stages[0],
@@ -582,8 +603,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
"{$or: [ {'control.min.a': {$_internalExprLt: 5}},"
"{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]},"
"{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"));
- ASSERT_BSONOBJ_EQ(stages[1], unpackBucketObj);
- ASSERT_BSONOBJ_EQ(stages[2], matchObj);
+ ASSERT_BSONOBJ_EQ(stages[1],
+ fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", "
+ "bucketMaxSpanSeconds: 3600,"
+ "eventFilter: { $and: [ { b: { $gte: 2 } }, { c: { $gt: 1 } }, { a: "
+ "{ $lt: 5 } } ] } } }"));
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -601,7 +625,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate == nullptr);
+ ASSERT(predicate.loosePredicate == nullptr);
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -619,7 +644,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate == nullptr);
+ ASSERT(predicate.loosePredicate == nullptr);
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -637,7 +663,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT(predicate == nullptr);
+ ASSERT(predicate.loosePredicate == nullptr);
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -656,7 +683,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
->createPredicatesOnBucketLevelField(original->getMatchExpression());
// Meta predicates are mapped to the meta field, not the control min/max fields.
- ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{meta: {$gt: 5}}"));
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{meta: {$gt: 5}}"));
+ ASSERT_BSONOBJ_EQ(predicate.tightPredicate->serialize(true), fromjson("{meta: {$gt: 5}}"));
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -675,7 +703,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
->createPredicatesOnBucketLevelField(original->getMatchExpression());
// Meta predicates are mapped to the meta field, not the control min/max fields.
- ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{'meta.foo': {$gt: 5}}"));
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true),
+ fromjson("{'meta.foo': {$gt: 5}}"));
+ ASSERT_BSONOBJ_EQ(predicate.tightPredicate->serialize(true),
+ fromjson("{'meta.foo': {$gt: 5}}"));
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
@@ -693,7 +724,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true),
fromjson("{$and: ["
" {$or: ["
" {'control.max.a': {$_internalExprGt: 1}},"
@@ -704,6 +735,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
" ]},"
" {meta: {$eq: 5}}"
"]}"));
+ ASSERT(predicate.tightPredicate == nullptr);
}
TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePredicatesOnId) {
@@ -740,7 +772,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get());
+ auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get());
auto children = andExpr->getChildVector();
ASSERT_EQ(children->size(), 3);
@@ -797,7 +829,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre
auto predicate =
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get());
+ auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get());
auto children = andExpr->getChildVector();
ASSERT_EQ(children->size(), 3);
@@ -846,7 +878,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre
auto predicate =
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get());
+ auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get());
auto children = andExpr->getChildVector();
ASSERT_EQ(children->size(), 6);
@@ -908,7 +940,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get());
+ auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get());
auto children = andExpr->getChildVector();
ASSERT_EQ(children->size(), 3);
@@ -957,7 +989,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre
auto predicate =
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get());
+ auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get());
auto children = andExpr->getChildVector();
ASSERT_EQ(children->size(), 3);
@@ -1000,7 +1032,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_FALSE(predicate);
+ ASSERT_FALSE(predicate.loosePredicate);
+ ASSERT_FALSE(predicate.tightPredicate);
}
}
{
@@ -1021,7 +1054,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate =
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_FALSE(predicate);
+ ASSERT_FALSE(predicate.loosePredicate);
+ ASSERT_FALSE(predicate.tightPredicate);
}
}
{
@@ -1042,7 +1076,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate =
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_FALSE(predicate);
+ ASSERT_FALSE(predicate.loosePredicate);
+ ASSERT_FALSE(predicate.tightPredicate);
}
}
{
@@ -1065,7 +1100,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_FALSE(predicate);
+ ASSERT_FALSE(predicate.loosePredicate);
+ ASSERT_FALSE(predicate.tightPredicate);
}
}
{
@@ -1086,7 +1122,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate =
dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_FALSE(predicate);
+ ASSERT_FALSE(predicate.loosePredicate);
+ ASSERT_FALSE(predicate.tightPredicate);
}
}
}
@@ -1107,10 +1144,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest,
auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get())
->createPredicatesOnBucketLevelField(original->getMatchExpression());
- ASSERT_BSONOBJ_EQ(predicate->serialize(true),
+ ASSERT_BSONOBJ_EQ(predicate.loosePredicate->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 70b524c7510..78b2931cdbc 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,6 +94,49 @@ 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', "
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 cfe92c09f5f..0f9a60dbf2a 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
@@ -54,7 +54,7 @@ TEST_F(OptimizePipeline, MixedMatchPushedDown) {
// To get the optimized $match from the pipeline, we have to serialize with explain.
auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(3u, stages.size());
+ ASSERT_EQ(2u, 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.
@@ -63,8 +63,10 @@ TEST_F(OptimizePipeline, MixedMatchPushedDown) {
"{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]},"
"{$type: [ \"$control.max.a\" ] } ] } } ] }]}}"),
stages[0].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("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", "
+ "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { a: { $lte: 4 } } } }"),
+ stages[1].getDocument().toBson());
}
TEST_F(OptimizePipeline, MetaMatchPushedDown) {
@@ -103,7 +105,7 @@ TEST_F(OptimizePipeline, MixedMatchOr) {
pipeline->optimizePipeline();
auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(3u, stages.size());
+ ASSERT_EQ(2u, stages.size());
auto expected = fromjson(
"{$match: {$and: ["
// Result of pushing down {x: {$lte: 1}}.
@@ -123,8 +125,11 @@ TEST_F(OptimizePipeline, MixedMatchOr) {
" ]}"
"]}}");
ASSERT_BSONOBJ_EQ(expected, stages[0].getDocument().toBson());
- ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson());
- ASSERT_BSONOBJ_EQ(match, stages[2].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());
}
TEST_F(OptimizePipeline, MixedMatchOnlyMetaMatchPushedDown) {
@@ -142,11 +147,13 @@ 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(3u, serialized.size());
+ ASSERT_EQ(2u, serialized.size());
ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$gte: 0}}, {meta: {$lte: 5}}]}}"),
serialized[0]);
- ASSERT_BSONOBJ_EQ(unpack, serialized[1]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$type: [ 2 ]}}}"), serialized[2]);
+ ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", "
+ "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { a: { $type: [ 2 ] } } } }"),
+ serialized[1]);
}
TEST_F(OptimizePipeline, MultipleMatchesPushedDown) {
@@ -164,15 +171,17 @@ 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(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(3u, stages.size());
+ ASSERT_EQ(2u, 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(unpack, stages[1].getDocument().toBson());
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson());
+ ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", "
+ "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { a: { $lte: 4 } } } }"),
+ stages[1].getDocument().toBson());
}
TEST_F(OptimizePipeline, MultipleMatchesPushedDownWithSort) {
@@ -191,16 +200,18 @@ 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(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(4u, stages.size());
+ 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(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());
+ 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());
}
TEST_F(OptimizePipeline, MetaMatchThenCountPushedDown) {
@@ -261,7 +272,7 @@ TEST_F(OptimizePipeline, SortThenMixedMatchPushedDown) {
// We should push down both the $sort and parts of the $match.
auto serialized = pipeline->serializeToBson();
- ASSERT_EQ(4u, serialized.size());
+ ASSERT_EQ(3u, serialized.size());
auto expected = fromjson(
"{$match: {$and: ["
" {meta: {$eq: 'abc'}},"
@@ -274,8 +285,10 @@ TEST_F(OptimizePipeline, SortThenMixedMatchPushedDown) {
"]}}");
ASSERT_BSONOBJ_EQ(expected, serialized[0]);
ASSERT_BSONOBJ_EQ(fromjson("{$sort: {meta: -1}}"), serialized[1]);
- ASSERT_BSONOBJ_EQ(unpack, serialized[2]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$gte: 5}}}"), serialized[3]);
+ ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", "
+ "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { a: { $gte: 5 } } } }"),
+ serialized[2]);
}
TEST_F(OptimizePipeline, MetaMatchThenSortPushedDown) {
@@ -331,18 +344,19 @@ TEST_F(OptimizePipeline, MixedMatchThenProjectPushedDown) {
// We can push down part of the $match and use dependency analysis on the end of the pipeline.
auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(4u, stages.size());
+ ASSERT_EQ(3u, 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}}"),
- stages[1].getDocument().toBson());
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].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("{$project: {_id: true, x: true}}"),
- stages[3].getDocument().toBson());
+ stages[2].getDocument().toBson());
}
@@ -379,21 +393,21 @@ TEST_F(OptimizePipeline, ProjectThenMixedMatchPushedDown) {
// We should push down part of the $match and do dependency analysis on the rest.
auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(4u, stages.size());
+ ASSERT_EQ(3u, 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}}"),
+ fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"x\", \"myMeta\" ], "
+ "timeField: \"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { a: { $lte: 4 } } } }"),
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[3].getDocument().toBson()),
+ stages[2].getDocument().toBson()),
0);
}
@@ -410,7 +424,7 @@ TEST_F(OptimizePipeline, ProjectWithRenameThenMixedMatchPushedDown) {
// We should push down part of the $match and do dependency analysis on the end of the pipeline.
auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner);
- ASSERT_EQ(4u, stages.size());
+ ASSERT_EQ(3u, stages.size());
ASSERT_BSONOBJ_EQ(
fromjson("{$match: {$and: [{$or: [ {'control.max.y': {$_internalExprGte: \"abc\"}},"
"{$expr: {$ne: [ {$type: [ \"$control.min.y\" ]},"
@@ -419,13 +433,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}}"),
- stages[1].getDocument().toBson());
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{y: {$gte: 'abc'}}, {a: {$lte: 4}}]}}"),
- stages[2].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'}}"),
- stages[3].getDocument().toBson());
+ stages[2].getDocument().toBson());
}
TEST_F(OptimizePipeline, ComputedProjectThenMetaMatchPushedDown) {
@@ -466,15 +480,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(3u, serialized.size());
+ ASSERT_EQ(2u, 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']}}"),
+ fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"myMeta\" ], timeField: "
+ "\"time\", metaField: \"myMeta\", "
+ "bucketMaxSpanSeconds: 3600, computedMetaProjFields: [ \"myMeta\" ], "
+ "eventFilter: { myMeta: { $gte: \"abc\" } } } }"),
serialized[1]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {myMeta: {$gte: 'abc'}}}"), serialized[2]);
} // namespace
TEST_F(OptimizePipeline, ComputedProjectThenMatchNotPushedDown) {
@@ -491,13 +505,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(3u, serialized.size());
+ ASSERT_EQ(2u, 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']}}"),
- serialized[1]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {y: {$gt: 'abc'}}}"), serialized[2]);
+ ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"y\" ], "
+ "timeField: \"time\", metaField: \"myMeta\", "
+ "bucketMaxSpanSeconds: 3600, computedMetaProjFields: [ \"y\" ], "
+ "eventFilter: { y: { $gt: \"abc\" } } } }"),
+ serialized[1]);
}
TEST_F(OptimizePipeline, MetaSortThenProjectPushedDown) {
@@ -750,6 +764,55 @@ 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', "
@@ -808,7 +871,7 @@ TEST_F(OptimizePipeline, MatchWithGeoWithinOnMeasurementsPushedDownUsingInternal
pipeline->optimizePipeline();
auto serialized = pipeline->serializeToBson();
- ASSERT_EQ(serialized.size(), 3U);
+ ASSERT_EQ(serialized.size(), 2U);
// $match with $geoWithin on a non-metadata field is pushed down and $_internalBucketGeoWithin
// is used.
@@ -817,12 +880,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}}"),
- serialized[1]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {loc: {$geoWithin: {$geometry: {type: \"Polygon\", "
- "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]}}}}}"),
- serialized[2]);
+ 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]);
}
TEST_F(OptimizePipeline, MatchWithGeoWithinOnMetaFieldIsPushedDown) {
@@ -864,7 +927,7 @@ TEST_F(OptimizePipeline,
pipeline->optimizePipeline();
auto serialized = pipeline->serializeToBson();
- ASSERT_EQ(serialized.size(), 3U);
+ ASSERT_EQ(serialized.size(), 2U);
// $match with $geoIntersects on a non-metadata field is pushed down and
// $_internalBucketGeoWithin is used.
@@ -873,12 +936,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}}"),
- serialized[1]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {loc: {$geoIntersects: {$geometry: {type: \"Polygon\", "
- "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]}}}}}"),
- serialized[2]);
+ 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]);
}
TEST_F(OptimizePipeline, MatchWithGeoIntersectsOnMetaFieldIsPushedDown) {
@@ -907,5 +970,56 @@ 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());
+}
} // 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 ba4f31adf17..4ce5d558ac4 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(3u, serialized.size());
+ ASSERT_EQ(2u, serialized.size());
ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: ["
" {meta: {$gte: 0}},"
" {meta: {$lte: 5}},"
@@ -68,8 +68,13 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeSplitsMatchAndMaps
" ]}"
"]}}"),
serialized[0]);
- ASSERT_BSONOBJ_EQ(unpack, serialized[1]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), serialized[2]);
+ ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { "
+ "exclude: [], "
+ "timeField: \"foo\", "
+ "metaField: \"myMeta\", "
+ "bucketMaxSpanSeconds: 3600, "
+ "eventFilter: { a: { $lte: 4 } } } }"),
+ serialized[1]);
}
TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeMovesMetaMatchBeforeUnpack) {
@@ -94,10 +99,6 @@ 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}}}")),
@@ -108,9 +109,11 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename,
// The $match on meta is not moved before $_internalUnpackBucket since the field is excluded.
auto serialized = pipeline->serializeToBson();
- ASSERT_EQ(2u, serialized.size());
- ASSERT_BSONOBJ_EQ(unpackExcluded, serialized[0]);
- ASSERT_BSONOBJ_EQ(fromjson("{$match: {myMeta: {$gte: 0}}}"), serialized[1]);
+ 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]);
}
TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename,
@@ -134,7 +137,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(3u, serialized.size());
+ ASSERT_EQ(2u, serialized.size());
auto expected = fromjson(
"{$match: {$and: ["
// Result of pushing down {x: {$lte: 1}}.
@@ -154,8 +157,13 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename,
" ]}"
"]}}");
ASSERT_BSONOBJ_EQ(expected, serialized[0]);
- ASSERT_BSONOBJ_EQ(unpack, serialized[1]);
- ASSERT_BSONOBJ_EQ(match, serialized[2]);
+ 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]);
}
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/pipeline/document_source_lookup.cpp b/src/mongo/db/pipeline/document_source_lookup.cpp
index cfb885e94c5..0b4585de2bc 100644
--- a/src/mongo/db/pipeline/document_source_lookup.cpp
+++ b/src/mongo/db/pipeline/document_source_lookup.cpp
@@ -35,6 +35,7 @@
#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"
@@ -108,7 +109,8 @@ 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::kTenantMigrationOplogView ||
+ nss == NamespaceString::kConfigsvrCollectionsNamespace);
return nss;
}
@@ -399,6 +401,11 @@ 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
@@ -661,7 +668,7 @@ void DocumentSourceLookUp::addCacheStageAndOptimize(Pipeline& pipeline) {
}
DocumentSource::GetModPathsReturn DocumentSourceLookUp::getModifiedPaths() const {
- std::set<std::string> modifiedPaths{_as.fullPath()};
+ OrderedPathSet modifiedPaths{_as.fullPath()};
if (_unwindSrc) {
auto pathsModifiedByUnwind = _unwindSrc->getModifiedPaths();
invariant(pathsModifiedByUnwind.type == GetModPathsReturn::Type::kFiniteSet);
diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp
index 10346eb6a46..418f1821b4b 100644
--- a/src/mongo/db/pipeline/document_source_match.cpp
+++ b/src/mongo/db/pipeline/document_source_match.cpp
@@ -93,11 +93,12 @@ 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.
+ // 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.
BSONObj toMatch = _dependencies.needWholeDocument
- ? nextInput.getDocument().toBson()
- : document_path_support::documentToBsonWithPaths(nextInput.getDocument(),
- _dependencies.fields);
+ ? nextInput.getDocument().toBson<BSONObj::LargeSizeTrait>()
+ : document_path_support::documentToBsonWithPaths<BSONObj::LargeSizeTrait>(
+ nextInput.getDocument(), _dependencies.fields);
if (_expression->matchesBSON(toMatch)) {
return nextInput;
@@ -410,13 +411,13 @@ void DocumentSourceMatch::joinMatchWith(intrusive_ptr<DocumentSourceMatch> other
}
pair<intrusive_ptr<DocumentSourceMatch>, intrusive_ptr<DocumentSourceMatch>>
-DocumentSourceMatch::splitSourceBy(const std::set<std::string>& fields,
+DocumentSourceMatch::splitSourceBy(const OrderedPathSet& 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 std::set<std::string>& fields,
+DocumentSourceMatch::splitSourceByFunc(const OrderedPathSet& fields,
const StringMap<std::string>& renames,
expression::ShouldSplitExprFunc func) && {
pair<unique_ptr<MatchExpression>, unique_ptr<MatchExpression>> newExpr(
@@ -499,7 +500,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.
- std::set<std::string> modifiedPaths;
+ OrderedPathSet 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 5bf27ddfe5c..f01c655771d 100644
--- a/src/mongo/db/pipeline/document_source_match.h
+++ b/src/mongo/db/pipeline/document_source_match.h
@@ -140,7 +140,7 @@ public:
GetModPathsReturn getModifiedPaths() const final {
// This stage does not modify or rename any paths.
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
/**
@@ -199,7 +199,7 @@ public:
* z: "baz"}} and {$match: {a: "foo"}}.
*/
std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>>
- splitSourceBy(const std::set<std::string>& fields, const StringMap<std::string>& renames) &&;
+ splitSourceBy(const OrderedPathSet& fields, const StringMap<std::string>& renames) &&;
boost::optional<DistributedPlanLogic> distributedPlanLogic() final {
return boost::none;
@@ -220,7 +220,7 @@ protected:
private:
std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>>
- splitSourceByFunc(const std::set<std::string>& fields,
+ splitSourceByFunc(const OrderedPathSet& fields,
const StringMap<std::string>& renames,
expression::ShouldSplitExprFunc func) &&;
diff --git a/src/mongo/db/pipeline/document_source_merge.cpp b/src/mongo/db/pipeline/document_source_merge.cpp
index 96a1dd55547..e2746296f01 100644
--- a/src/mongo/db/pipeline/document_source_merge.cpp
+++ b/src/mongo/db/pipeline/document_source_merge.cpp
@@ -57,10 +57,11 @@ 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 = std::function<void(DocumentSourceMerge::BatchedObjects&)>;
+using BatchTransform = DocumentSourceMerge::BatchTransform;
using UpdateModification = write_ops::UpdateModification;
using UpsertType = MongoProcessInterface::UpsertType;
@@ -83,21 +84,71 @@ 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));
+ };
+}
+
/**
- * 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'.
+ * Converts 'batch' into a vector of UpdateOpEntries.
*/
-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);
- }
+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;
+}
+/**
+ * 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(batch), wc, upsert, multi, epoch));
+ expCtx, ns, std::move(updateCommand), wc, upsert, multi, epoch));
};
}
@@ -106,20 +157,22 @@ MergeStrategy makeUpdateStrategy(UpsertType upsert, BatchTransform transform) {
* 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. If 'BatchTransform' function is provided, it will be called to transform batched objects
- * before passing them to the 'update'.
+ * error.
*/
-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);
- }
-
+MergeStrategy makeStrictUpdateStrategy() {
+ return [](const auto& expCtx,
+ const auto& ns,
+ const auto& wc,
+ auto epoch,
+ auto&& batch,
+ auto&& bcr,
+ UpsertType upsert) {
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(batch), wc, upsert, multi, epoch));
+ expCtx, ns, std::move(updateCommand), 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),
@@ -131,28 +184,34 @@ MergeStrategy makeStrictUpdateStrategy(UpsertType upsert, BatchTransform transfo
* 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) {
+ return [](const auto& expCtx,
+ const auto& ns,
+ const auto& wc,
+ auto epoch,
+ auto&& batch,
+ auto&& bcr,
+ UpsertType upsertType) {
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();
});
- uassertStatusOK(expCtx->mongoProcessInterface->insert(
- expCtx, ns, std::move(objectsToInsert), wc, epoch));
+ auto insertCommand = bcr.extractInsertRequest();
+ insertCommand->setDocuments(std::move(objectsToInsert));
+ uassertStatusOK(
+ expCtx->mongoProcessInterface->insert(expCtx, ns, std::move(insertCommand), wc, epoch));
};
}
/**
- * Creates a batched objects transformation function which wraps each element of the
- * 'batch.modifications' array into the given 'updateOp' operator.
+ * Creates a batched object transformation function which wraps 'obj' into the given 'updateOp'
+ * operator.
*/
BatchTransform makeUpdateTransform(const std::string& updateOp) {
- return [updateOp](auto& batch) {
- for (auto&& obj : batch) {
- std::get<UpdateModification>(obj) = UpdateModification::parseFromClassicUpdate(
- BSON(updateOp << std::get<UpdateModification>(obj).getUpdateReplacement()));
- }
+ return [updateOp](auto& obj) {
+ std::get<UpdateModification>(obj) = UpdateModification::parseFromClassicUpdate(
+ BSON(updateOp << std::get<UpdateModification>(obj).getUpdateReplacement()));
};
}
@@ -171,53 +230,95 @@ 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, {})}},
- // 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()}}};
+ 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()}}};
return mergeStrategyDescriptors;
}
@@ -543,17 +644,29 @@ std::pair<DocumentSourceMerge::BatchObject, int> DocumentSourceMerge::makeBatchO
auto mergeOnFields = extractMergeOnFieldsFromDoc(doc, _mergeOnFields);
auto mod = makeBatchUpdateModification(doc);
auto vars = resolveLetVariablesIfNeeded(doc);
- auto modSize = mod.objsize() + (vars ? vars->objsize() : 0);
- return {{std::move(mergeOnFields), std::move(mod), std::move(vars)}, modSize};
+ 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)};
}
-void DocumentSourceMerge::spill(BatchedObjects&& batch) try {
+void DocumentSourceMerge::spill(BatchedCommandRequest&& bcr, 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));
+ _descriptor.strategy(pExpCtx,
+ _outputNs,
+ _writeConcern,
+ targetEpoch,
+ std::move(batch),
+ std::move(bcr),
+ _descriptor.upsertType);
} catch (const ExceptionFor<ErrorCodes::ImmutableField>& ex) {
uassertStatusOKWithContext(ex.toStatus(),
"$merge failed to update the matching document, did you "
@@ -577,6 +690,10 @@ void DocumentSourceMerge::spill(BatchedObjects&& batch) try {
}
}
+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 05a87f1e340..9388add24ba 100644
--- a/src/mongo/db/pipeline/document_source_merge.h
+++ b/src/mongo/db/pipeline/document_source_merge.h
@@ -44,24 +44,38 @@ class DocumentSourceMerge final : public DocumentSourceWriter<MongoProcessInterf
public:
static constexpr StringData kStageName = "$merge"_sd;
- // 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.
+ 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.
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&&)>;
+ 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&)>;
MergeMode mode;
ActionSet actions;
MergeStrategy strategy;
+ BatchTransform transform;
+ UpsertType upsertType;
+ BatchedCommandGenerator batchedCommandGenerator;
};
/**
@@ -177,8 +191,11 @@ 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::parseFromClassicUpdate(doc.toBson());
+ return _pipeline
+ ? write_ops::UpdateModification(*_pipeline)
+ : write_ops::UpdateModification(doc.toBson(),
+ write_ops::UpdateModification::ClassicTag{},
+ true /* isReplacement */);
}
/**
@@ -201,7 +218,9 @@ private:
return bob.obj();
}
- void spill(BatchedObjects&& batch) override;
+ void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) override;
+
+ BatchedCommandRequest initializeBatchedWriteRequest() const override;
void waitWhileFailPointEnabled() override;
diff --git a/src/mongo/db/pipeline/document_source_mock.h b/src/mongo/db/pipeline/document_source_mock.h
index a6be10cbb84..39b3e17a265 100644
--- a/src/mongo/db/pipeline/document_source_mock.h
+++ b/src/mongo/db/pipeline/document_source_mock.h
@@ -106,7 +106,7 @@ public:
* This stage does not modify anything.
*/
GetModPathsReturn getModifiedPaths() const override {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
boost::optional<DistributedPlanLogic> distributedPlanLogic() override {
diff --git a/src/mongo/db/pipeline/document_source_out.cpp b/src/mongo/db/pipeline/document_source_out.cpp
index 9be7c7a24ba..9b826fb3c89 100644
--- a/src/mongo/db/pipeline/document_source_out.cpp
+++ b/src/mongo/db/pipeline/document_source_out.cpp
@@ -177,6 +177,11 @@ 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) {
diff --git a/src/mongo/db/pipeline/document_source_out.h b/src/mongo/db/pipeline/document_source_out.h
index 64dda167eb3..0be5153cdf0 100644
--- a/src/mongo/db/pipeline/document_source_out.h
+++ b/src/mongo/db/pipeline/document_source_out.h
@@ -121,19 +121,25 @@ private:
void finalize() override;
- void spill(BatchedObjects&& batch) override {
+ void spill(BatchedCommandRequest&& bcr, 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(batch), _writeConcern, targetEpoch));
+ pExpCtx, _tempNs, std::move(insertCommand), _writeConcern, targetEpoch));
}
std::pair<BSONObj, int> makeBatchObject(Document&& doc) const override {
auto obj = doc.toBson();
- return {obj, obj.objsize()};
+ tassert(6628900, "_writeSizeEstimator should be initialized", _writeSizeEstimator);
+ return {obj, _writeSizeEstimator->estimateInsertSizeBytes(obj)};
}
+ 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_queue.h b/src/mongo/db/pipeline/document_source_queue.h
index 31dc128f6cb..e7eb6452d06 100644
--- a/src/mongo/db/pipeline/document_source_queue.h
+++ b/src/mongo/db/pipeline/document_source_queue.h
@@ -75,7 +75,7 @@ public:
* This stage does not modify anything.
*/
GetModPathsReturn getModifiedPaths() const override {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
/**
diff --git a/src/mongo/db/pipeline/document_source_redact.h b/src/mongo/db/pipeline/document_source_redact.h
index 10fcfc14f63..b566f2c7adf 100644
--- a/src/mongo/db/pipeline/document_source_redact.h
+++ b/src/mongo/db/pipeline/document_source_redact.h
@@ -72,6 +72,16 @@ public:
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 95d001e4dac..6605796138b 100644
--- a/src/mongo/db/pipeline/document_source_redact_test.cpp
+++ b/src/mongo/db/pipeline/document_source_redact_test.cpp
@@ -81,5 +81,26 @@ 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 e302ce5d88c..f288aa10321 100644
--- a/src/mongo/db/pipeline/document_source_replace_root.h
+++ b/src/mongo/db/pipeline/document_source_replace_root.h
@@ -74,7 +74,7 @@ public:
DocumentSource::GetModPathsReturn getModifiedPaths() const final {
// Replaces the entire root, so all paths are modified.
- return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}};
+ return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}};
}
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 5ec204d1fa9..24e86b38cba 100644
--- a/src/mongo/db/pipeline/document_source_sample.cpp
+++ b/src/mongo/db/pipeline/document_source_sample.cpp
@@ -52,8 +52,10 @@ REGISTER_DOCUMENT_SOURCE(sample,
AllowedWithApiStrict::kAlways);
DocumentSource::GetNextResult DocumentSourceSample::doGetNext() {
- if (_size == 0)
+ if (_size == 0) {
+ pSource->dispose();
return GetNextResult::makeEOF();
+ }
if (!_sortStage->isPopulated()) {
// Exhaust source stage, add random metadata, and push all into sorter.
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 33c32f096b3..39a13272df6 100644
--- a/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp
+++ b/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp
@@ -41,7 +41,6 @@ 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();
@@ -96,7 +95,7 @@ Pipeline::SourceContainer::iterator DocumentSourceSequentialDocumentCache::doOpt
_hasOptimizedPos = true;
// If the cache is the only stage in the pipeline, return immediately.
- if (itr == container->begin()) {
+ if (itr == container->begin() && std::next(itr) == container->end()) {
return container->end();
}
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 0fccb527fad..1fea0368017 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,6 +76,7 @@ public:
}
// This stage doesn't modify documents.
setVariableConstraints.preservesOrderAndMetadata = true;
+ setVariableConstraints.canSwapWithSkippingOrLimitingStage = true;
return setVariableConstraints;
}
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 45223bb1cf4..73806992561 100644
--- a/src/mongo/db/pipeline/document_source_set_window_fields.cpp
+++ b/src/mongo/db/pipeline/document_source_set_window_fields.cpp
@@ -464,8 +464,12 @@ 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();
}
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 652d1206ec9..f0f9b0742ce 100644
--- a/src/mongo/db/pipeline/document_source_set_window_fields.h
+++ b/src/mongo/db/pipeline/document_source_set_window_fields.h
@@ -117,7 +117,7 @@ public:
_iterator(expCtx.get(), pSource, &_memoryTracker, std::move(partitionBy), _sortBy){};
GetModPathsReturn getModifiedPaths() const final {
- std::set<std::string> outputPaths;
+ OrderedPathSet outputPaths;
for (auto&& outputField : _outputFields) {
outputPaths.insert(outputField.fieldName);
}
diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp
new file mode 100644
index 00000000000..ff9326ceae4
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp
@@ -0,0 +1,129 @@
+/**
+ * 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
new file mode 100644
index 00000000000..23705cf6aeb
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_sharded_data_distribution.h
@@ -0,0 +1,73 @@
+/**
+ * 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_sort.cpp b/src/mongo/db/pipeline/document_source_sort.cpp
index 2c07c43c501..84b28d6fde9 100644
--- a/src/mongo/db/pipeline/document_source_sort.cpp
+++ b/src/mongo/db/pipeline/document_source_sort.cpp
@@ -142,13 +142,19 @@ REGISTER_DOCUMENT_SOURCE(sort,
LiteParsedDocumentSourceDefault::parse,
DocumentSourceSort::createFromBson,
AllowedWithApiStrict::kAlways);
+
REGISTER_DOCUMENT_SOURCE_CONDITIONALLY(
_internalBoundedSort,
LiteParsedDocumentSourceDefault::parse,
DocumentSourceSort::parseBoundedSort,
- AllowedWithApiStrict::kNeverInVersion1,
- AllowedWithClientType::kAny,
- feature_flags::gFeatureFlagBucketUnpackWithSort.getVersion(),
+ ::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*/,
feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV());
DocumentSource::GetNextResult::ReturnStatus DocumentSourceSort::timeSorterPeek() {
@@ -301,8 +307,8 @@ void DocumentSourceSort::serializeToArray(
if (explain >= ExplainOptions::Verbosity::kExecStats) {
mutDoc["totalDataSizeSortedBytesEstimate"] =
Value(static_cast<long long>(_timeSorter->totalDataSizeBytes()));
- mutDoc["usedDisk"] = Value(_timeSorter->numSpills() > 0);
- mutDoc["spills"] = Value(static_cast<long long>(_timeSorter->numSpills()));
+ mutDoc["usedDisk"] = Value(_timeSorter->stats().spilledRanges() > 0);
+ mutDoc["spills"] = Value(static_cast<long long>(_timeSorter->stats().spilledRanges()));
}
array.push_back(Value{mutDoc.freeze()});
@@ -482,6 +488,10 @@ 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};
{
@@ -649,7 +659,7 @@ boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceSort::distri
}
bool DocumentSourceSort::canRunInParallelBeforeWriteStage(
- const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const {
+ const OrderedPathSet& 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 f0ab0c28a3c..ba087ee4a3f 100644
--- a/src/mongo/db/pipeline/document_source_sort.h
+++ b/src/mongo/db/pipeline/document_source_sort.h
@@ -79,7 +79,7 @@ public:
GetModPathsReturn getModifiedPaths() const final {
// A $sort does not modify any paths.
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
StageConstraints constraints(Pipeline::SplitState) const final {
@@ -102,7 +102,7 @@ public:
boost::optional<DistributedPlanLogic> distributedPlanLogic() final;
bool canRunInParallelBeforeWriteStage(
- const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const final;
+ const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const final;
/**
* Returns the sort key pattern.
diff --git a/src/mongo/db/pipeline/document_source_streaming_group.cpp b/src/mongo/db/pipeline/document_source_streaming_group.cpp
new file mode 100644
index 00000000000..44702ab550d
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_streaming_group.cpp
@@ -0,0 +1,279 @@
+/**
+ * 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, boost::optional<ExplainOptions::Verbosity> explain) const {
+ std::vector<Value> monotonicIdFields;
+ if (_idFieldNames.empty()) {
+ monotonicIdFields.emplace_back("_id"_sd);
+ } else {
+ for (size_t i : _monotonicExpressionIndexes) {
+ monotonicIdFields.emplace_back(_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
new file mode 100644
index 00000000000..bd72a3a7e7d
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_streaming_group.h
@@ -0,0 +1,126 @@
+/**
+ * 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,
+ boost::optional<ExplainOptions::Verbosity> explain) 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_unwind.cpp b/src/mongo/db/pipeline/document_source_unwind.cpp
index daf7adcd5ed..2161b0b9bbb 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 {
- std::set<std::string> modifiedFields{_unwindPath.fullPath()};
+ OrderedPathSet modifiedFields{_unwindPath.fullPath()};
if (_indexPath) {
modifiedFields.insert(_indexPath->fullPath());
}
diff --git a/src/mongo/db/pipeline/document_source_writer.h b/src/mongo/db/pipeline/document_source_writer.h
index a94b4efca6b..25fd08aac5c 100644
--- a/src/mongo/db/pipeline/document_source_writer.h
+++ b/src/mongo/db/pipeline/document_source_writer.h
@@ -36,8 +36,11 @@
#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;
@@ -83,11 +86,14 @@ 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 two methods to be able to write into the output collection:
+ * type 'B'. A subclass must override the following methods to be able to write into the output
+ * collection:
*
- * 1. 'makeBatchObject()' - to create an object of type 'B' from the given 'Document', which is,
+ * - 'makeBatchObject()' - creates an object of type 'B' from the given 'Document', which is,
* essentially, a result of the input source's 'getNext()' .
- * 2. 'spill()' - to write the batch into the output collection.
+ * - 'spill()' - writes the batch into the output collection.
+ * - 'initializeBatchedWriteRequest()' - initializes the request object for writing a batch to
+ * 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
@@ -99,12 +105,26 @@ 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()) {}
+ _writeConcern(expCtx->opCtx->getWriteConcern()),
+ _writeSizeEstimator(
+ expCtx->mongoProcessInterface->getWriteSizeEstimator(expCtx->opCtx, outputNs)) {}
DepsTracker::State getDependencies(DepsTracker* deps) const override {
deps->needWholeDocument = true;
@@ -114,7 +134,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, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}};
}
boost::optional<DistributedPlanLogic> distributedPlanLogic() override {
@@ -122,7 +142,7 @@ public:
}
bool canRunInParallelBeforeWriteStage(
- const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const override {
+ const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const override {
return true;
}
@@ -143,9 +163,31 @@ protected:
virtual void finalize() {}
/**
- * Writes the documents in 'batch' to the output namespace.
+ * 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.
*/
- virtual void spill(BatchedObjects&& batch) = 0;
+ virtual BatchedCommandRequest initializeBatchedWriteRequest() const = 0;
/**
* Creates a batch object from the given document and returns it to the caller along with the
@@ -169,6 +211,9 @@ 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};
@@ -198,9 +243,30 @@ DocumentSource::GetNextResult DocumentSourceWriter<B>::doGetNext() {
_initialized = true;
}
- BatchedObjects batch;
- int bufferedBytes = 0;
+ // 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;
auto nextInput = pSource->getNext();
for (; nextInput.isAdvanced(); nextInput = pSource->getNext()) {
waitWhileFailPointEnabled();
@@ -210,16 +276,17 @@ DocumentSource::GetNextResult DocumentSourceWriter<B>::doGetNext() {
bufferedBytes += objSize;
if (!batch.empty() &&
- (bufferedBytes > BSONObjMaxUserSize ||
+ (bufferedBytes > maxBatchSizeBytes ||
batch.size() >= write_ops::kMaxWriteBatchSize)) {
- spill(std::move(batch));
+ spill(std::move(batchWrite), std::move(batch));
batch.clear();
+ batchWrite = initializeBatchedWriteRequest();
bufferedBytes = objSize;
}
batch.push_back(obj);
}
if (!batch.empty()) {
- spill(std::move(batch));
+ spill(std::move(batchWrite), std::move(batch));
batch.clear();
}
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp
index bf0915deddd..ecb1f2382de 100644
--- a/src/mongo/db/pipeline/expression.cpp
+++ b/src/mongo/db/pipeline/expression.cpp
@@ -188,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.addAggExpressionCounter(key);
+ operatorCountersAggExpressions.addCounter(key);
}
intrusive_ptr<Expression> Expression::parseExpression(ExpressionContext* const expCtx,
@@ -308,111 +308,204 @@ const char* ExpressionAbs::getOpName() const {
/* ------------------------- ExpressionAdd ----------------------------- */
-StatusWith<Value> ExpressionAdd::apply(Value lhs, Value rhs) {
- BSONType diffType = Value::getWidestNumeric(rhs.getType(), lhs.getType());
-
- 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;
+namespace {
- // If there is an overflow, convert the values to doubles.
- if (overflow::add(lhs.coerceToLong(), rhs.coerceToLong(), &result)) {
- return Value(lhs.coerceToDouble() + rhs.coerceToDouble());
+/**
+ * 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;
}
- 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()));
- }
-}
-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;
+ if (isDate) {
+ addToDateValue(valToAdd);
+ return;
+ }
- const size_t n = _children.size();
- for (size_t i = 0; i < n; ++i) {
- Value val = _children[i]->evaluate(root, variables);
+ // 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;
+ }
+ }
- switch (val.getType()) {
- case NumberDecimal:
- decimalTotal = decimalTotal.add(val.getDecimal());
- totalType = NumberDecimal;
+ // 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;
+ }
break;
case NumberDouble:
- nonDecimalTotal.addDouble(val.getDouble());
- if (totalType != NumberDecimal)
- totalType = NumberDouble;
+ doubleTotal += valToAdd.coerceToDouble();
break;
- case NumberLong:
- nonDecimalTotal.addLong(val.getLong());
- if (totalType == NumberInt)
- totalType = NumberLong;
+ case NumberDecimal:
+ decimalTotal = decimalTotal.add(valToAdd.coerceToDecimal());
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:
- nonDecimalTotal.addDouble(val.getInt());
+ case NumberLong:
+ if (overflow::add(longTotal, valToAdd.coerceToLong(), &longTotal)) {
+ uasserted(ErrorCodes::Overflow, "date overflow");
+ }
break;
- case Date:
- uassert(16612, "only one date allowed in an $add expression", !haveDate);
- haveDate = true;
- nonDecimalTotal.addLong(val.getDate().toMillisSinceEpoch());
+ 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");
+ }
+ 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");
+ }
break;
+ }
default:
- uassert(16554,
- str::stream() << "$add only supports numeric or date types, not "
- << typeName(val.getType()),
- val.nullish());
- return Value(BSONNULL);
+ MONGO_UNREACHABLE;
}
}
- 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));
+ 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()));
}
- 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 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;
}
+ return state.getValue();
}
REGISTER_STABLE_EXPRESSION(add, ExpressionAdd::parse);
@@ -2134,6 +2227,17 @@ 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 {
@@ -2507,6 +2611,11 @@ 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);
@@ -2983,9 +3092,11 @@ const std::string sortKeyName = "sortKey";
const std::string searchScoreDetailsName = "searchScoreDetails";
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},
@@ -3001,6 +3112,7 @@ 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},
@@ -3044,6 +3156,9 @@ Value ExpressionMeta::serialize(bool explain) 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:
@@ -5535,14 +5650,45 @@ StatusWith<Value> ExpressionSubtract::apply(Value lhs, Value rhs) {
} else if (lhs.nullish() || rhs.nullish()) {
return Value(BSONNULL);
} else if (lhs.getType() == 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");
+ 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");
}
} else {
return Status(ErrorCodes::TypeMismatch,
@@ -5556,24 +5702,35 @@ 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 (auto&& branch : _branches) {
- Value caseExpression(branch.first->evaluate(root, variables));
+ for (int i = 0; i < numBranches(); ++i) {
+ auto [caseExpr, thenExpr] = getBranch(i);
+ Value caseResult = caseExpr->evaluate(root, variables);
- if (caseExpression.coerceToBool()) {
- return branch.second->evaluate(root, variables);
+ if (caseResult.coerceToBool()) {
+ return thenExpr->evaluate(root, variables);
}
}
uassert(40066,
"$switch could not find a matching branch for an input, and no default was specified.",
- _default);
+ defaultExpr());
- return _default->evaluate(root, variables);
+ return defaultExpr()->evaluate(root, variables);
}
boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* const expCtx,
@@ -5582,7 +5739,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() == Object);
+ expr.type() == BSONType::Object);
boost::intrusive_ptr<Expression> expDefault;
std::vector<boost::intrusive_ptr<Expression>> children;
@@ -5594,13 +5751,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() == Array);
+ elem.type() == BSONType::Array);
for (auto&& branch : elem.Array()) {
uassert(40062,
str::stream() << "$switch expected each branch to be an object, found: "
<< typeName(branch.type()),
- branch.type() == Object);
+ branch.type() == BSONType::Object);
boost::intrusive_ptr<Expression> switchCase, switchThen;
@@ -5632,80 +5789,77 @@ 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;
- }
- }
- uassert(40068, "$switch requires at least one branch.", !branches.empty());
+ return new ExpressionSwitch(expCtx, std::move(children));
+}
- return new ExpressionSwitch(expCtx, std::move(children), std::move(branches));
+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));
}
void ExpressionSwitch::_doAddDependencies(DepsTracker* deps) const {
- for (auto&& branch : _branches) {
- branch.first->addDependencies(deps);
- branch.second->addDependencies(deps);
- }
-
- if (_default) {
- _default->addDependencies(deps);
+ 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);
+ }
}
}
boost::intrusive_ptr<Expression> ExpressionSwitch::optimize() {
- if (_default) {
- _default = _default->optimize();
+ if (defaultExpr()) {
+ _children.back() = _children.back()->optimize();
}
- std::vector<ExpressionPair>::iterator it = _branches.begin();
- bool true_const = false;
+ bool trueConst = false;
- while (!true_const && it != _branches.end()) {
- (it->first) = (it->first)->optimize();
+ 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();
- if (auto* val = dynamic_cast<ExpressionConstant*>((it->first).get())) {
- if (!((val->getValue()).coerceToBool())) {
+ if (auto* val = dynamic_cast<ExpressionConstant*>(caseExpr.get())) {
+ if (!val->getValue().coerceToBool()) {
// Case is constant and evaluates to false, so it is removed.
- it = _branches.erase(it);
+ deleteBranch(i);
} else {
- // 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);
+ // 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;
}
} else {
// Since case is not removed from the switch, its then is now optimized.
- (it->second) = (it->second)->optimize();
- ++it;
+ thenExpr = thenExpr->optimize();
+ ++i;
}
}
// Erasing the rest of the cases because found a default true value.
- if (true_const) {
- _branches.erase(it, _branches.end());
+ if (trueConst) {
+ while (i < numBranches()) {
+ deleteBranch(i);
+ }
}
// If there are no cases, make the switch its default.
- if (_branches.size() == 0 && _default) {
- return _default;
- } else if (_branches.size() == 0) {
+ if (numBranches() == 0) {
uassert(40069,
- "One cannot execute a switch statement where all the cases evaluate to false "
- "without a default.",
- _branches.size());
+ "Cannot execute a switch statement where all the cases evaluate to false "
+ "without a default",
+ defaultExpr());
+ return _children.back();
}
return this;
@@ -5713,17 +5867,18 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::optimize() {
Value ExpressionSwitch::serialize(bool explain) const {
std::vector<Value> serializedBranches;
- serializedBranches.reserve(_branches.size());
+ serializedBranches.reserve(numBranches());
- for (auto&& branch : _branches) {
- serializedBranches.push_back(Value(Document{{"case", branch.first->serialize(explain)},
- {"then", branch.second->serialize(explain)}}));
+ for (int i = 0; i < numBranches(); ++i) {
+ auto [caseExpr, thenExpr] = getBranch(i);
+ serializedBranches.push_back(Value(Document{{"case", caseExpr->serialize(explain)},
+ {"then", thenExpr->serialize(explain)}}));
}
- if (_default) {
+ if (defaultExpr()) {
return Value(Document{{"$switch",
Document{{"branches", Value(serializedBranches)},
- {"default", _default->serialize(explain)}}}});
+ {"default", defaultExpr()->serialize(explain)}}}});
}
return Value(Document{{"$switch", Document{{"branches", Value(serializedBranches)}}}});
@@ -7551,6 +7706,15 @@ 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);
@@ -7575,6 +7739,11 @@ 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);
@@ -7604,6 +7773,11 @@ 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);
@@ -7776,6 +7950,13 @@ 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,
diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h
index 9cce6d0b1e2..4ad28bf3d04 100644
--- a/src/mongo/db/pipeline/expression.h
+++ b/src/mongo/db/pipeline/expression.h
@@ -49,6 +49,7 @@
#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"
@@ -170,7 +171,7 @@ public:
*/
struct ComputedPaths {
// Non-rename computed paths.
- std::set<std::string> paths;
+ OrderedPathSet paths;
// Mappings from the old name of a path before applying this expression, to the new one
// after applying this expression.
@@ -339,6 +340,18 @@ 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;
+ }
+
protected:
using ExpressionVector = std::vector<boost::intrusive_ptr<Expression>>;
@@ -737,6 +750,10 @@ protected:
void _doAddDependencies(DepsTracker* deps) const override;
private:
+ monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final {
+ return monotonic::State::Constant;
+ }
+
Value _value;
};
@@ -943,6 +960,11 @@ 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());
+ };
};
@@ -1188,6 +1210,11 @@ 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);
+ }
};
@@ -1662,6 +1689,8 @@ 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;
@@ -1832,6 +1861,8 @@ protected:
private:
+ monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final;
+
/*
Internal implementation of evaluate(), used recursively.
@@ -1923,6 +1954,11 @@ 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);
+ }
};
@@ -2860,7 +2896,10 @@ public:
}
bool isCommutative() const final {
- return true;
+ // 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;
}
void acceptVisitor(ExpressionMutableVisitor* visitor) final {
@@ -3276,6 +3315,9 @@ public:
void acceptVisitor(ExpressionConstVisitor* visitor) const final {
return visitor->visit(this);
}
+
+private:
+ monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final;
};
@@ -3285,11 +3327,10 @@ public:
std::pair<boost::intrusive_ptr<Expression>&, boost::intrusive_ptr<Expression>&>;
ExpressionSwitch(ExpressionContext* const expCtx,
- std::vector<boost::intrusive_ptr<Expression>> children,
- std::vector<ExpressionPair> branches)
- : Expression(expCtx, std::move(children)),
- _default(_children.back()),
- _branches(std::move(branches)) {}
+ std::vector<boost::intrusive_ptr<Expression>> children)
+ : Expression(expCtx, std::move(children)) {
+ uassert(40068, "$switch requires at least one branch", numBranches() >= 1);
+ }
Value evaluate(const Document& root, Variables* variables) const final;
boost::intrusive_ptr<Expression> optimize() final;
@@ -3306,12 +3347,38 @@ 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:
- boost::intrusive_ptr<Expression>& _default;
- std::vector<ExpressionPair> _branches;
+ // Helper for 'optimize()'. Deletes the 'case' and 'then' children associated with the i-th
+ // branch of the switch.
+ void deleteBranch(int i);
};
@@ -3982,6 +4049,10 @@ 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;
@@ -4016,10 +4087,14 @@ public:
}
private:
- virtual Value evaluateDateArithmetics(Date_t date,
- TimeUnit unit,
- long long amount,
- const TimeZone& timezone) const override;
+ 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;
};
class ExpressionDateSubtract final : public ExpressionDateArithmetics {
@@ -4039,10 +4114,14 @@ public:
}
private:
- virtual Value evaluateDateArithmetics(Date_t date,
- TimeUnit unit,
- long long amount,
- const TimeZone& timezone) const override;
+ 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;
};
struct SubstituteFieldPathWalker {
@@ -4128,6 +4207,8 @@ 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;
diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp
index 2c41c612595..7258456a1ac 100644
--- a/src/mongo/db/pipeline/expression_context.cpp
+++ b/src/mongo/db/pipeline/expression_context.cpp
@@ -251,11 +251,27 @@ 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 3f83fba6c8e..38747fd60ae 100644
--- a/src/mongo/db/pipeline/expression_context.h
+++ b/src/mongo/db/pipeline/expression_context.h
@@ -60,15 +60,6 @@ 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 {
@@ -118,6 +109,8 @@ public:
struct ExpressionCounters {
StringMap<uint64_t> aggExprCountersMap;
StringMap<uint64_t> matchExprCountersMap;
+ StringMap<uint64_t> groupAccumulatorExprCountersMap;
+ StringMap<uint64_t> windowAccumulatorExprCountersMap;
};
/**
@@ -380,6 +373,16 @@ 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.
*/
@@ -433,7 +436,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.
+ // True if this 'ExpressionContext' object is for the inner side of a $lookup or $graphLookup.
bool inLookup = false;
// If set, this will disallow use of features introduced in versions above the provided version.
@@ -481,11 +484,57 @@ public:
// The resume token version that should be generated by a change stream.
int changeStreamTokenVersion = ResumeTokenData::kDefaultTokenVersion;
+ // If set to true, always use 'changeStreamTokenVersion' when resuming a stream, regardless of
+ // the client resume token's version.
+ bool ignoreTokenVersionOnResume = false;
+
// True if the expression context is the original one for a given pipeline.
// False if another context is created for the same pipeline. Used to disable duplicate
// 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;
@@ -510,6 +559,8 @@ protected:
bool _isCappedDelete = false;
+ bool _requiresTimeseriesExtendedRangeSupport = false;
+
private:
boost::optional<ExpressionCounters> _expressionCounters = boost::none;
};
diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp
index efe0a577ef6..9205ea29633 100644
--- a/src/mongo/db/pipeline/expression_date_test.cpp
+++ b/src/mongo/db/pipeline/expression_date_test.cpp
@@ -1771,7 +1771,7 @@ TEST_F(ExpressionDateDiffTest, AddsDependencies) {
auto depsTracker = dateDiffExpression->getDependencies();
ASSERT_TRUE(
(depsTracker.fields ==
- std::set<std::string>{
+ OrderedPathSet{
"startDateField", "endDateField", "unitField", "timezoneField", "startOfWeekField"}));
}
} // namespace ExpressionDateDiffTest
@@ -1869,7 +1869,7 @@ TEST_F(ExpressionDateTruncTest, AddsDependencies) {
const auto depsTracker = dateTruncExpression->getDependencies();
ASSERT_TRUE(
(depsTracker.fields ==
- std::set<std::string>{
+ OrderedPathSet{
"dateField", "unitField", "binSizeField", "timezoneField", "startOfWeekField"}));
}
} // namespace
diff --git a/src/mongo/db/pipeline/expression_js_emit.cpp b/src/mongo/db/pipeline/expression_js_emit.cpp
index d19a9c53191..ca6ba238c6e 100644
--- a/src/mongo/db/pipeline/expression_js_emit.cpp
+++ b/src/mongo/db/pipeline/expression_js_emit.cpp
@@ -145,11 +145,14 @@ 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. 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);
+
+ // 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);
// 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_test.cpp b/src/mongo/db/pipeline/expression_test.cpp
index fd6f1c3490e..03fc4d3b663 100644
--- a/src/mongo/db/pipeline/expression_test.cpp
+++ b/src/mongo/db/pipeline/expression_test.cpp
@@ -1121,7 +1121,7 @@ TEST(ExpressionSwitch, ExpressionSwitchWithAllConstantFalsesAndNoDefaultErrors)
ASSERT_THROWS_CODE(switchExp->optimize(), AssertionException, 40069);
}
-TEST(ExpressionSwitch, ExpressionSwitchWithZeroAsConstantFalsesAndNoDefaulErrors) {
+TEST(ExpressionSwitch, ExpressionSwitchWithZeroAsConstantFalseAndNoDefaultErrors) {
auto expCtx = ExpressionContextForTest{};
VariablesParseState vps = expCtx.variablesParseState;
@@ -1233,6 +1233,62 @@ 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;
@@ -3105,6 +3161,18 @@ 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 {
diff --git a/src/mongo/db/pipeline/field_path.cpp b/src/mongo/db/pipeline/field_path.cpp
index ab00617bbd3..8839b906daf 100644
--- a/src/mongo/db/pipeline/field_path.cpp
+++ b/src/mongo/db/pipeline/field_path.cpp
@@ -54,7 +54,12 @@ const StringDataSet kAllowedDollarPrefixedFields = {
"$sortKey",
// This is necessary for the "showRecordId" feature.
- "$recordId"};
+ "$recordId",
+
+ // This is necessary for $search queries with a specified sort.
+ "$searchSortValues"_sd,
+ "$searchScore"_sd,
+};
} // namespace
diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.cpp b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp
new file mode 100644
index 00000000000..e1c711f30f3
--- /dev/null
+++ b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp
@@ -0,0 +1,87 @@
+/**
+ * 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 {
+
+ 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, 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
new file mode 100644
index 00000000000..541cd87967c
--- /dev/null
+++ b/src/mongo/db/pipeline/group_from_first_document_transformation.h
@@ -0,0 +1,92 @@
+/**
+ * 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 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.h b/src/mongo/db/pipeline/javascript_execution.h
index 924ae447216..d95e90f6eea 100644
--- a/src/mongo/db/pipeline/javascript_execution.h
+++ b/src/mongo/db/pipeline/javascript_execution.h
@@ -104,11 +104,8 @@ public:
* Injects the given function 'emitFn' as a native JS function named 'emit', callable from
* user-defined functions.
*/
- void injectEmitIfNecessary(NativeFunction emitFn, void* data) {
- if (!_emitCreated) {
- _scope->injectNative("emit", emitFn, data);
- _emitCreated = true;
- }
+ void injectEmit(NativeFunction emitFn, void* data) {
+ _scope->injectNative("emit", emitFn, data);
}
Scope* getScope() {
@@ -118,7 +115,6 @@ 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 34648576917..aed1d4fefcf 100644
--- a/src/mongo/db/pipeline/lite_parsed_document_source.h
+++ b/src/mongo/db/pipeline/lite_parsed_document_source.h
@@ -171,6 +171,20 @@ 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 36b1d78d9b7..f5760429206 100644
--- a/src/mongo/db/pipeline/lite_parsed_pipeline.cpp
+++ b/src/mongo/db/pipeline/lite_parsed_pipeline.cpp
@@ -157,7 +157,6 @@ 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);
@@ -179,25 +178,10 @@ 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 712d23c32bd..2c424a84ee3 100644
--- a/src/mongo/db/pipeline/lite_parsed_pipeline.h
+++ b/src/mongo/db/pipeline/lite_parsed_pipeline.h
@@ -123,6 +123,13 @@ 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 {
@@ -132,6 +139,13 @@ public:
}
/**
+ * Returns true if the pipeline ends with a $changeStreamSplitLargeEvent stage.
+ */
+ bool endsWithChangeStreamSplitLargeEvent() const {
+ return !_stageSpecs.empty() && _stageSpecs.back()->isChangeStreamSplitLargeEvent();
+ }
+
+ /**
* Returns false if the pipeline has any stages which cannot be passed through to the shards.
*/
bool allowedToPassthroughFromMongos() const {
@@ -207,8 +221,7 @@ public:
/**
* Verifies that the pipeline contains valid stages. Optionally calls
- * 'validatePipelineStagesforAPIVersion' with 'opCtx', and throws UserException if there is
- * more than one $_internalUnpackBucket stage in the pipeline.
+ * 'validatePipelineStagesforAPIVersion' with 'opCtx'.
*/
void validate(const OperationContext* opCtx, bool performApiVersionChecks = true) const;
diff --git a/src/mongo/db/pipeline/lookup_set_cache_test.cpp b/src/mongo/db/pipeline/lookup_set_cache_test.cpp
index 6670b5b5971..ec78b0b0572 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.getApproximateSize();
+ const auto sizeOfDoc1Before = doc1.getCurrentApproximateSize();
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.getApproximateSize());
+ ASSERT_EQ(cache.getMemoryUsage(), prevCacheSize + doc2.getCurrentApproximateSize());
// 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.getApproximateSize());
+ ASSERT_LT(sizeOfDoc1Before, doc1.getCurrentApproximateSize());
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
new file mode 100644
index 00000000000..55d5e133b6f
--- /dev/null
+++ b/src/mongo/db/pipeline/map_reduce_options.idl
@@ -0,0 +1,42 @@
+# 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 275183081ef..b91f0eb57ac 100644
--- a/src/mongo/db/pipeline/memory_usage_tracker.h
+++ b/src/mongo/db/pipeline/memory_usage_tracker.h
@@ -50,20 +50,8 @@ public:
PerFunctionMemoryTracker() = delete;
void update(long long diff) {
-
- // 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));
+ // TODO SERVER-61281: Check for memory underflow.
+ set(std::max(_currentMemoryBytes + diff, 0ll));
}
void set(long long total) {
@@ -154,11 +142,8 @@ public:
* Updates total memory usage.
*/
void update(long long diff) {
- 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);
+ // TODO SERVER-61281: Check for memory underflow.
+ set(std::max(_memoryUsageBytes + diff, 0ll));
}
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 cc354e7fd69..f60933a4655 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);
}
-DEATH_TEST_F(MemoryUsageTrackerTest,
- UpdateGlobalToNegativeIsDisallowed,
- "Underflow on memory tracking") {
- _tracker.set(50LL);
+// 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);
ASSERT_EQ(_tracker.currentMemoryBytes(), 50LL);
ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL);
- _tracker.update(-100);
+ _funcTracker.update(-100);
+ ASSERT_EQ(_tracker.currentMemoryBytes(), 0LL);
+ ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL);
}
-TEST_F(MemoryUsageTrackerTest, UpdateFunctionUsageToNegativeIsDisallowed) {
- _funcTracker.set(50LL);
+// TODO SERVER-61281: Switch to 'DEATH_TEST_F' checking the underflow case.
+TEST_F(MemoryUsageTrackerTest, UpdateMemUsageToNegativeIsDisallowed) {
+ _tracker.set(50LL);
ASSERT_EQ(_tracker.currentMemoryBytes(), 50LL);
ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL);
- // 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);
+ _tracker.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
new file mode 100644
index 00000000000..bc34b756fec
--- /dev/null
+++ b/src/mongo/db/pipeline/monotonic_expression.cpp
@@ -0,0 +1,60 @@
+/**
+ * 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
new file mode 100644
index 00000000000..6f1664cf199
--- /dev/null
+++ b/src/mongo/db/pipeline/monotonic_expression.h
@@ -0,0 +1,70 @@
+/**
+ * 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
new file mode 100644
index 00000000000..0c667108201
--- /dev/null
+++ b/src/mongo/db/pipeline/monotonic_expression_test.cpp
@@ -0,0 +1,149 @@
+/**
+ * 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 97a896e5898..22ca4e7a85f 100644
--- a/src/mongo/db/pipeline/pipeline.cpp
+++ b/src/mongo/db/pipeline/pipeline.cpp
@@ -40,6 +40,7 @@
#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"
@@ -105,14 +106,32 @@ 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.
- if (firstStageConstraints.isChangeStreamStage()) {
- for (auto&& source : sources) {
- uassert(ErrorCodes::IllegalOperation,
- str::stream() << source->getSourceName()
- << " is not permitted in a $changeStream pipeline",
- source->constraints().isAllowedInChangeStream());
+ 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;
}
}
+ 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.
@@ -231,39 +250,41 @@ 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);
- for (auto&& stage : _sources) {
+ // 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;
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 && 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));
+ !(constraints.requiredPosition == PositionRequirement::kFirst &&
+ sourceIter != _sources.begin()));
+ // 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",
- !(i != 0 && matchStage && matchStage->isTextQuery()));
+ !(sourceIter != _sources.begin() && matchStage && matchStage->isTextQuery()));
uassert(40601,
str::stream() << stage->getSourceName()
<< " can only be the final stage in the pipeline",
!(constraints.requiredPosition == PositionRequirement::kLast &&
- i != _sources.size() - 1));
- ++i;
+ 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);
+ }
// Verify that we are not attempting to run a mongoS-only stage on mongoD.
uassert(40644,
@@ -275,6 +296,12 @@ 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));
}
}
@@ -721,6 +748,24 @@ Pipeline::SourceContainer::iterator Pipeline::optimizeEndOfPipeline(
return std::next(itr);
}
+Pipeline::SourceContainer::iterator Pipeline::optimizeAtEndOfPipeline(
+ Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) {
+ if (itr == container->end()) {
+ return itr;
+ }
+ itr = std::next(itr);
+ try {
+ while (itr != container->end()) {
+ invariant((*itr).get());
+ itr = (*itr).get()->optimizeAt(itr, container);
+ }
+ } catch (DBException& ex) {
+ ex.addContext("Failed to optimize pipeline");
+ throw;
+ }
+ return itr;
+}
+
std::unique_ptr<Pipeline, PipelineDeleter> Pipeline::makePipelineFromViewDefinition(
const boost::intrusive_ptr<ExpressionContext>& subPipelineExpCtx,
ExpressionContext::ResolvedNamespace resolvedNs,
diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h
index 96cf6426be3..8965f493dc2 100644
--- a/src/mongo/db/pipeline/pipeline.h
+++ b/src/mongo/db/pipeline/pipeline.h
@@ -171,6 +171,16 @@ public:
static Pipeline::SourceContainer::iterator optimizeEndOfPipeline(
Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container);
+ /**
+ * Applies optimizeAt() to all stages in the given pipeline after the stage that 'itr' points
+ * to.
+ *
+ * Returns a valid iterator that points to the new "end of the pipeline": i.e., the stage that
+ * comes after 'itr' in the newly optimized pipeline.
+ */
+ static Pipeline::SourceContainer::iterator optimizeAtEndOfPipeline(
+ Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container);
+
static std::unique_ptr<Pipeline, PipelineDeleter> makePipelineFromViewDefinition(
const boost::intrusive_ptr<ExpressionContext>& subPipelineExpCtx,
ExpressionContext::ResolvedNamespace resolvedNs,
diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp
index b21e8635c51..7d2eef919f4 100644
--- a/src/mongo/db/pipeline/pipeline_d.cpp
+++ b/src/mongo/db/pipeline/pipeline_d.cpp
@@ -42,7 +42,6 @@
#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"
@@ -802,10 +801,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<DocumentSourceGroup>>
+std::pair<boost::intrusive_ptr<DocumentSourceSort>, boost::intrusive_ptr<DocumentSourceGroupBase>>
getSortAndGroupStagesFromPipeline(const Pipeline::SourceContainer& sources) {
boost::intrusive_ptr<DocumentSourceSort> sortStage = nullptr;
- boost::intrusive_ptr<DocumentSourceGroup> groupStage = nullptr;
+ boost::intrusive_ptr<DocumentSourceGroupBase> groupStage = nullptr;
auto sourcesIt = sources.begin();
if (sourcesIt != sources.end()) {
@@ -821,7 +820,7 @@ getSortAndGroupStagesFromPipeline(const Pipeline::SourceContainer& sources) {
}
if (sourcesIt != sources.end()) {
- groupStage = dynamic_cast<DocumentSourceGroup*>(sourcesIt->get());
+ groupStage = dynamic_cast<DocumentSourceGroupBase*>(sourcesIt->get());
}
return std::make_pair(sortStage, groupStage);
@@ -929,8 +928,10 @@ 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.
- if (part.fieldPath && *part.fieldPath == bucketUnpacker.getTimeField()) {
+ // 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 that the directions agree.
if ((scan->getDirection() == CollectionScanParams::Direction::FORWARD) ==
part.isAscending)
@@ -1037,6 +1038,15 @@ 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;
@@ -1208,12 +1218,7 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll
// sort optimization. We check eligibility and perform the rewrite here.
auto [unpack, sort] = findUnpackThenSort(pipeline->_sources);
QueryPlannerParams plannerOpts;
- if (serverGlobalParams.featureCompatibility.isVersionInitialized() &&
- serverGlobalParams.featureCompatibility.isGreaterThanOrEqualTo(
- multiversion::FeatureCompatibilityVersion::kVersion_6_0) &&
- feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabled(
- serverGlobalParams.featureCompatibility) &&
- unpack && sort) {
+ if (feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV() && unpack && sort) {
plannerOpts.traversalPreference = createTimeSeriesTraversalPreference(unpack, sort);
}
@@ -1235,12 +1240,7 @@ 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 (serverGlobalParams.featureCompatibility.isVersionInitialized() &&
- serverGlobalParams.featureCompatibility.isGreaterThanOrEqualTo(
- multiversion::FeatureCompatibilityVersion::kVersion_6_0) &&
- feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabled(
- serverGlobalParams.featureCompatibility) &&
- unpack && sort) {
+ if (feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV() && unpack && sort) {
auto execImpl = dynamic_cast<PlanExecutorImpl*>(exec.get());
if (execImpl) {
@@ -1306,7 +1306,7 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll
dynamic_cast<const DocumentSourceInternalUnpackBucket*>(
iter->get())) {
unpackIter = iter;
- uassert(6505001,
+ tassert(6505001,
str::stream()
<< "Expected at most one "
<< DocumentSourceInternalUnpackBucket::kStageNameInternal
@@ -1669,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(DocumentSourceGroup::kStageName);
+ pipeline->popFrontWithName(rewrittenGroupStage->originalStageName());
boost::intrusive_ptr<DocumentSource> groupTransform(
new DocumentSourceSingleDocumentTransformation(
diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp
index 1854c07a97a..cd1b99870bc 100644
--- a/src/mongo/db/pipeline/pipeline_test.cpp
+++ b/src/mongo/db/pipeline/pipeline_test.cpp
@@ -75,6 +75,7 @@ using std::string;
using std::vector;
const NamespaceString kTestNss = NamespaceString("a.collection");
+const NamespaceString kAdminCollectionlessNss = NamespaceString("admin.$cmd.aggregate");
constexpr size_t getChangeStreamStageSize() {
return 6;
@@ -114,7 +115,8 @@ class StubExplainInterface : public StubMongoProcessInterface {
};
void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson,
std::string outputPipeJson,
- std::string serializedPipeJson) {
+ std::string serializedPipeJson,
+ NamespaceString aggNss = kTestNss) {
QueryTestServiceContext testServiceContext;
auto opCtx = testServiceContext.makeOperationContext();
@@ -128,7 +130,7 @@ void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson,
ASSERT_EQUALS(stageElem.type(), BSONType::Object);
rawPipeline.push_back(stageElem.embeddedObject());
}
- AggregateCommandRequest request(kTestNss, rawPipeline);
+ AggregateCommandRequest request(aggNss, rawPipeline);
intrusive_ptr<ExpressionContextForTest> ctx =
new ExpressionContextForTest(opCtx.get(), request);
ctx->mongoProcessInterface = std::make_shared<StubExplainInterface>();
@@ -2981,6 +2983,71 @@ 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(
"["
@@ -4599,7 +4666,7 @@ public:
* Returns a description which communicate that this stage modifies nothing.
*/
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>(), {}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet(), {}};
}
};
@@ -4676,7 +4743,7 @@ public:
* Returns a description which communicate that this stage modifies nothing.
*/
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>(), {}};
+ return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet(), {}};
}
};
@@ -4712,7 +4779,7 @@ public:
return new RenamesAToB(expCtx);
}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"b", "a"}}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"b", "a"}}};
}
};
@@ -4836,7 +4903,7 @@ public:
return new RenamesBToC(expCtx);
}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"c", "b"}}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"c", "b"}}};
}
};
@@ -4879,7 +4946,7 @@ public:
return new RenamesBToA(expCtx);
}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"a", "b"}}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"a", "b"}}};
}
};
diff --git a/src/mongo/db/pipeline/plan_executor_pipeline.cpp b/src/mongo/db/pipeline/plan_executor_pipeline.cpp
index 0b26a7db813..958c8a653af 100644
--- a/src/mongo/db/pipeline/plan_executor_pipeline.cpp
+++ b/src/mongo/db/pipeline/plan_executor_pipeline.cpp
@@ -39,6 +39,11 @@
#include "mongo/db/repl/speculative_majority_read_info.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,
@@ -78,9 +83,7 @@ PlanExecutor::ExecState PlanExecutorPipeline::getNext(BSONObj* objOut, RecordId*
Document docOut;
auto execState = getNextDocument(&docOut, nullptr);
if (execState == PlanExecutor::ADVANCED) {
- // Include metadata if the output will be consumed by a merging node.
- *objOut = _expCtx->needsMerge || _expCtx->forPerShardCursor ? docOut.toBsonWithMetaData()
- : docOut.toBson();
+ *objOut = _trySerializeToBson(docOut);
}
return execState;
}
@@ -140,6 +143,19 @@ 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:
@@ -185,9 +201,8 @@ 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 = eventBSON.getObjectField("_id");
+ auto idField = event.getField("_id");
invariant(!resumeToken.missing());
uassert(ErrorCodes::ChangeStreamFatalError,
str::stream() << "Encountered an event whose _id field, which contains the resume "
@@ -196,9 +211,9 @@ void PlanExecutorPipeline::_validateChangeStreamsResumeToken(const Document& eve
"transformations that retain the unmodified _id field are allowed. "
"Expected: "
<< BSON("_id" << resumeToken) << " but found: "
- << (eventBSON["_id"] ? BSON("_id" << eventBSON["_id"]) : BSONObj()),
- (resumeToken.getType() == BSONType::Object) &&
- idField.binaryEqual(resumeToken.getDocument().toBson()));
+ << (idField.missing() ? BSONObj() : BSON("_id" << idField)),
+ resumeToken.getType() == BSONType::Object &&
+ ValueComparator::kInstance.evaluate(idField == resumeToken));
}
void PlanExecutorPipeline::_performResumableOplogScanAccounting() {
diff --git a/src/mongo/db/pipeline/plan_executor_pipeline.h b/src/mongo/db/pipeline/plan_executor_pipeline.h
index 7192de3668f..52139d1b34d 100644
--- a/src/mongo/db/pipeline/plan_executor_pipeline.h
+++ b/src/mongo/db/pipeline/plan_executor_pipeline.h
@@ -182,6 +182,11 @@ 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/process_interface/SConscript b/src/mongo/db/pipeline/process_interface/SConscript
index 44b0afe1591..c9d3c820073 100644
--- a/src/mongo/db/pipeline/process_interface/SConscript
+++ b/src/mongo/db/pipeline/process_interface/SConscript
@@ -47,6 +47,7 @@ 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',
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 469ce5821aa..34497978daa 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
@@ -45,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/write_conflict_exception.h"
+#include "mongo/db/concurrency/exception_util.h"
#include "mongo/db/curop.h"
#include "mongo/db/cursor_manager.h"
#include "mongo/db/db_raii.h"
@@ -200,7 +200,8 @@ std::vector<Document> CommonMongodProcessInterface::getIndexStats(OperationConte
auto idxCatalog = collection->getIndexCatalog();
auto idx = idxCatalog->findIndexByName(opCtx,
indexName,
- /* includeUnfinishedIndexes */ true);
+ IndexCatalog::InclusionPolicy::kReady |
+ IndexCatalog::InclusionPolicy::kUnfinished);
uassert(ErrorCodes::IndexNotFound,
"Could not find entry in IndexCatalog for index " + indexName,
idx);
@@ -604,7 +605,8 @@ bool CommonMongodProcessInterface::fieldsHaveSupportingUniqueIndex(
return fieldPaths == std::set<FieldPath>{"_id"};
}
- auto indexIterator = collection->getIndexCatalog()->getIndexIterator(opCtx, false);
+ auto indexIterator = collection->getIndexCatalog()->getIndexIterator(
+ opCtx, IndexCatalog::InclusionPolicy::kReady);
while (indexIterator->more()) {
const IndexCatalogEntry* entry = indexIterator->next();
if (supportsUniqueKey(expCtx, entry, fieldPaths)) {
@@ -745,59 +747,6 @@ 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 4a02ecce883..f979430de5f 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
@@ -142,23 +142,6 @@ 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 513edd5a6f4..55dc54837b1 100644
--- a/src/mongo/db/pipeline/process_interface/common_process_interface.h
+++ b/src/mongo/db/pipeline/process_interface/common_process_interface.h
@@ -32,6 +32,7 @@
#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 {
@@ -46,6 +47,70 @@ 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.
@@ -64,6 +129,7 @@ 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 19477adf8c9..5ee0e401924 100644
--- a/src/mongo/db/pipeline/process_interface/mongo_process_interface.h
+++ b/src/mongo/db/pipeline/process_interface/mongo_process_interface.h
@@ -78,7 +78,7 @@ 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 folliwng elements:
+ * in the batch we store a tuple of the following 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
@@ -106,6 +106,30 @@ 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.
*/
@@ -127,6 +151,12 @@ 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.
*/
@@ -149,29 +179,30 @@ public:
virtual void updateClientOperationTime(OperationContext* opCtx) const = 0;
/**
- * 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.
+ * 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.
*/
virtual Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- std::vector<BSONObj>&& objs,
+ std::unique_ptr<write_ops::InsertCommandRequest> insertCommand,
const WriteConcernOptions& wc,
boost::optional<OID> targetEpoch) = 0;
/**
- * 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.
+ * 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.
*/
- 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;
+ 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;
/**
* Returns index usage statistics for each index on collection 'ns' along with additional
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 6f17c7a0121..fb0890f5d61 100644
--- a/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp
+++ b/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp
@@ -99,6 +99,12 @@ 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,
@@ -175,8 +181,15 @@ 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);
+ auto requests =
+ getVersionedRequestsForTargetedShards(expCtx->opCtx,
+ nss,
+ cm,
+ findCmd,
+ filterObj,
+ CollationSpec::kSimpleSpec,
+ boost::none /*letParameters*/,
+ boost::none /*runtimeConstants*/);
// 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 82eedfa6dec..28d056b7aa9 100644
--- a/src/mongo/db/pipeline/process_interface/mongos_process_interface.h
+++ b/src/mongo/db/pipeline/process_interface/mongos_process_interface.h
@@ -45,6 +45,9 @@ 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,
@@ -69,7 +72,7 @@ public:
Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- std::vector<BSONObj>&& objs,
+ std::unique_ptr<write_ops::InsertCommandRequest> insertCommand,
const WriteConcernOptions& wc,
boost::optional<OID>) final {
MONGO_UNREACHABLE;
@@ -77,7 +80,7 @@ public:
StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
const WriteConcernOptions& wc,
UpsertType upsert,
bool multi,
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 682c0075340..d6d33e12fe7 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/write_conflict_exception.h"
+#include "mongo/db/concurrency/exception_util.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::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));
+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);
// Need to check each result in the batch since the writes are unordered.
for (const auto& result : writeResults.results) {
@@ -116,13 +116,12 @@ Status NonShardServerProcessInterface::insert(const boost::intrusive_ptr<Express
StatusWith<MongoProcessInterface::UpdateResult> NonShardServerProcessInterface::update(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
const WriteConcernOptions& wc,
UpsertType upsert,
bool multi,
boost::optional<OID> targetEpoch) {
- auto writeResults = write_ops_exec::performUpdates(
- expCtx->opCtx, buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi));
+ auto writeResults = write_ops_exec::performUpdates(expCtx->opCtx, *updateCommand);
// Need to check each result in the batch since the writes are unordered.
UpdateResult updateResult;
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 ccbe90205c9..9b96e83a1a8 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::vector<BSONObj>&& objs,
+ std::unique_ptr<write_ops::InsertCommandRequest> insertCommand,
const WriteConcernOptions& wc,
boost::optional<OID> targetEpoch) override;
StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
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 01db33c2337..694038eff96 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,7 +35,6 @@
#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"
@@ -70,26 +69,27 @@ void ReplicaSetNodeProcessInterface::setReplicaSetNodeExecutor(
replicaSetNodeExecutor(service) = std::move(executor);
}
-Status ReplicaSetNodeProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx,
- const NamespaceString& ns,
- std::vector<BSONObj>&& objs,
- const WriteConcernOptions& wc,
- boost::optional<OID> targetEpoch) {
+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) {
auto&& opCtx = expCtx->opCtx;
if (_canWriteLocally(opCtx, ns)) {
- return NonShardServerProcessInterface::insert(expCtx, ns, std::move(objs), wc, targetEpoch);
+ return NonShardServerProcessInterface::insert(
+ expCtx, ns, std::move(insertCommand), wc, targetEpoch);
}
- BatchedCommandRequest insertCommand(
- buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation));
+ BatchedCommandRequest batchInsertCommand(std::move(insertCommand));
- return _executeCommandOnPrimary(opCtx, ns, std::move(insertCommand.toBSON())).getStatus();
+ return _executeCommandOnPrimary(opCtx, ns, batchInsertCommand.toBSON()).getStatus();
}
StatusWith<MongoProcessInterface::UpdateResult> ReplicaSetNodeProcessInterface::update(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
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(batch), wc, upsert, multi, targetEpoch);
+ expCtx, ns, std::move(updateCommand), wc, upsert, multi, targetEpoch);
}
+ BatchedCommandRequest batchUpdateCommand(std::move(updateCommand));
- BatchedCommandRequest updateCommand(buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi));
- auto result = _executeCommandOnPrimary(opCtx, ns, std::move(updateCommand.toBSON()));
+ auto result = _executeCommandOnPrimary(opCtx, ns, batchUpdateCommand.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 c61f654e844..55b645c59aa 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,6 +43,15 @@ 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);
@@ -59,12 +68,13 @@ public:
Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- std::vector<BSONObj>&& objs,
+ std::unique_ptr<write_ops::InsertCommandRequest> insertCommand,
const WriteConcernOptions& wc,
boost::optional<OID> targetEpoch) final;
+
StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
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 69b5a111e2b..4cb90818562 100644
--- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp
+++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp
@@ -29,8 +29,6 @@
#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>
@@ -51,7 +49,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.h"
+#include "mongo/s/router_role.h"
#include "mongo/s/stale_shard_version_helpers.h"
namespace mongo {
@@ -76,14 +74,11 @@ void ShardServerProcessInterface::checkRoutingInfoEpochOrThrow(
catalogCache->invalidateShardOrEntireCollectionEntryForShardedCollection(
nss, targetCollectionVersion, shardId);
- const auto routingInfo =
- uassertStatusOK(catalogCache->getCollectionRoutingInfo(expCtx->opCtx, nss));
-
- const auto foundVersion =
- routingInfo.isSharded() ? routingInfo.getVersion() : ChunkVersion::UNSHARDED();
+ const auto cm = uassertStatusOK(catalogCache->getCollectionRoutingInfo(expCtx->opCtx, nss));
+ auto foundVersion = cm.isSharded() ? cm.getVersion() : ChunkVersion::UNSHARDED();
- uassert(StaleEpochInfo(nss),
- str::stream() << "Could not act as router for " << nss.ns() << ", wanted "
+ uassert(StaleEpochInfo(nss, targetCollectionVersion, foundVersion),
+ str::stream() << "Could not act as router for " << nss.ns() << ", received "
<< targetCollectionVersion.toString() << ", but found "
<< foundVersion.toString(),
foundVersion.isSameCollection(targetCollectionVersion));
@@ -104,20 +99,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::vector<BSONObj>&& objs,
- const WriteConcernOptions& wc,
- boost::optional<OID> targetEpoch) {
+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) {
BatchedCommandResponse response;
BatchWriteExecStats stats;
- BatchedCommandRequest insertCommand(
- buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation));
+ BatchedCommandRequest batchInsertCommand(std::move(insertCommand));
- insertCommand.setWriteConcern(wc.toBSON());
+ batchInsertCommand.setWriteConcern(wc.toBSON());
- cluster::write(expCtx->opCtx, insertCommand, &stats, &response, targetEpoch);
+ cluster::write(expCtx->opCtx, batchInsertCommand, &stats, &response, targetEpoch);
return response.toStatus();
}
@@ -125,7 +120,7 @@ Status ShardServerProcessInterface::insert(const boost::intrusive_ptr<Expression
StatusWith<MongoProcessInterface::UpdateResult> ShardServerProcessInterface::update(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
const WriteConcernOptions& wc,
UpsertType upsert,
bool multi,
@@ -133,11 +128,10 @@ StatusWith<MongoProcessInterface::UpdateResult> ShardServerProcessInterface::upd
BatchedCommandResponse response;
BatchWriteExecStats stats;
- BatchedCommandRequest updateCommand(buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi));
-
- updateCommand.setWriteConcern(wc.toBSON());
+ BatchedCommandRequest batchUpdateCommand(std::move(updateCommand));
+ batchUpdateCommand.setWriteConcern(wc.toBSON());
- cluster::write(expCtx->opCtx, updateCommand, &stats, &response, targetEpoch);
+ cluster::write(expCtx->opCtx, batchUpdateCommand, &stats, &response, targetEpoch);
if (auto status = response.toStatus(); status != Status::OK()) {
return status;
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 f6026f6ef3a..a08aa23777f 100644
--- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h
+++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h
@@ -53,6 +53,11 @@ 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
@@ -71,23 +76,15 @@ 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::vector<BSONObj>&& objs,
+ std::unique_ptr<write_ops::InsertCommandRequest> insertCommand,
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,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
const WriteConcernOptions& wc,
UpsertType upsert,
bool multi,
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 aceff8e6928..dc562b9089e 100644
--- a/src/mongo/db/pipeline/process_interface/standalone_process_interface.h
+++ b/src/mongo/db/pipeline/process_interface/standalone_process_interface.h
@@ -41,6 +41,11 @@ 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 3fe1430ac72..5c9581b1720 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,6 +52,33 @@ 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 isSharded(OperationContext* opCtx, const NamespaceString& ns) override {
return false;
}
@@ -60,7 +87,7 @@ public:
Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- std::vector<BSONObj>&& objs,
+ std::unique_ptr<write_ops::InsertCommandRequest> insertCommand,
const WriteConcernOptions& wc,
boost::optional<OID>) override {
MONGO_UNREACHABLE;
@@ -68,7 +95,7 @@ public:
StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& ns,
- BatchedObjects&& batch,
+ std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand,
const WriteConcernOptions& wc,
UpsertType upsert,
bool multi,
@@ -227,11 +254,11 @@ public:
return BackupCursorState{UUID::gen(), boost::none, nullptr, {}};
}
- void closeBackupCursor(OperationContext* opCtx, const UUID& backupId) final {}
+ void closeBackupCursor(OperationContext* opCtx, const UUID& backupId) override {}
BackupCursorExtendState extendBackupCursor(OperationContext* opCtx,
const UUID& backupId,
- const Timestamp& extendTo) final {
+ const Timestamp& extendTo) override {
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 a008cc92115..cc1f696fcaa 100644
--- a/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp
+++ b/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp
@@ -35,6 +35,8 @@
#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"
@@ -47,7 +49,6 @@ 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 */),
@@ -55,7 +56,6 @@ 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,7 +92,6 @@ 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 */),
@@ -100,7 +99,6 @@ 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();
@@ -128,9 +126,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.get().toBson(), BSON("a" << BSON("b" << 10) << "c" << 5));
+ ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("a.b" << 10 << "c" << 5));
next = pipeline->getNext();
- ASSERT_BSONOBJ_EQ(next.get().toBson(), BSON("a" << BSON("b" << 20) << "c" << 1));
+ ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("a.b" << 20 << "c" << 1));
ASSERT(!pipeline->getNext());
}
@@ -149,11 +147,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.get().toBson(),
- BSON("a" << BSON("b" << 10 << "c" << -6548868637522515075LL) << "c" << 5));
+ ASSERT_BSONOBJ_EQ(next.value().toBson(),
+ BSON("a.b" << 10 << "c" << 5 << "a.c" << -6548868637522515075LL));
next = pipeline->getNext();
- ASSERT_BSONOBJ_EQ(next.get().toBson(),
- BSON("a" << BSON("b" << 20 << "c" << 2598032665634823220LL) << "c" << 1));
+ ASSERT_BSONOBJ_EQ(next.value().toBson(),
+ BSON("a.b" << 20 << "c" << 1 << "a.c" << 2598032665634823220LL));
ASSERT(!pipeline->getNext());
}
@@ -200,5 +198,78 @@ 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 cc6d3631fd3..8cc4fde72a0 100644
--- a/src/mongo/db/pipeline/resume_token.cpp
+++ b/src/mongo/db/pipeline/resume_token.cpp
@@ -38,7 +38,6 @@
#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"
@@ -88,7 +87,8 @@ 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);
+ (Value::compare(this->eventIdentifier, other.eventIdentifier, nullptr) == 0) &&
+ fragmentNum == other.fragmentNum;
}
std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) {
@@ -102,7 +102,11 @@ std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) {
out << ", fromInvalidate: " << static_cast<bool>(tokenData.fromInvalidate);
}
out << ", uuid: " << tokenData.uuid;
- out << ", eventIdentifier: " << tokenData.eventIdentifier << "}";
+ out << ", eventIdentifier: " << tokenData.eventIdentifier;
+ if (tokenData.version >= 2) {
+ out << ", fragmentNum: " << tokenData.fragmentNum;
+ }
+ out << "}";
return out;
}
@@ -163,6 +167,14 @@ 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());
@@ -285,6 +297,14 @@ 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;
}
diff --git a/src/mongo/db/pipeline/resume_token.h b/src/mongo/db/pipeline/resume_token.h
index a157c97fed9..c4e8b5d06fd 100644
--- a/src/mongo/db/pipeline/resume_token.h
+++ b/src/mongo/db/pipeline/resume_token.h
@@ -107,6 +107,9 @@ 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);
diff --git a/src/mongo/db/pipeline/resume_token_test.cpp b/src/mongo/db/pipeline/resume_token_test.cpp
index eed85344ddd..8005911d5a9 100644
--- a/src/mongo/db/pipeline/resume_token_test.cpp
+++ b/src/mongo/db/pipeline/resume_token_test.cpp
@@ -491,5 +491,51 @@ 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 c1613a6a85f..04aa2de4541 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 std::set<std::string>& pathsOfInterest) {
+ const StringMap<std::string>& renamedPaths, const OrderedPathSet& 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,
- std::set<std::string> pathsOfInterest,
+ OrderedPathSet 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,
- std::set<std::string> pathsOfInterest,
+ OrderedPathSet 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,
- std::set<std::string> pathsOfInterest,
+ OrderedPathSet 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
-std::set<std::string> extractModifiedDependencies(const std::set<std::string>& dependencies,
- const std::set<std::string>& preservedPaths) {
- std::set<std::string> modifiedDependencies;
+OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies,
+ const OrderedPathSet& preservedPaths) {
+ OrderedPathSet 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 @@ std::set<std::string> extractModifiedDependencies(const std::set<std::string>& d
return modifiedDependencies;
}
-boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>& pathsOfInterest,
+boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& pathsOfInterest,
const DocumentSource& stage,
const Direction& traversalDir) {
auto modifiedPathsRet = stage.getModifiedPaths();
@@ -351,19 +351,11 @@ boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>
case DocumentSource::GetModPathsReturn::Type::kAllPaths:
return boost::none;
case DocumentSource::GetModPathsReturn::Type::kFiniteSet: {
- 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;
- }
- }
+ // 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;
}
// None of the paths of interest were modified, construct the result map, mapping
@@ -401,7 +393,7 @@ boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>
boost::optional<StringMap<std::string>> renamedPaths(
const Pipeline::SourceContainer::const_iterator start,
const Pipeline::SourceContainer::const_iterator end,
- const std::set<std::string>& pathsOfInterest,
+ const OrderedPathSet& pathsOfInterest,
boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) {
return renamedPathsFullPipeline(
start, end, pathsOfInterest, Direction::kForward, additionalStageValidatorCallback);
@@ -410,7 +402,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 std::set<std::string>& pathsOfInterest,
+ const OrderedPathSet& pathsOfInterest,
boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) {
return renamedPathsFullPipeline(
start, end, pathsOfInterest, Direction::kBackward, additionalStageValidatorCallback);
@@ -420,7 +412,7 @@ std::pair<Pipeline::SourceContainer::const_iterator, StringMap<std::string>>
findLongestViablePrefixPreservingPaths(
const Pipeline::SourceContainer::const_iterator start,
const Pipeline::SourceContainer::const_iterator end,
- const std::set<std::string>& pathsOfInterest,
+ const OrderedPathSet& 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 1931800b9df..befbddac923 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 std::set<std::string>& pathsOfInterest,
+boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& pathsOfInterest,
const DocumentSource& stage,
const Direction& traversalDir);
/**
@@ -72,7 +72,7 @@ boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>
boost::optional<StringMap<std::string>> renamedPaths(
Pipeline::SourceContainer::const_iterator start,
Pipeline::SourceContainer::const_iterator end,
- const std::set<std::string>& pathsOfInterest,
+ const OrderedPathSet& 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 std::set<std::string>& pathsOfInterest,
+ const OrderedPathSet& 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 std::set<std::string>& pathsOfInterest,
+ const OrderedPathSet& pathsOfInterest,
boost::optional<std::function<bool(DocumentSource*)>>
additionalStageValidatorCallback = boost::none);
@@ -115,10 +115,9 @@ 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).
*/
-std::set<std::string> extractModifiedDependencies(const std::set<std::string>& dependencies,
- const std::set<std::string>& preservedPaths);
+OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies,
+ const OrderedPathSet& preservedPaths);
-bool pathSetContainsOverlappingPath(const std::set<std::string>& paths,
- const std::string& targetPath);
+bool pathSetContainsOverlappingPath(const OrderedPathSet& 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 ca95d9098a2..0b6e4d224bf 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, std::set<std::string>{}, {{"b", "a"}}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"b", "a"}}};
}
};
@@ -132,9 +132,7 @@ public:
: DocumentSourceTestOptimizations(expCtx) {}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kAllExcept,
- std::set<std::string>{"e", "f", "g"},
- {{"d", "c"}}};
+ return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{"e", "f", "g"}, {{"d", "c"}}};
}
};
@@ -196,7 +194,7 @@ public:
: DocumentSourceTestOptimizations(expCtx) {}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kAllExcept, std::set<std::string>{"f.g"}, {{"e", "c.d"}}};
+ return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{"f.g"}, {{"e", "c.d"}}};
}
};
@@ -295,7 +293,7 @@ public:
: DocumentSourceTestOptimizations(expCtx) {}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{"c.d"}, {{"x.y", "a"}}};
+ return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{"c.d"}, {{"x.y", "a"}}};
}
};
@@ -386,7 +384,7 @@ public:
ModifiesAllPaths(const boost::intrusive_ptr<ExpressionContext>& expCtx)
: DocumentSourceTestOptimizations(expCtx) {}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}};
}
};
@@ -415,7 +413,7 @@ public:
ModificationsUnknown(const boost::intrusive_ptr<ExpressionContext>& expCtx)
: DocumentSourceTestOptimizations(expCtx) {}
GetModPathsReturn getModifiedPaths() const final {
- return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>{}, {}};
+ return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet{}, {}};
}
};
diff --git a/src/mongo/db/pipeline/sharded_agg_helpers.cpp b/src/mongo/db/pipeline/sharded_agg_helpers.cpp
index 9a7b4df2242..42e8ae24d32 100644
--- a/src/mongo/db/pipeline/sharded_agg_helpers.cpp
+++ b/src/mongo/db/pipeline/sharded_agg_helpers.cpp
@@ -61,7 +61,7 @@
#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.h"
+#include "mongo/s/router_role.h"
#include "mongo/s/stale_exception.h"
#include "mongo/s/transaction_router.h"
#include "mongo/util/fail_point.h"
@@ -414,7 +414,7 @@ void moveEligibleStreamingStagesBeforeSortOnShards(Pipeline* shardPipe,
// Expected last stage on the shards to be a $sort.
return;
}
- auto sortPaths = sortPattern.getFieldNames<std::set<std::string>>();
+ auto sortPaths = sortPattern.getFieldNames<OrderedPathSet>();
auto firstMergeStage = mergePipe->getSources().cbegin();
std::function<bool(DocumentSource*)> distributedPlanLogicCallback = [](DocumentSource* stage) {
return !static_cast<bool>(stage->distributedPlanLogic());
@@ -561,7 +561,7 @@ void limitFieldsSentFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe)
}
bool stageCanRunInParallel(const boost::intrusive_ptr<DocumentSource>& stage,
- const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) {
+ const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) {
if (stage->distributedPlanLogic()) {
return stage->canRunInParallelBeforeWriteStage(nameOfShardKeyFieldsUponEntryToStage);
} else {
@@ -598,7 +598,7 @@ BSONObj buildNewKeyPattern(const ShardKeyPattern& shardKey, StringMap<std::strin
}
StringMap<std::string> computeShardKeyRenameMap(const Pipeline* mergePipeline,
- std::set<std::string>&& pathsOfShardKey) {
+ OrderedPathSet&& pathsOfShardKey) {
auto traversalStart = mergePipeline->getSources().crbegin();
auto traversalEnd = mergePipeline->getSources().crend();
const auto leadingGroup =
@@ -626,7 +626,7 @@ StringMap<std::string> computeShardKeyRenameMap(const Pipeline* mergePipeline,
*
* Purposefully takes 'shardKeyPaths' by value so that it can be modified throughout.
*/
-bool anyStageModifiesShardKeyOrNeedsMerge(std::set<std::string> shardKeyPaths,
+bool anyStageModifiesShardKeyOrNeedsMerge(OrderedPathSet shardKeyPaths,
const Pipeline* mergePipeline) {
const auto& stages = mergePipeline->getSources();
for (auto it = stages.crbegin(); it != stages.crend(); ++it) {
@@ -653,7 +653,7 @@ boost::optional<ShardedExchangePolicy> walkPipelineBackwardsTrackingShardKey(
OperationContext* opCtx, const Pipeline* mergePipeline, const ChunkManager& chunkManager) {
const ShardKeyPattern& shardKey = chunkManager.getShardKeyPattern();
- std::set<std::string> shardKeyPaths;
+ OrderedPathSet shardKeyPaths;
for (auto&& path : shardKey.getKeyPatternFields()) {
shardKeyPaths.emplace(path->dottedField().toString());
}
@@ -769,10 +769,16 @@ 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));
@@ -955,6 +961,7 @@ 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);
@@ -986,28 +993,22 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont
targetedCmd[AggregateCommandRequest::kExchangeFieldName] =
exchangeSpec ? Value(exchangeSpec->exchangeSpec.toBSON()) : Value();
- auto shardCommand = genericTransformForShards(std::move(targetedCmd),
- expCtx,
- expCtx->explain,
- expCtx->getCollatorBSON(),
- std::move(readConcern));
+ auto shardCommand = genericTransformForShards(
+ std::move(targetedCmd), expCtx, explain, expCtx->getCollatorBSON(), std::move(readConcern));
// Apply RW concern to the final shard command.
return applyReadWriteConcern(expCtx->opCtx,
- true, /* appendRC */
- !expCtx->explain, /* appendWC */
+ true, /* appendRC */
+ !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();
@@ -1051,7 +1052,7 @@ DispatchShardPipelineResults dispatchShardPipeline(
: expCtx->getCollatorBSON();
// Determine whether we can run the entire aggregation on a single shard.
- const bool mustRunOnAll = mustRunOnAllShards(expCtx->ns, hasChangeStream);
+ const bool mustRunOnAll = mustRunOnAllShards(expCtx->ns, hasChangeStream, startsWithDocuments);
std::set<ShardId> shardIds = getTargetedShards(
expCtx, mustRunOnAll, executionNsRoutingInfo, shardQuery, shardTargetingCollation);
@@ -1103,10 +1104,11 @@ DispatchShardPipelineResults dispatchShardPipeline(
*splitPipelines,
exchangeSpec,
true /* needsMerge */,
+ explain,
std::move(readConcern))
: createPassthroughCommandForShard(expCtx,
serializedCommand,
- expCtx->explain,
+ explain,
pipeline.get(),
expCtx->getCollatorBSON(),
std::move(readConcern),
@@ -1139,7 +1141,7 @@ DispatchShardPipelineResults dispatchShardPipeline(
shardIds.size() > 0);
// Explain does not produce a cursor, so instead we scatter-gather commands to the shards.
- if (expCtx->explain) {
+ if (explain) {
if (mustRunOnAll) {
// Some stages (such as $currentOp) need to be broadcast to all shards, and
// should not participate in the shard version protocol.
@@ -1154,7 +1156,7 @@ DispatchShardPipelineResults dispatchShardPipeline(
// shards, and should participate in the shard version protocol.
invariant(executionNsRoutingInfo);
shardResults =
- scatterGatherVersionedTargetByRoutingTable(opCtx,
+ scatterGatherVersionedTargetByRoutingTable(expCtx,
expCtx->ns.db(),
expCtx->ns,
*executionNsRoutingInfo,
@@ -1175,7 +1177,7 @@ DispatchShardPipelineResults dispatchShardPipeline(
targetedCommand,
ReadPreferenceSetting::get(opCtx));
- } catch (const StaleConfigException& e) {
+ } catch (const ExceptionFor<ErrorCodes::StaleConfig>& e) {
// Check to see if the command failed because of a stale shard version or something
// else.
auto staleInfo = e.extraInfo<StaleConfigInfo>();
@@ -1486,10 +1488,13 @@ 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,
- std::move(pipeline));
+ startsWithDocuments,
+ std::move(pipeline),
+ expCtx->explain);
BSONObjBuilder explainBuilder;
auto appendStatus =
appendExplainResults(std::move(shardDispatchResults), expCtx, &explainBuilder);
@@ -1523,11 +1528,13 @@ Shard::RetryPolicy getDesiredRetryPolicy(OperationContext* opCtx) {
return Shard::RetryPolicy::kIdempotent;
}
-bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream) {
+bool mustRunOnAllShards(const NamespaceString& nss,
+ bool hasChangeStream,
+ bool startsWithDocuments) {
// 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 nss.isCollectionlessAggregateNS() || hasChangeStream;
+ return !startsWithDocuments && (nss.isCollectionlessAggregateNS() || hasChangeStream);
}
std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline(
@@ -1583,11 +1590,14 @@ std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline(
[&](OperationContext* opCtx, const ChunkManager& cm) {
auto pipelineToTarget = pipeline->clone();
- if (!cm.isSharded()) {
+ if (!cm.isSharded() && expCtx->ns != NamespaceString::kConfigsvrCollectionsNamespace) {
// 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 5f30f6f6a93..12945397b59 100644
--- a/src/mongo/db/pipeline/sharded_agg_helpers.h
+++ b/src/mongo/db/pipeline/sharded_agg_helpers.h
@@ -120,11 +120,26 @@ 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);
@@ -142,6 +157,7 @@ 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);
/**
@@ -179,7 +195,7 @@ StatusWith<ChunkManager> getExecutionNsRoutingInfo(OperationContext* opCtx,
/**
* Returns true if an aggregation over 'nss' must run on all shards.
*/
-bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream);
+bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream, bool startsWithDocuments);
/**
* Retrieves the desired retry policy based on whether the default writeConcern is set on 'opCtx'.
@@ -202,6 +218,11 @@ 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/skip_and_limit.cpp b/src/mongo/db/pipeline/skip_and_limit.cpp
index e9e7e9772ce..0f9f7d3aca7 100644
--- a/src/mongo/db/pipeline/skip_and_limit.cpp
+++ b/src/mongo/db/pipeline/skip_and_limit.cpp
@@ -82,8 +82,21 @@ Pipeline::SourceContainer::iterator eraseAndStich(Pipeline::SourceContainer::ite
} // namespace
-boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::iterator itr,
- Pipeline::SourceContainer* container) {
+/**
+ * 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) {
int64_t skipSum = 0;
boost::optional<long long> minLimit;
while (itr != container->end()) {
@@ -104,7 +117,11 @@ boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::it
minLimit = std::min(static_cast<long long>(safeSum), *minLimit);
}
- itr = eraseAndStich(itr, container);
+ if (shouldModifyPipeline) {
+ itr = eraseAndStich(itr, container);
+ } else {
+ ++itr;
+ }
} else if (!nextStage->constraints().canSwapWithSkippingOrLimitingStage) {
break;
} else {
@@ -115,6 +132,16 @@ boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::it
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 2a3ecec5982..07023afa1d9 100644
--- a/src/mongo/db/pipeline/skip_and_limit.h
+++ b/src/mongo/db/pipeline/skip_and_limit.h
@@ -89,6 +89,13 @@ 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 17537069d51..475279869c0 100644
--- a/src/mongo/db/pipeline/stage_constraints.h
+++ b/src/mongo/db/pipeline/stage_constraints.h
@@ -55,11 +55,10 @@ struct StageConstraints {
enum class PositionRequirement {
kNone,
kFirst,
- // 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
+ kLast,
+ // Stages with 'kCustom' requirement must also implement the 'validatePipelinePosition()'
+ // method which is called during pipeline validation.
+ kCustom
};
/**
@@ -93,10 +92,15 @@ 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 is
- * denylisted from $changeStream.
+ * stage, whether it is allowed to exist in a $changeStream pipeline, or whether it can only
+ * exist in a change stream pipeline.
*/
- enum class ChangeStreamRequirement { kChangeStreamStage, kAllowlist, kDenylist };
+ 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.
+ };
/**
* A FacetRequirement indicates whether this stage may be used within a $facet pipeline.
@@ -249,6 +253,13 @@ 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.
*/
@@ -343,6 +354,11 @@ struct StageConstraints {
// Indicates that a stage is allowed within a pipeline-stlye update.
bool isAllowedWithinUpdatePipeline = 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;
@@ -358,6 +374,8 @@ struct StageConstraints {
isIndependentOfAnyCollection == other.isIndependentOfAnyCollection &&
canSwapWithMatch == other.canSwapWithMatch &&
canSwapWithSkippingOrLimitingStage == other.canSwapWithSkippingOrLimitingStage &&
+ canSwapWithSingleDocTransform == other.canSwapWithSingleDocTransform &&
+ canAppearOnlyOnceInPipeline == other.canAppearOnlyOnceInPipeline &&
isAllowedWithinUpdatePipeline == other.isAllowedWithinUpdatePipeline &&
unionRequirement == other.unionRequirement &&
preservesOrderAndMetadata == other.preservesOrderAndMetadata;
diff --git a/src/mongo/db/pipeline/window_function/partition_iterator.cpp b/src/mongo/db/pipeline/window_function/partition_iterator.cpp
index d8992aec28c..3b27b4ab03e 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 for EOF or the next partition.
- if (_state == IteratorState::kAwaitingAdvanceToNext ||
+ // Check whether the next document is available.
+ if (isPaused() || _state == IteratorState::kAwaitingAdvanceToNext ||
_state == IteratorState::kAwaitingAdvanceToEOF) {
return boost::none;
}
@@ -163,6 +163,7 @@ 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();
@@ -301,8 +302,13 @@ 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.
- return i;
+ // 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;
+ }
}
}
// We scanned every document in the partition, and none crossed the
@@ -467,9 +473,12 @@ void PartitionIterator::getNextDocument() {
return;
}
- if (!getNextRes.isAdvanced())
+ if (getNextRes.isPaused()) {
+ _state = IteratorState::kPauseExecution;
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
@@ -477,7 +486,7 @@ void PartitionIterator::getNextDocument() {
doc.fillCache();
if (_partitionExpr) {
- if (_state == IteratorState::kNotInitialized) {
+ if (!_partitionComparator) {
_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 128901834ee..28e0e6a6242 100644
--- a/src/mongo/db/pipeline/window_function/partition_iterator.h
+++ b/src/mongo/db/pipeline/window_function/partition_iterator.h
@@ -77,6 +77,13 @@ public:
return (*this)[0];
}
+ /**
+ * Returns true if iterator execution is paused.
+ */
+ bool isPaused() {
+ return _state == IteratorState::kPauseExecution;
+ }
+
enum class AdvanceResult {
kAdvanced,
kNewPartition,
@@ -287,6 +294,9 @@ 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 cb6b5bcf1bf..4ba0a692e47 100644
--- a/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp
+++ b/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp
@@ -499,7 +499,7 @@ 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().getApproximateSize();
+ size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize();
// 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
@@ -525,7 +525,7 @@ 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().getApproximateSize();
+ size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize();
// 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
@@ -550,7 +550,7 @@ 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().getApproximateSize();
+ size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize();
// 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
@@ -575,7 +575,7 @@ 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().getApproximateSize();
+ size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize();
// Pull in the first document, and verify the reported size. TODO SERVER-57011: The approximate
// size should not double count the nested strings.
@@ -592,7 +592,7 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForReleasedDocuments) {
const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx());
auto accessor = makeDefaultAccessor(mock, boost::none);
- size_t initialDocSize = docs[0].getDocument().getApproximateSize();
+ size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize();
// Pull in the first document, and verify the reported size of the iterator is roughly double
// the size of the document.
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 62ec29b4d9c..7599842de57 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/write_conflict_exception.h"
+#include "mongo/db/concurrency/exception_util.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_function_expression.cpp b/src/mongo/db/pipeline/window_function/window_function_expression.cpp
index 54f0552b59b..0d43689c977 100644
--- a/src/mongo/db/pipeline/window_function/window_function_expression.cpp
+++ b/src/mongo/db/pipeline/window_function/window_function_expression.cpp
@@ -37,6 +37,7 @@
#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"
@@ -152,6 +153,7 @@ intrusive_ptr<Expression> Expression::parse(BSONObj obj,
assertLanguageFeatureIsAllowed(
opCtx, exprName, allowedWithApi, AllowedWithClientType::kAny);
+ expCtx->incrementWindowAccumulatorExprCounter(exprName);
return parser(obj, sortBy, expCtx);
}
@@ -187,6 +189,7 @@ 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));
}
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 0d638ac1350..261d4212fe5 100644
--- a/src/mongo/db/pipeline/window_function/window_function_expression.h
+++ b/src/mongo/db/pipeline/window_function/window_function_expression.h
@@ -555,7 +555,7 @@ protected:
case TimeUnit::year:
case TimeUnit::quarter:
case TimeUnit::month:
- uasserted(5490704, "unit must be 'week' or smaller");
+ uasserted(5490710, "unit must be 'week' or smaller");
// Only these time units are allowed.
case TimeUnit::week:
case TimeUnit::day:
@@ -803,11 +803,11 @@ public:
}
boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final {
- MONGO_UNREACHABLE_TASSERT(5490701);
+ MONGO_UNREACHABLE_TASSERT(5490704);
}
std::unique_ptr<WindowFunctionState> buildRemovable() const final {
- MONGO_UNREACHABLE_TASSERT(5490702);
+ MONGO_UNREACHABLE_TASSERT(5490705);
}
Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final {
@@ -851,11 +851,11 @@ public:
}
boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final {
- MONGO_UNREACHABLE_TASSERT(5490701);
+ MONGO_UNREACHABLE_TASSERT(5490706);
}
std::unique_ptr<WindowFunctionState> buildRemovable() const final {
- MONGO_UNREACHABLE_TASSERT(5490702);
+ MONGO_UNREACHABLE_TASSERT(5490707);
}
};
@@ -873,11 +873,11 @@ public:
}
boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final {
- MONGO_UNREACHABLE_TASSERT(5490701);
+ MONGO_UNREACHABLE_TASSERT(5490708);
}
std::unique_ptr<WindowFunctionState> buildRemovable() const final {
- MONGO_UNREACHABLE_TASSERT(5490702);
+ MONGO_UNREACHABLE_TASSERT(5490709);
}
};
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 fbf77c59fa2..64d3319efb9 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));
- _memUsageBytes = sizeof(*this);
+ updateMemUsage();
}
void add(Value value) final {
_acc.process(value, false);
- _memUsageBytes = _acc.getMemUsage();
+ updateMemUsage();
}
void remove(Value value) final {
_acc.remove(value);
- _memUsageBytes = _acc.getMemUsage();
+ updateMemUsage();
}
Value getValue() const final {
@@ -75,10 +75,14 @@ public:
void reset() final {
_acc.reset();
- _memUsageBytes = _acc.getMemUsage();
+ updateMemUsage();
}
private:
+ void updateMemUsage() {
+ _memUsageBytes = sizeof(*this) + _acc.getMemUsage();
+ }
+
AccumulatorTopBottomN<sense, single> _acc;
};