SERVER-120937 Decouple non-const bindViewInfo() from const ViewPolicy callback in extensions (#49112)
GitOrigin-RevId: 5a9ec32b023a93fba6157beaaeb554231161f1d9
This commit is contained in:
committed by
MongoDB Bot
parent
6f971ec981
commit
01b3f71df0
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* This test verifies that the $addViewName extension stage correctly passes the view name
|
||||
* through the extension boundary (AstNode -> LogicalStage -> ExecAggStage) and adds
|
||||
* it as a field in output documents. It also verifies that the view pipeline is correctly appended when the first stage's ViewPolicy is kDefaultPrepend, and not prepended if it's kDoNothing.
|
||||
* This test verifies that the $addViewName extension stage correctly passes the view name through
|
||||
* the extension boundary (AstNode -> LogicalStage -> ExecAggStage) and adds it as a field in output
|
||||
* documents. It also verifies that the view pipeline is correctly appended when the first stage's
|
||||
* FirstStageViewApplicationPolicy is kDefaultPrepend, and not prepended if it's kDoNothing.
|
||||
*
|
||||
* This test also verifies that the $disallowViews extension stage correctly uasserts when
|
||||
* used in a view context.
|
||||
@@ -98,7 +99,7 @@ function testNestedViewsWithViewNameStage(suffix, userPipeStage) {
|
||||
const outerViewNss = `${db.getName()}.${outerViewName}`;
|
||||
|
||||
const userPipe = [userPipeStage];
|
||||
const shouldPrepend = false; // ViewPolicy is kDoNothing
|
||||
const shouldPrepend = false; // FirstStageViewApplicationPolicy is kDoNothing
|
||||
|
||||
const outerView = {view: db[outerViewName], viewName: outerViewName, viewNss: outerViewNss};
|
||||
// Verify outer view name is present when running on outerView
|
||||
@@ -213,7 +214,7 @@ describe("View policy extension stages", function () {
|
||||
const viewPipe = [{$addFields: {a: 1}}];
|
||||
const userPipe = [{$addViewName: {}}];
|
||||
const viewName = "test_view";
|
||||
const shouldPrepend = false; // ViewPolicy is kDoNothing
|
||||
const shouldPrepend = false; // FirstStageViewApplicationPolicy is kDoNothing
|
||||
testViewNameWithTemporaryView(viewName, viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
@@ -221,7 +222,7 @@ describe("View policy extension stages", function () {
|
||||
const viewPipe = [{$testFoo: {}}];
|
||||
const userPipe = [{$addViewName: {}}];
|
||||
const viewName = "foo_view";
|
||||
const shouldPrepend = false; // ViewPolicy is kDoNothing
|
||||
const shouldPrepend = false; // FirstStageViewApplicationPolicy is kDoNothing
|
||||
testViewNameWithTemporaryView(viewName, viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
@@ -229,7 +230,7 @@ describe("View policy extension stages", function () {
|
||||
const viewPipe = [{$addFields: {a: 1}}];
|
||||
const userPipe = [{$testFoo: {}}, {$addViewName: {}}];
|
||||
const viewName = "test_view_later";
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameWithTemporaryView(viewName, viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
@@ -237,7 +238,7 @@ describe("View policy extension stages", function () {
|
||||
const viewPipe = [{$addFields: {a: 1}}];
|
||||
const userPipe = [{$testFoo: {}}, {$desugarAddViewName: {}}];
|
||||
const viewName = "test_view_desugar";
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameWithTemporaryView(viewName, viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
@@ -277,14 +278,14 @@ describe("View policy extension stages", function () {
|
||||
it("should not add view name when $addViewName is in view definition", function () {
|
||||
const viewPipe = [{$addViewName: {}}];
|
||||
const userPipe = [{$testFoo: {}}];
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameAbsentInView("add_viewname_in_view", viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
it("should not add view name when view has $desugarAddViewName in definition and desugaring should work in view def", function () {
|
||||
const viewPipe = [{$desugarAddViewName: {}}];
|
||||
const userPipe = [{$addFields: {a: 1}}];
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameAbsentInView("desugar_add_viewname_in_view", viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
@@ -292,21 +293,21 @@ describe("View policy extension stages", function () {
|
||||
it("should not add view name when $addViewName is at first position in view definition", function () {
|
||||
const viewPipe = [{$addViewName: {}}, {$addFields: {a: 1}}];
|
||||
const userPipe = [{$addFields: {a: 1}}];
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameAbsentInView("add_viewname_first", viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
it("should not add view name when $addViewName is at middle position in view definition", function () {
|
||||
const viewPipe = [{$addFields: {a: 1}}, {$addViewName: {}}, {$addFields: {a: 1}}];
|
||||
const userPipe = [{$addFields: {a: 1}}];
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameAbsentInView("add_viewname_middle", viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
it("should not add view name when $addViewName is at last position in view definition", function () {
|
||||
const viewPipe = [{$addFields: {a: 1}}, {$addViewName: {}}];
|
||||
const userPipe = [{$addFields: {a: 1}}];
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameAbsentInView("add_viewname_last", viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
});
|
||||
@@ -314,7 +315,7 @@ describe("View policy extension stages", function () {
|
||||
it("should not add view name when view has multiple extension stages in definition", function () {
|
||||
const viewPipe = [{$desugarAddViewName: {}}, {$testFoo: {}}, {$addViewName: {}}];
|
||||
const userPipe = [{$addFields: {a: 1}}];
|
||||
const shouldPrepend = true; // ViewPolicy is kDefaultPrepend
|
||||
const shouldPrepend = true; // FirstStageViewApplicationPolicy is kDefaultPrepend
|
||||
testViewNameAbsentInView("multi_extension_view", viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
});
|
||||
@@ -440,7 +441,7 @@ describe("View policy extension stages", function () {
|
||||
const viewPipe = [{$addViewName: {}}, {$addFields: {a: 1}}];
|
||||
const userPipe = [{$addViewName: {}}];
|
||||
const viewName = "view_with_add_viewname";
|
||||
const shouldPrepend = false; // ViewPolicy is kDoNothing
|
||||
const shouldPrepend = false; // FirstStageViewApplicationPolicy is kDoNothing
|
||||
testViewNameWithTemporaryView(viewName, viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
|
||||
@@ -448,7 +449,7 @@ describe("View policy extension stages", function () {
|
||||
const viewPipe = [{$desugarAddViewName: {}}, {$addFields: {a: 1}}];
|
||||
const userPipe = [{$desugarAddViewName: {}}];
|
||||
const viewName = "view_with_desugar";
|
||||
const shouldPrepend = false; // ViewPolicy is kDoNothing
|
||||
const shouldPrepend = false; // FirstStageViewApplicationPolicy is kDoNothing
|
||||
testViewNameWithTemporaryView(viewName, viewPipe, userPipe, shouldPrepend);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -985,7 +985,7 @@ SecondParseRequirement maybeApplyViewPipeline(const AggExState& aggExState,
|
||||
// For search queries on views don't do any of the pipeline stitching that is done for
|
||||
// normal views.
|
||||
// TODO SERVER-115069 Remove this once search queries are desugared at LiteParsed time and
|
||||
// handle the view through a custom ViewPolicy.
|
||||
// handle the view through a bindViewInfo() override.
|
||||
if (search_helpers::isMongotLiteParsedPipeline(*desugaredLPP)) {
|
||||
LOGV2_DEBUG(11856001, 4, "Skipping view application because this is a mongot query");
|
||||
return currentRequirement;
|
||||
|
||||
@@ -204,7 +204,7 @@ private:
|
||||
}
|
||||
|
||||
static ::MongoExtensionStatus* _hostBindViewInfo(
|
||||
const ::MongoExtensionAggStageAstNode* astNode,
|
||||
::MongoExtensionAggStageAstNode* astNode,
|
||||
const ::MongoExtensionViewInfo* viewInfo) noexcept {
|
||||
return wrapCXXAndConvertExceptionToStatus([&]() {
|
||||
tasserted(11507501,
|
||||
|
||||
@@ -196,18 +196,24 @@ MONGO_INITIALIZER_WITH_PREREQUISITES(RegisterStageExpanderForLiteParsedExtension
|
||||
DocumentSourceExtensionOptimizable::LiteParsedExpandable::stageExpander);
|
||||
}
|
||||
|
||||
// TODO SERVER-116021 Remove this check when the extension can do this through ViewPolicy.
|
||||
// TODO SERVER-116021 Remove this check when the extension can do this through bindViewInfo().
|
||||
bool DocumentSourceExtensionOptimizable::LiteParsedExpandable::hasExtensionVectorSearchStage()
|
||||
const {
|
||||
return search_helpers::isExtensionVectorSearchStage(getParseTimeName());
|
||||
}
|
||||
|
||||
// TODO SERVER-116021 Remove this check when the extension can do this through ViewPolicy.
|
||||
// TODO SERVER-116021 Remove this check when the extension can do this through bindViewInfo().
|
||||
bool DocumentSourceExtensionOptimizable::LiteParsedExpanded::hasExtensionVectorSearchStage() const {
|
||||
return search_helpers::isExtensionVectorSearchStage(getParseTimeName());
|
||||
}
|
||||
|
||||
ViewPolicy DocumentSourceExtensionOptimizable::LiteParsedExpanded::getViewPolicy() const {
|
||||
FirstStageViewApplicationPolicy
|
||||
DocumentSourceExtensionOptimizable::LiteParsedExpanded::getFirstStageViewApplicationPolicy() const {
|
||||
return view_util::toFirstStageApplicationPolicy(_astNode->getFirstStageViewApplicationPolicy());
|
||||
}
|
||||
|
||||
void DocumentSourceExtensionOptimizable::LiteParsedExpanded::bindViewInfo(
|
||||
const ViewInfo& viewInfo, const ResolvedNamespaceMap& resolvedNamespaces) {
|
||||
if (!feature_flags::gFeatureFlagExtensionViewsAndUnionWith.isEnabled()) {
|
||||
// If this is not a $vectorSearch stage, views are banned entirely with the feature flag
|
||||
// disabled.
|
||||
@@ -223,15 +229,8 @@ ViewPolicy DocumentSourceExtensionOptimizable::LiteParsedExpanded::getViewPolicy
|
||||
"$vectorSearch-as-an-extension is not allowed against views."));
|
||||
}
|
||||
|
||||
return ViewPolicy{.policy = view_util::toFirstStageApplicationPolicy(
|
||||
_astNode->getFirstStageViewApplicationPolicy()),
|
||||
.callback = [this](const ViewInfo& viewInfo,
|
||||
StringData stageName,
|
||||
const ResolvedNamespaceMap&) {
|
||||
host_connector::ViewInfoAdapter viewInfoAdapter =
|
||||
host_connector::ViewInfoAdapter::fromViewInfo(viewInfo);
|
||||
_astNode->bindViewInfo(viewInfoAdapter.getAsBoundaryType());
|
||||
}};
|
||||
auto viewInfoAdapter = host_connector::ViewInfoAdapter::fromViewInfo(viewInfo);
|
||||
_astNode->bindViewInfo(viewInfoAdapter.getAsBoundaryType());
|
||||
}
|
||||
|
||||
// static
|
||||
|
||||
@@ -191,7 +191,7 @@ public:
|
||||
}
|
||||
|
||||
// TODO SERVER-116021 Remove this override when extensions can handle views through
|
||||
// ViewPolicy.
|
||||
// bindViewInfo().
|
||||
bool hasExtensionVectorSearchStage() const override;
|
||||
|
||||
ReadConcernSupportResult supportsReadConcern(repl::ReadConcernLevel level,
|
||||
@@ -317,7 +317,10 @@ public:
|
||||
|
||||
bool hasExtensionVectorSearchStage() const override;
|
||||
|
||||
ViewPolicy getViewPolicy() const override;
|
||||
FirstStageViewApplicationPolicy getFirstStageViewApplicationPolicy() const override;
|
||||
|
||||
void bindViewInfo(const ViewInfo& viewInfo,
|
||||
const ResolvedNamespaceMap& resolvedNamespaces) override;
|
||||
|
||||
ReadConcernSupportResult supportsReadConcern(repl::ReadConcernLevel level,
|
||||
bool isImplicitDefault) const override {
|
||||
@@ -330,7 +333,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
const AggStageAstNodeHandle _astNode;
|
||||
AggStageAstNodeHandle _astNode;
|
||||
const MongoExtensionStaticProperties _properties;
|
||||
const NamespaceString _nss;
|
||||
};
|
||||
|
||||
@@ -2433,7 +2433,7 @@ public:
|
||||
return _viewPolicy;
|
||||
}
|
||||
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) const override {
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) override {
|
||||
_boundViewName = std::string(viewInfo.viewName());
|
||||
}
|
||||
|
||||
@@ -2448,12 +2448,12 @@ public:
|
||||
|
||||
private:
|
||||
MongoExtensionFirstStageViewApplicationPolicy _viewPolicy;
|
||||
mutable std::string _boundViewName;
|
||||
std::string _boundViewName;
|
||||
};
|
||||
|
||||
void testViewPolicyHelper(const NamespaceString& nss,
|
||||
MongoExtensionFirstStageViewApplicationPolicy extensionPolicy,
|
||||
ViewPolicy::kFirstStageApplicationPolicy expectedPolicy,
|
||||
FirstStageViewApplicationPolicy expectedPolicy,
|
||||
const std::string& expectedViewName) {
|
||||
auto astNodeImpl = ConfigurableViewPolicyTestAstNode::make(extensionPolicy);
|
||||
auto* astNodeImplPtr = static_cast<ConfigurableViewPolicyTestAstNode*>(astNodeImpl.get());
|
||||
@@ -2463,16 +2463,13 @@ void testViewPolicyHelper(const NamespaceString& nss,
|
||||
host::DocumentSourceExtensionOptimizable::LiteParsedExpanded liteParsed(
|
||||
ConfigurableViewPolicyTestAstNode::kStageName, std::move(handle), nss);
|
||||
|
||||
auto viewPolicy = liteParsed.getViewPolicy();
|
||||
ASSERT_EQ(viewPolicy.policy, expectedPolicy);
|
||||
|
||||
const auto viewNss = NamespaceString::createNamespaceString_forTest("test.view"_sd);
|
||||
const auto resolvedNss = NamespaceString::createNamespaceString_forTest("test.collection"_sd);
|
||||
std::vector<BSONObj> viewPipeline = {BSON("$match" << BSON("x" << 1))};
|
||||
ViewInfo viewInfo(viewNss, resolvedNss, std::move(viewPipeline));
|
||||
|
||||
viewPolicy.callback(
|
||||
viewInfo, ConfigurableViewPolicyTestAstNode::kStageName, ResolvedNamespaceMap{});
|
||||
ASSERT_EQ(liteParsed.getFirstStageViewApplicationPolicy(), expectedPolicy);
|
||||
liteParsed.bindViewInfo(viewInfo, {});
|
||||
ASSERT_EQ(astNodeImplPtr->getBoundViewName(), expectedViewName);
|
||||
}
|
||||
|
||||
@@ -2493,7 +2490,7 @@ public:
|
||||
return std::make_unique<ViewPipelineValidatorTestAstNode>();
|
||||
}
|
||||
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) const override {
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) override {
|
||||
for (const auto& stage : viewInfo.viewPipeline()) {
|
||||
if (stage.isEmpty()) {
|
||||
continue;
|
||||
@@ -2518,14 +2515,12 @@ void runViewPipelineValidatorCallback(const std::vector<BSONObj>& viewPipeline)
|
||||
host::DocumentSourceExtensionOptimizable::LiteParsedExpanded liteParsed(
|
||||
ViewPipelineValidatorTestAstNode::kStageName, std::move(handle), nss);
|
||||
|
||||
auto viewPolicy = liteParsed.getViewPolicy();
|
||||
const auto viewNss = NamespaceString::createNamespaceString_forTest("test.view"_sd);
|
||||
const auto resolvedNss = NamespaceString::createNamespaceString_forTest("test.coll"_sd);
|
||||
std::vector<BSONObj> pipelineCopy = viewPipeline;
|
||||
ViewInfo viewInfo(viewNss, resolvedNss, std::move(pipelineCopy));
|
||||
|
||||
viewPolicy.callback(
|
||||
viewInfo, ViewPipelineValidatorTestAstNode::kStageName, ResolvedNamespaceMap{});
|
||||
liteParsed.bindViewInfo(viewInfo, {});
|
||||
}
|
||||
|
||||
TEST_F(DocumentSourceExtensionOptimizableTest,
|
||||
@@ -2535,7 +2530,7 @@ TEST_F(DocumentSourceExtensionOptimizableTest,
|
||||
const auto viewNss = NamespaceString::createNamespaceString_forTest("test.view"_sd);
|
||||
testViewPolicyHelper(_nss,
|
||||
MongoExtensionFirstStageViewApplicationPolicy::kDefaultPrepend,
|
||||
ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend,
|
||||
FirstStageViewApplicationPolicy::kDefaultPrepend,
|
||||
viewNss.coll().data());
|
||||
}
|
||||
|
||||
@@ -2546,7 +2541,7 @@ TEST_F(DocumentSourceExtensionOptimizableTest,
|
||||
const auto viewNss = NamespaceString::createNamespaceString_forTest("test.view"_sd);
|
||||
testViewPolicyHelper(_nss,
|
||||
MongoExtensionFirstStageViewApplicationPolicy::kDoNothing,
|
||||
ViewPolicy::kFirstStageApplicationPolicy::kDoNothing,
|
||||
FirstStageViewApplicationPolicy::kDoNothing,
|
||||
viewNss.coll().data());
|
||||
}
|
||||
|
||||
|
||||
@@ -103,13 +103,13 @@ inline boost::optional<StageConstraints::HostTypeRequirement> toHostTypeRequirem
|
||||
} // namespace static_properties_util
|
||||
|
||||
namespace view_util {
|
||||
inline ViewPolicy::kFirstStageApplicationPolicy toFirstStageApplicationPolicy(
|
||||
inline FirstStageViewApplicationPolicy toFirstStageApplicationPolicy(
|
||||
MongoExtensionFirstStageViewApplicationPolicy policy) {
|
||||
switch (policy) {
|
||||
case MongoExtensionFirstStageViewApplicationPolicy::kDefaultPrepend:
|
||||
return ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend;
|
||||
return FirstStageViewApplicationPolicy::kDefaultPrepend;
|
||||
case MongoExtensionFirstStageViewApplicationPolicy::kDoNothing:
|
||||
return ViewPolicy::kFirstStageApplicationPolicy::kDoNothing;
|
||||
return FirstStageViewApplicationPolicy::kDoNothing;
|
||||
}
|
||||
MONGO_UNREACHABLE_TASSERT(11507600);
|
||||
}
|
||||
|
||||
@@ -696,7 +696,7 @@ typedef struct MongoExtensionAggStageAstNodeVTable {
|
||||
* Ownership of the BSON members is not transferred over the API boundary, so the extension must
|
||||
* copy if they need to persist beyond the scope of bind_view_info().
|
||||
*/
|
||||
MongoExtensionStatus* (*bind_view_info)(const MongoExtensionAggStageAstNode* astNode,
|
||||
MongoExtensionStatus* (*bind_view_info)(MongoExtensionAggStageAstNode* astNode,
|
||||
const MongoExtensionViewInfo* viewInfo);
|
||||
} MongoExtensionAggStageAstNodeVTable;
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ public:
|
||||
// Note that viewInfo is non-owning, meaning if an extension wants to access the metadata
|
||||
// outside of the call to bindViewInfo, it must make its own copy of it. There are no guarantees
|
||||
// on the lifetime outside of the scope of this function.
|
||||
virtual void bindViewInfo(const ViewInfo& viewInfo) const {
|
||||
virtual void bindViewInfo(const ViewInfo& viewInfo) {
|
||||
// Default implementation is a no-op.
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ private:
|
||||
}
|
||||
|
||||
static ::MongoExtensionStatus* _extBindViewInfo(
|
||||
const ::MongoExtensionAggStageAstNode* astNode,
|
||||
::MongoExtensionAggStageAstNode* astNode,
|
||||
const ::MongoExtensionViewInfo* viewInfo) noexcept {
|
||||
return wrapCXXAndConvertExceptionToStatus([&]() {
|
||||
sdk_tassert(11905600, "Provided view info was invalid", viewInfo != nullptr);
|
||||
@@ -399,7 +399,7 @@ private:
|
||||
byteViewAsStringView(viewInfo->viewNamespace.collectionName),
|
||||
std::move(stages));
|
||||
|
||||
static_cast<const ExtensionAggStageAstNodeAdapter*>(astNode)->getImpl().bindViewInfo(
|
||||
static_cast<ExtensionAggStageAstNodeAdapter*>(astNode)->getImpl().bindViewInfo(
|
||||
viewInfoWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1714,7 +1714,7 @@ public:
|
||||
MONGO_UNIMPLEMENTED;
|
||||
}
|
||||
|
||||
void bindViewInfo(const ViewInfo& viewInfo) const override {
|
||||
void bindViewInfo(const ViewInfo& viewInfo) override {
|
||||
_boundDbName = std::string(viewInfo.dbName());
|
||||
_boundViewName = std::string(viewInfo.viewName());
|
||||
_boundPipeline = viewInfo.viewPipeline();
|
||||
@@ -1737,9 +1737,9 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::string _boundDbName;
|
||||
mutable std::string _boundViewName;
|
||||
mutable std::vector<BSONObj> _boundPipeline;
|
||||
std::string _boundDbName;
|
||||
std::string _boundViewName;
|
||||
std::vector<BSONObj> _boundPipeline;
|
||||
};
|
||||
|
||||
TEST_F(AggStageTest, ExtensionAstNodeCanReturnDefaultViewPolicy) {
|
||||
|
||||
@@ -77,7 +77,7 @@ AggStageAstNodeAPI::getFirstStageViewApplicationPolicy() const {
|
||||
return policy;
|
||||
}
|
||||
|
||||
void AggStageAstNodeAPI::bindViewInfo(const ::MongoExtensionViewInfo& viewInfo) const {
|
||||
void AggStageAstNodeAPI::bindViewInfo(const ::MongoExtensionViewInfo& viewInfo) {
|
||||
invokeCAndConvertStatusToException(
|
||||
[&]() { return _vtable().bind_view_info(get(), &viewInfo); });
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public:
|
||||
/**
|
||||
* Binds view information to this AST node.
|
||||
*/
|
||||
void bindViewInfo(const ::MongoExtensionViewInfo& viewInfo) const;
|
||||
void bindViewInfo(const ::MongoExtensionViewInfo& viewInfo);
|
||||
|
||||
static void assertVTableConstraints(const VTable_t& vtable) {
|
||||
tassert(11217601, "AggStageAstNode 'get_name' is null", vtable.get_name != nullptr);
|
||||
|
||||
@@ -164,7 +164,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) const override {
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) override {
|
||||
_viewName = std::string(viewInfo.viewName());
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public:
|
||||
|
||||
private:
|
||||
const mongo::BSONObj _arguments;
|
||||
mutable std::string _viewName;
|
||||
std::string _viewName;
|
||||
};
|
||||
|
||||
DEFAULT_PARSE_NODE(AddViewName)
|
||||
|
||||
@@ -47,7 +47,7 @@ public:
|
||||
DisallowViewAstNode(std::string_view stageName, const mongo::BSONObj& arguments)
|
||||
: sdk::AggStageAstNode(stageName), _arguments(arguments.getOwned()) {}
|
||||
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) const override {
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) override {
|
||||
std::string message = str::stream() << "Stage " << std::string(getName())
|
||||
<< " does not support views. Attempted to use in view: "
|
||||
<< std::string(viewInfo.viewName());
|
||||
|
||||
@@ -147,7 +147,7 @@ public:
|
||||
_arguments(arguments.getOwned()),
|
||||
_storedViewPipeline(std::move(storedViewPipeline)) {}
|
||||
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) const override {
|
||||
void bindViewInfo(const sdk::ViewInfo& viewInfo) override {
|
||||
_storedViewPipeline.clear();
|
||||
for (const auto& stage : viewInfo.viewPipeline()) {
|
||||
if (stage.isEmpty()) {
|
||||
@@ -187,7 +187,7 @@ private:
|
||||
}
|
||||
|
||||
const BSONObj _arguments;
|
||||
mutable std::vector<BSONObj> _storedViewPipeline;
|
||||
std::vector<BSONObj> _storedViewPipeline;
|
||||
};
|
||||
|
||||
DEFAULT_PARSE_NODE(ValidateViewPipeline)
|
||||
|
||||
@@ -283,15 +283,4 @@ ViewInfo ViewInfo::clone() const {
|
||||
return out;
|
||||
}
|
||||
|
||||
DisallowViewsPolicy::DisallowViewsPolicy()
|
||||
: ViewPolicy(kFirstStageApplicationPolicy::kDoNothing,
|
||||
[](const ViewInfo&, StringData stageName, const ResolvedNamespaceMap&) {
|
||||
uasserted(
|
||||
ErrorCodes::CommandNotSupportedOnView,
|
||||
std::string(str::stream() << stageName << " is not supported on views."));
|
||||
}) {}
|
||||
|
||||
DisallowViewsPolicy::DisallowViewsPolicy(ViewPolicyCallbackFn&& fn)
|
||||
: ViewPolicy(kFirstStageApplicationPolicy::kDoNothing, std::move(fn)) {}
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
@@ -167,61 +167,17 @@ public:
|
||||
std::unique_ptr<LiteParsedPipeline> viewPipeline;
|
||||
};
|
||||
|
||||
using ViewPolicyCallbackFn =
|
||||
std::function<void(const ViewInfo&, StringData, const ResolvedNamespaceMap&)>;
|
||||
|
||||
/**
|
||||
* Indicates how this stage will interact with a view. LiteParsedDocumentSources that
|
||||
* wish to perform custom behavior for views should override the callback function.
|
||||
* Describes what the pipeline as a whole should do with a view on the main aggregate
|
||||
* collection, if this stage is at the front of the pipeline.
|
||||
*/
|
||||
struct ViewPolicy {
|
||||
// Describes what the pipeline as a whole should do with a view on the main aggregate
|
||||
// collection, if this stage is at the front of the pipeline.
|
||||
enum class kFirstStageApplicationPolicy {
|
||||
// If this stage is at the front of the pipeline, the pipeline should
|
||||
// prepend the view.
|
||||
kDefaultPrepend,
|
||||
// If this stage is at the front of the pipeline, the pipeline should not
|
||||
// prepend the view. The stage will apply the view pipeline itself internally.
|
||||
kDoNothing,
|
||||
} policy = kFirstStageApplicationPolicy::kDefaultPrepend;
|
||||
|
||||
// Offers a stage the chance to receive/bind to a view definition on the command's resolved view
|
||||
// definitions. Receives resolved view information, the stage name, and the resolved namespaces
|
||||
// map.
|
||||
ViewPolicyCallbackFn callback = [](const ViewInfo&, StringData, const ResolvedNamespaceMap&) {
|
||||
// Default callback is a no-op.
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Default view policy for aggregation stages. This policy allows views to be used with the stage
|
||||
* by prepending the view pipeline when the stage is at the front of the pipeline.
|
||||
*
|
||||
* When a stage uses DefaultViewPolicy:
|
||||
* - If the stage is at the front of the pipeline and the main collection is a view, the view
|
||||
* pipeline will be prepended to the aggregation pipeline.
|
||||
* - The callback is a no-op, meaning the stage does not need to perform any special handling
|
||||
* when a view is encountered.
|
||||
*
|
||||
* This is the default behavior for most aggregation stages that support views.
|
||||
*/
|
||||
struct DefaultViewPolicy : ViewPolicy {};
|
||||
|
||||
/**
|
||||
* View policy that disallows views for aggregation stages. This policy prevents views from being
|
||||
* used with the stage by throwing an error when a view is encountered.
|
||||
*
|
||||
* When a stage uses DisallowViewsPolicy:
|
||||
* - The policy is set to kDoNothing, meaning the view pipeline will not be automatically prepended.
|
||||
* - The callback throws a CommandNotSupportedOnView error (or a custom error if a custom callback
|
||||
* is provided) when a view is detected.
|
||||
*
|
||||
* Use this policy for stages that cannot operate on views.
|
||||
*/
|
||||
struct DisallowViewsPolicy : public ViewPolicy {
|
||||
DisallowViewsPolicy();
|
||||
DisallowViewsPolicy(ViewPolicyCallbackFn&&);
|
||||
enum class FirstStageViewApplicationPolicy {
|
||||
// If this stage is at the front of the pipeline, the pipeline should
|
||||
// prepend the view.
|
||||
kDefaultPrepend,
|
||||
// If this stage is at the front of the pipeline, the pipeline should not
|
||||
// prepend the view. The stage will apply the view pipeline itself internally.
|
||||
kDoNothing,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -443,11 +399,16 @@ public:
|
||||
|
||||
virtual std::unique_ptr<StageParams> getStageParams() const = 0;
|
||||
|
||||
virtual FirstStageViewApplicationPolicy getFirstStageViewApplicationPolicy() const {
|
||||
return FirstStageViewApplicationPolicy::kDefaultPrepend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ViewPolicy for this stage.
|
||||
* Bind view information to this stage. Called by handleView() when running against a view.
|
||||
*/
|
||||
virtual ViewPolicy getViewPolicy() const {
|
||||
return DefaultViewPolicy{};
|
||||
virtual void bindViewInfo(const ViewInfo& viewInfo,
|
||||
const ResolvedNamespaceMap& resolvedNamespaces) {
|
||||
// Default implementation is a no-op.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,7 +478,8 @@ public:
|
||||
|
||||
/**
|
||||
* Returns true if this is a vector search stage ($vectorSearch) or has a nested $vectorSearch.
|
||||
* TODO SERVER-116021 Remove this override when extensions can handle views through ViewPolicy.
|
||||
* TODO SERVER-116021 Remove this override when extensions can handle views through
|
||||
* bindViewInfo().
|
||||
*/
|
||||
virtual bool hasExtensionVectorSearchStage() const {
|
||||
return false;
|
||||
|
||||
@@ -336,66 +336,19 @@ TEST(StageParams, CanGetStageParamsFromLiteParsed) {
|
||||
ASSERT_NE(baseParams->getId(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewPolicy, CanSpecifyDefaultViewPolicyWithNoCustomReadFunction) {
|
||||
TEST(ViewPolicy, DefaultPolicyReturnsDefaultPrepend) {
|
||||
BSONObj spec = BSON("$testStage" << BSONObj());
|
||||
auto lp = TestLiteParsed(spec.firstElement());
|
||||
|
||||
ASSERT_EQ(lp.getViewPolicy().policy, ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend);
|
||||
ASSERT_EQ(lp.getFirstStageViewApplicationPolicy(),
|
||||
FirstStageViewApplicationPolicy::kDefaultPrepend);
|
||||
}
|
||||
|
||||
TEST(ViewPolicy, CanSpecifyViewPolicyWithCustomReadFunction) {
|
||||
// Make a custom view function that just sets a bool flag indicating that the function has been
|
||||
// called successfully.
|
||||
bool fired = false;
|
||||
auto viewFunc = [&fired](const ViewInfo&, StringData stageName, const ResolvedNamespaceMap&) {
|
||||
ASSERT_FALSE(fired);
|
||||
fired = true;
|
||||
};
|
||||
|
||||
TEST(ViewPolicy, CanSpecifyDoNothingPolicy) {
|
||||
BSONObj spec = BSON("$testStage" << BSONObj());
|
||||
auto lp =
|
||||
TestLiteParsed(spec.firstElement(),
|
||||
ViewPolicy{.policy = ViewPolicy::kFirstStageApplicationPolicy::kDoNothing,
|
||||
.callback = viewFunc});
|
||||
auto lp = TestLiteParsed(spec.firstElement(), FirstStageViewApplicationPolicy::kDoNothing);
|
||||
|
||||
auto viewPolicy = lp.getViewPolicy();
|
||||
ASSERT_EQ(viewPolicy.policy, ViewPolicy::kFirstStageApplicationPolicy::kDoNothing);
|
||||
|
||||
// Make sure that we can actually call the custom view function.
|
||||
viewPolicy.callback({}, "", ResolvedNamespaceMap{});
|
||||
ASSERT_TRUE(fired);
|
||||
}
|
||||
|
||||
TEST(ViewPolicy, CanSpecifyDisallowViewPolicyDefaultValues) {
|
||||
BSONObj spec = BSON("$testStage" << BSONObj());
|
||||
auto lp = TestLiteParsed(spec.firstElement(), DisallowViewsPolicy{});
|
||||
|
||||
auto viewPolicy = lp.getViewPolicy();
|
||||
ASSERT_EQ(viewPolicy.policy, ViewPolicy::kFirstStageApplicationPolicy::kDoNothing);
|
||||
|
||||
ASSERT_THROWS_CODE_AND_WHAT(viewPolicy.callback({}, "$test", ResolvedNamespaceMap{}),
|
||||
DBException,
|
||||
ErrorCodes::CommandNotSupportedOnView,
|
||||
"$test is not supported on views.");
|
||||
}
|
||||
|
||||
TEST(ViewPolicy, CanSpecifyDisallowViewPolicyCustomValues) {
|
||||
BSONObj spec = BSON("$testStage" << BSONObj());
|
||||
|
||||
const int kCustomErrorCode = 11505700;
|
||||
const std::string kCustomErrorMsg = "test error message";
|
||||
auto lp = TestLiteParsed(spec.firstElement(),
|
||||
DisallowViewsPolicy{[&](const auto&, auto, const auto&) {
|
||||
uasserted(kCustomErrorCode, kCustomErrorMsg);
|
||||
}});
|
||||
|
||||
auto viewPolicy = lp.getViewPolicy();
|
||||
ASSERT_EQ(viewPolicy.policy, ViewPolicy::kFirstStageApplicationPolicy::kDoNothing);
|
||||
|
||||
ASSERT_THROWS_CODE_AND_WHAT(viewPolicy.callback({}, "$test", ResolvedNamespaceMap{}),
|
||||
DBException,
|
||||
kCustomErrorCode,
|
||||
kCustomErrorMsg);
|
||||
ASSERT_EQ(lp.getFirstStageViewApplicationPolicy(), FirstStageViewApplicationPolicy::kDoNothing);
|
||||
}
|
||||
|
||||
TEST(LiteParsedPipelineClone, CloneCreatesDeepCopy) {
|
||||
|
||||
@@ -242,20 +242,14 @@ void LiteParsedPipeline::_stitchFront(LiteParsedPipeline&& prefix) {
|
||||
|
||||
void LiteParsedPipeline::handleView(const ViewInfo& viewInfo,
|
||||
const ResolvedNamespaceMap& resolvedNamespaces) {
|
||||
for (const auto& stage : _stageSpecs) {
|
||||
// Let each stage bind to the view info.
|
||||
auto thisPolicy = stage->getViewPolicy();
|
||||
thisPolicy.callback(viewInfo, stage->getParseTimeName(), resolvedNamespaces);
|
||||
for (auto& stage : _stageSpecs) {
|
||||
stage->bindViewInfo(viewInfo, resolvedNamespaces);
|
||||
}
|
||||
|
||||
// Determine whether the pipeline should automatically prepend the view pipeline. This decision
|
||||
// is made solely by the first stage, if any. Other stages' first-stage policies are not
|
||||
// consulted here.
|
||||
const auto firstStagePolicy = _stageSpecs.empty()
|
||||
? ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend
|
||||
: _stageSpecs.front()->getViewPolicy().policy;
|
||||
|
||||
if (firstStagePolicy == ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend) {
|
||||
? FirstStageViewApplicationPolicy::kDefaultPrepend
|
||||
: _stageSpecs.front()->getFirstStageViewApplicationPolicy();
|
||||
if (firstStagePolicy == FirstStageViewApplicationPolicy::kDefaultPrepend) {
|
||||
// If the first stage doesn't explicitly disallow it, clone and prepend the desugared view
|
||||
// pipeline to the current pipeline.
|
||||
auto clonedViewPipe = viewInfo.getViewPipeline();
|
||||
|
||||
@@ -398,15 +398,16 @@ public:
|
||||
/**
|
||||
* Applies view semantics to this pipeline.
|
||||
*
|
||||
* Each stage is given a chance to validate the view or modify itself with the view via its
|
||||
* ViewPolicy callback. Whether the view pipeline is automatically prepended is determined
|
||||
* solely by the first stage: if its policy is kDefaultPrepend (or the pipeline is empty), the
|
||||
* desugared view pipeline is cloned and prepended; otherwise it is not.
|
||||
* Each stage is given a chance to validate the view or modify itself via its bindViewInfo()
|
||||
* override. Whether the view pipeline is automatically prepended is determined solely by the
|
||||
* first stage's FirstStageViewApplicationPolicy: if it is kDefaultPrepend (or the
|
||||
* pipeline is empty), the desugared view pipeline is cloned and prepended; otherwise it is not.
|
||||
*
|
||||
* The provided ViewInfo is not mutated. The resolvedNamespaces map is passed to each stage's
|
||||
* ViewPolicy callback to provide access to all resolved namespaces in the aggregation. This
|
||||
* bindViewInfo() to provide access to all resolved namespaces in the aggregation. This
|
||||
* will be used for view resolution in secondary namespaces (e.g. `from` field in $unionWith or
|
||||
* $lookup). */
|
||||
* $lookup).
|
||||
*/
|
||||
void handleView(const ViewInfo& viewInfo, const ResolvedNamespaceMap& resolvedNamespaces);
|
||||
|
||||
private:
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/db/namespace_string.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_document_source.h"
|
||||
#include "mongo/db/pipeline/resolved_namespace.h"
|
||||
#include "mongo/db/pipeline/test_lite_parsed.h"
|
||||
#include "mongo/unittest/assert.h"
|
||||
#include "mongo/unittest/framework.h"
|
||||
@@ -239,17 +238,14 @@ TEST(LiteParsedPipelineTest, ClonedPipelineWithViewStagesPreservesOwnership) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// Parses { <stageName>: {} } into a TestLiteParsed whose ViewPolicy is fixed.
|
||||
std::unique_ptr<LiteParsedDocumentSource> createViewPolicyDefaultParser(
|
||||
const NamespaceString& nss, const BSONElement& spec, const LiteParserOptions& options) {
|
||||
return std::make_unique<TestLiteParsed>(
|
||||
spec, ViewPolicy{.policy = ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend});
|
||||
return std::make_unique<TestLiteParsed>(spec, FirstStageViewApplicationPolicy::kDefaultPrepend);
|
||||
}
|
||||
|
||||
std::unique_ptr<LiteParsedDocumentSource> createViewPolicyDoNothingParser(
|
||||
const NamespaceString& nss, const BSONElement& spec, const LiteParserOptions& options) {
|
||||
return std::make_unique<TestLiteParsed>(
|
||||
spec, ViewPolicy{.policy = ViewPolicy::kFirstStageApplicationPolicy::kDoNothing});
|
||||
return std::make_unique<TestLiteParsed>(spec, FirstStageViewApplicationPolicy::kDoNothing);
|
||||
}
|
||||
|
||||
class LiteParsedPipelineViewPolicyTest : public unittest::Test {
|
||||
@@ -339,71 +335,4 @@ TEST_F(LiteParsedPipelineViewPolicyTest, PrependWhenDefaultPrependIsTrueForAllSt
|
||||
ASSERT_EQ(out[2]->getParseTimeName(), _defaultStageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that handleView passes resolvedNamespaces to stage ViewPolicy callbacks when the pipeline
|
||||
* has involved namespaces (e.g., from $lookup, $graphLookup).
|
||||
*/
|
||||
TEST_F(LiteParsedPipelineViewPolicyTest, HandleViewPassesResolvedNamespacesToStageCallbacks) {
|
||||
const std::string kResolveNsCheckStage = "$resolveNsCheck";
|
||||
ResolvedNamespaceMap receivedResolvedNamespaces;
|
||||
|
||||
auto createResolveNsCheckParser =
|
||||
[&receivedResolvedNamespaces](
|
||||
const NamespaceString& nss, const BSONElement& spec, const LiteParserOptions& options) {
|
||||
return std::make_unique<TestLiteParsed>(
|
||||
spec,
|
||||
ViewPolicy{.policy = ViewPolicy::kFirstStageApplicationPolicy::kDefaultPrepend,
|
||||
.callback = [&receivedResolvedNamespaces](
|
||||
const ViewInfo&,
|
||||
StringData,
|
||||
const ResolvedNamespaceMap& resolvedNs) {
|
||||
receivedResolvedNamespaces = resolvedNs;
|
||||
}});
|
||||
};
|
||||
|
||||
LiteParsedDocumentSource::registerParser(
|
||||
kResolveNsCheckStage,
|
||||
{.parser = createResolveNsCheckParser,
|
||||
.allowedWithApiStrict = AllowedWithApiStrict::kAlways,
|
||||
.allowedWithClientType = AllowedWithClientType::kAny});
|
||||
|
||||
auto guard = ScopeGuard([name = kResolveNsCheckStage] {
|
||||
LiteParsedPipelineViewPolicyTest::unregisterParserByName(name);
|
||||
});
|
||||
|
||||
// Build a ResolvedNamespaceMap simulating resolution of involved namespaces (e.g., a $lookup
|
||||
// foreign collection "test.foreign" resolved to a view "test.otherView" with its pipeline).
|
||||
const auto kForeignNss = NamespaceString::createNamespaceString_forTest("test.foreign"_sd);
|
||||
const auto kUnderlyingNss =
|
||||
NamespaceString::createNamespaceString_forTest("test.underlyingColl"_sd);
|
||||
|
||||
ResolvedNamespaceMap resolvedNamespaces;
|
||||
resolvedNamespaces[kForeignNss] =
|
||||
ResolvedNamespace(kUnderlyingNss, {BSON("$match" << BSON("x" << 1)), BSON("$limit" << 10)});
|
||||
resolvedNamespaces[kResolvedNss] = ResolvedNamespace(kResolvedNss, {});
|
||||
|
||||
std::vector<BSONObj> userStages = {BSON(kResolveNsCheckStage << BSONObj())};
|
||||
LiteParsedPipeline pipeline(kTestNss, userStages);
|
||||
|
||||
std::vector<BSONObj> viewStages = {BSON("$match" << BSON("a" << 1))};
|
||||
const auto viewInfo = createTestViewInfo(std::move(viewStages));
|
||||
|
||||
pipeline.handleView(viewInfo, resolvedNamespaces);
|
||||
|
||||
// Verify the stage's ViewPolicy callback received the resolved namespaces.
|
||||
ASSERT_EQ(receivedResolvedNamespaces.size(), 2U);
|
||||
ASSERT_TRUE(receivedResolvedNamespaces.contains(kForeignNss));
|
||||
ASSERT_TRUE(receivedResolvedNamespaces.contains(kResolvedNss));
|
||||
ASSERT_EQ(receivedResolvedNamespaces.at(kForeignNss).ns, kUnderlyingNss);
|
||||
ASSERT_EQ(receivedResolvedNamespaces.at(kForeignNss).pipeline.size(), 2U);
|
||||
ASSERT_EQ(receivedResolvedNamespaces.at(kResolvedNss).ns, kResolvedNss);
|
||||
ASSERT_TRUE(receivedResolvedNamespaces.at(kResolvedNss).pipeline.empty());
|
||||
|
||||
// Also verify view pipeline was prepended as usual.
|
||||
const auto& stages = pipeline.getStages();
|
||||
ASSERT_EQ(stages.size(), 2U);
|
||||
ASSERT_EQ(stages[0]->getParseTimeName(), "$match");
|
||||
ASSERT_EQ(stages[1]->getParseTimeName(), kResolveNsCheckStage);
|
||||
}
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
@@ -227,8 +227,7 @@ std::unique_ptr<Pipeline> makePipelineFromViewDefinition(
|
||||
subPipelineExpCtx->addResolvedNamespaces(viewLiteParsedPipeline.getInvolvedNamespaces());
|
||||
}
|
||||
|
||||
// Create a LiteParsedPipeline for the user pipeline and apply view handling with ViewPolicy
|
||||
// callbacks.
|
||||
// Create a LiteParsedPipeline for the user pipeline and apply view handling via bindViewInfo().
|
||||
LiteParsedPipeline userLiteParsedPipeline(
|
||||
makeLiteParsedPipeline(subPipelineExpCtx, currentPipeline));
|
||||
if (opts.desugar) {
|
||||
|
||||
@@ -586,14 +586,12 @@ TEST_F(InternalSearchIdLookupBuildDocumentSourceTest,
|
||||
auto liteParsed =
|
||||
LiteParsedInternalSearchIdLookUp::parse(kTestNss, spec.firstElement(), LiteParserOptions{});
|
||||
|
||||
// Simulate what handleView() does: invoke the ViewPolicy callback with a view pipeline.
|
||||
// Simulate what handleView() does: invoke bindViewInfo with a view pipeline.
|
||||
std::vector<BSONObj> viewPipeline = {BSON("$match" << BSON("active" << true)),
|
||||
BSON("$sort" << BSON("createdAt" << -1))};
|
||||
ViewInfo viewInfo(kViewNss, kResolvedNss, viewPipeline);
|
||||
|
||||
auto viewPolicy = liteParsed->getViewPolicy();
|
||||
viewPolicy.callback(
|
||||
viewInfo, DocumentSourceInternalSearchIdLookUp::kStageName, ResolvedNamespaceMap{});
|
||||
liteParsed->bindViewInfo(viewInfo, {});
|
||||
|
||||
// Now use the registry to build the DocumentSource from the LiteParsed.
|
||||
auto docSources = buildDocumentSource(*liteParsed, getExpCtx());
|
||||
@@ -641,7 +639,7 @@ TEST_F(InternalSearchIdLookupBuildDocumentSourceTest,
|
||||
auto liteParsed =
|
||||
LiteParsedInternalSearchIdLookUp::parse(kTestNss, spec.firstElement(), LiteParserOptions{});
|
||||
|
||||
// Don't invoke the ViewPolicy callback.
|
||||
// Don't invoke bindViewInfo.
|
||||
|
||||
auto docSources = buildDocumentSource(*liteParsed, getExpCtx());
|
||||
|
||||
|
||||
@@ -86,12 +86,12 @@ public:
|
||||
return _ownedSpec;
|
||||
}
|
||||
|
||||
ViewPolicy getViewPolicy() const final {
|
||||
return ViewPolicy{
|
||||
.policy = ViewPolicy::kFirstStageApplicationPolicy::kDoNothing,
|
||||
.callback = [this](const ViewInfo& viewInfo, StringData, const ResolvedNamespaceMap&) {
|
||||
_ownedSpec.setViewPipeline(viewInfo.getSerializedViewPipeline());
|
||||
}};
|
||||
FirstStageViewApplicationPolicy getFirstStageViewApplicationPolicy() const override {
|
||||
return FirstStageViewApplicationPolicy::kDoNothing;
|
||||
}
|
||||
|
||||
void bindViewInfo(const ViewInfo& viewInfo, const ResolvedNamespaceMap&) override {
|
||||
_ownedSpec.setViewPipeline(viewInfo.getSerializedViewPipeline());
|
||||
}
|
||||
|
||||
LiteParsedInternalSearchIdLookUp(DocumentSourceIdLookupSpec spec)
|
||||
@@ -99,7 +99,7 @@ public:
|
||||
_ownedSpec(std::move(spec)) {}
|
||||
|
||||
private:
|
||||
mutable DocumentSourceIdLookupSpec _ownedSpec;
|
||||
DocumentSourceIdLookupSpec _ownedSpec;
|
||||
};
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
@@ -45,23 +45,22 @@ const NamespaceString kResolvedNss =
|
||||
NamespaceString::createNamespaceString_forTest("unittests.resolved_coll");
|
||||
|
||||
/**
|
||||
* Tests for LiteParsed::getViewPolicy() and LiteParsed::getStageParams() to verify the
|
||||
* ViewPolicy callback correctly stores view pipeline BSON for use in desugaring.
|
||||
* Tests for LiteParsed::bindViewInfo() and LiteParsed::getStageParams() to verify that
|
||||
* bindViewInfo correctly stores view pipeline BSON for use in desugaring.
|
||||
*/
|
||||
class LiteParsedInternalSearchIdLookUpTest : public unittest::Test {};
|
||||
|
||||
TEST_F(LiteParsedInternalSearchIdLookUpTest, GetViewPolicyReturnsDoNothingPolicy) {
|
||||
TEST_F(LiteParsedInternalSearchIdLookUpTest, GetFirstStageViewApplicationPolicyReturnsDoNothing) {
|
||||
BSONObj spec = BSON(LiteParsedInternalSearchIdLookUp::kStageName << BSON("limit" << 100LL));
|
||||
auto liteParsed =
|
||||
LiteParsedInternalSearchIdLookUp::parse(kTestNss, spec.firstElement(), LiteParserOptions{});
|
||||
|
||||
auto viewPolicy = liteParsed->getViewPolicy();
|
||||
|
||||
// The policy should be kDoNothing since IdLookup handles view resolution itself.
|
||||
ASSERT_EQ(viewPolicy.policy, ViewPolicy::kFirstStageApplicationPolicy::kDoNothing);
|
||||
ASSERT_EQ(liteParsed->getFirstStageViewApplicationPolicy(),
|
||||
FirstStageViewApplicationPolicy::kDoNothing);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedInternalSearchIdLookUpTest, ViewPolicyCallbackStoresViewPipelineBson) {
|
||||
TEST_F(LiteParsedInternalSearchIdLookUpTest, BindViewInfoStoresViewPipelineBson) {
|
||||
BSONObj spec = BSON(LiteParsedInternalSearchIdLookUp::kStageName << BSON("limit" << 100LL));
|
||||
auto liteParsed =
|
||||
LiteParsedInternalSearchIdLookUp::parse(kTestNss, spec.firstElement(), LiteParserOptions{});
|
||||
@@ -71,10 +70,7 @@ TEST_F(LiteParsedInternalSearchIdLookUpTest, ViewPolicyCallbackStoresViewPipelin
|
||||
BSON("$project" << BSON("name" << 1 << "status" << 1))};
|
||||
ViewInfo viewInfo(kViewNss, kResolvedNss, viewPipeline);
|
||||
|
||||
// Invoke the callback.
|
||||
auto viewPolicy = liteParsed->getViewPolicy();
|
||||
viewPolicy.callback(
|
||||
viewInfo, LiteParsedInternalSearchIdLookUp::kStageName, ResolvedNamespaceMap{});
|
||||
liteParsed->bindViewInfo(viewInfo, {});
|
||||
|
||||
// Now getStageParams should return params with the view pipeline BSON.
|
||||
auto stageParams = liteParsed->getStageParams();
|
||||
@@ -100,7 +96,7 @@ TEST_F(LiteParsedInternalSearchIdLookUpTest, GetStageParamsReturnsLimitFromSpec)
|
||||
// Verify the limit was extracted correctly from the spec.
|
||||
ASSERT(typedParams->ownedSpec.getLimit());
|
||||
ASSERT_EQ(typedParams->ownedSpec.getLimit().get(), 42);
|
||||
// Without view callback, the view pipeline should be empty.
|
||||
// Without bindViewInfo call, the view pipeline should be empty.
|
||||
ASSERT_FALSE(typedParams->ownedSpec.getViewPipeline());
|
||||
}
|
||||
|
||||
@@ -117,7 +113,7 @@ TEST_F(LiteParsedInternalSearchIdLookUpTest, GetStageParamsReturnsNothingWhenNot
|
||||
ASSERT_FALSE(typedParams->ownedSpec.getViewPipeline());
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedInternalSearchIdLookUpTest, ViewPolicyCallbackWithEmptyViewPipeline) {
|
||||
TEST_F(LiteParsedInternalSearchIdLookUpTest, BindViewInfoWithEmptyViewPipeline) {
|
||||
BSONObj spec = BSON(LiteParsedInternalSearchIdLookUp::kStageName << BSON("limit" << 10LL));
|
||||
auto liteParsed =
|
||||
LiteParsedInternalSearchIdLookUp::parse(kTestNss, spec.firstElement(), LiteParserOptions{});
|
||||
@@ -125,9 +121,7 @@ TEST_F(LiteParsedInternalSearchIdLookUpTest, ViewPolicyCallbackWithEmptyViewPipe
|
||||
// Create an empty view pipeline.
|
||||
ViewInfo viewInfo(kViewNss, kResolvedNss, {});
|
||||
|
||||
auto viewPolicy = liteParsed->getViewPolicy();
|
||||
viewPolicy.callback(
|
||||
viewInfo, LiteParsedInternalSearchIdLookUp::kStageName, ResolvedNamespaceMap{});
|
||||
liteParsed->bindViewInfo(viewInfo, {});
|
||||
|
||||
auto stageParams = liteParsed->getStageParams();
|
||||
auto* typedParams = dynamic_cast<InternalSearchIdLookupStageParams*>(stageParams.get());
|
||||
@@ -212,7 +206,7 @@ TEST_F(LiteParsedInternalSearchIdLookUpTest,
|
||||
ASSERT_EQ(stages.size(), 1U);
|
||||
ASSERT_EQ(stages[0]->getParseTimeName(), LiteParsedInternalSearchIdLookUp::kStageName);
|
||||
|
||||
// The IdLookup stage should now carry the desugared view pipeline via its ViewPolicy callback.
|
||||
// The IdLookup stage should now carry the desugared view pipeline via bindViewInfo().
|
||||
auto* idLookup = dynamic_cast<LiteParsedInternalSearchIdLookUp*>(stages[0].get());
|
||||
ASSERT_TRUE(idLookup != nullptr);
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ bool isMongotStage(DocumentSource* stage) {
|
||||
dynamic_cast<mongo::DocumentSourceSearchMeta*>(stage));
|
||||
}
|
||||
|
||||
// TODO SERVER-116021 Remove this function when the extension can do this through ViewPolicy.
|
||||
// TODO SERVER-116021 Remove this function when the extension can do this through bindViewInfo().
|
||||
bool isExtensionVectorSearchStage(std::string stageName) {
|
||||
return stageName == kExtensionVectorSearchStageName ||
|
||||
stageName == DocumentSourceVectorSearch::kStageName;
|
||||
|
||||
@@ -114,7 +114,7 @@ bool isMongotPipeline(const Pipeline* pipeline);
|
||||
* stage that will rely on calls to mongot.
|
||||
*
|
||||
* TODO SERVER-115069 Remove this once search queries are desugared at LiteParsed time and handle
|
||||
* the view through a custom ViewPolicy.
|
||||
* the view through a bindViewInfo() override.
|
||||
*/
|
||||
bool isMongotLiteParsedPipeline(const LiteParsedPipeline& lpp);
|
||||
|
||||
@@ -135,7 +135,7 @@ bool isMongotStage(DocumentSource* stage);
|
||||
|
||||
/**
|
||||
* Check if this is a $vectorSearch-as-an-extension stage.
|
||||
* TODO SERVER-116021 Remove this function when the extension can do this through ViewPolicy.
|
||||
* TODO SERVER-116021 Remove this function when the extension can do this through bindViewInfo().
|
||||
*/
|
||||
bool isExtensionVectorSearchStage(std::string stageName);
|
||||
|
||||
|
||||
@@ -45,8 +45,10 @@ DECLARE_STAGE_PARAMS_DERIVED_DEFAULT(Test);
|
||||
*/
|
||||
class TestLiteParsed final : public LiteParsedDocumentSourceDefault<TestLiteParsed> {
|
||||
public:
|
||||
TestLiteParsed(const BSONElement& originalBson, ViewPolicy viewPolicy = DefaultViewPolicy{})
|
||||
: LiteParsedDocumentSourceDefault(originalBson), _viewPolicy(viewPolicy) {}
|
||||
TestLiteParsed(
|
||||
const BSONElement& originalBson,
|
||||
FirstStageViewApplicationPolicy policy = FirstStageViewApplicationPolicy::kDefaultPrepend)
|
||||
: LiteParsedDocumentSourceDefault(originalBson), _policy(policy) {}
|
||||
|
||||
stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final {
|
||||
return stdx::unordered_set<NamespaceString>();
|
||||
@@ -60,11 +62,11 @@ public:
|
||||
return std::make_unique<TestStageParams>(_originalBson);
|
||||
}
|
||||
|
||||
ViewPolicy getViewPolicy() const final {
|
||||
return _viewPolicy;
|
||||
FirstStageViewApplicationPolicy getFirstStageViewApplicationPolicy() const override {
|
||||
return _policy;
|
||||
}
|
||||
|
||||
ViewPolicy _viewPolicy;
|
||||
FirstStageViewApplicationPolicy _policy;
|
||||
};
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
@@ -104,7 +104,7 @@ buildResolvedPipelineForRegularView(OperationContext* opCtx,
|
||||
auto lpp = LiteParsedPipeline(request, true, LiteParserOptions{.ifrContext = ifrContext});
|
||||
lpp.makeOwned();
|
||||
|
||||
// Desugar and call handleView() to invoke ViewPolicy callbacks for extension stages.
|
||||
// Desugar and call handleView() to invoke bindViewInfo() for extension stages.
|
||||
LiteParsedDesugarer::desugar(&lpp);
|
||||
|
||||
auto resolvedNamespaces = helpers.resolveInvolvedNamespaces(lpp.getInvolvedNamespaces());
|
||||
|
||||
@@ -105,7 +105,7 @@ public:
|
||||
|
||||
/**
|
||||
* Builds a resolved aggregation request from a view for mongos, handling special cases for
|
||||
* mongot pipelines, timeseries views, and invoking ViewPolicy callbacks for extension stages.
|
||||
* mongot pipelines, timeseries views, and invoking bindViewInfo() for extension stages.
|
||||
*
|
||||
* This is the single entrypoint for mongos view resolution. It returns a fully resolved
|
||||
* request with the pipeline set, and optionally a LiteParsedPipeline that was created during
|
||||
|
||||
@@ -1165,7 +1165,7 @@ PipelineResolver::MongosViewRequestResult buildResolvedViewAggregateRequest(
|
||||
|
||||
// We populated a view through the kickback retry logic. Build a new resolved request
|
||||
// from this view, handling special cases for
|
||||
// mongot pipelines, timeseries views, and invoking ViewPolicy callbacks for
|
||||
// mongot pipelines, timeseries views, and invoking bindViewInfo() for
|
||||
// extension stages.
|
||||
PipelineResolver::MongosPipelineHelpers helpers{makeExpressionContext,
|
||||
resolveInvolvedNamespaces};
|
||||
|
||||
Reference in New Issue
Block a user