SERVER-125058 Forward LiteParsed subpipeline from lookup stage to full DocumentSourceLookup (#52408)
GitOrigin-RevId: ba4fd486e300131517ebbad3171f8f0fbee96849
This commit is contained in:
parent
618e4ff87e
commit
3f75f45a77
@ -45,7 +45,7 @@ const unstablePipelines = [
|
||||
[{$listSessions: {}}],
|
||||
[{$planCacheStats: {}}],
|
||||
[{$unionWith: {coll: "coll2", pipeline: [{$collStats: {latencyStats: {}}}]}}],
|
||||
[{$lookup: {from: "coll2", pipeline: [{$indexStats: {}}]}}],
|
||||
[{$lookup: {from: "coll2", as: "out", pipeline: [{$indexStats: {}}]}}],
|
||||
[{$facet: {field1: [], field2: [{$indexStats: {}}]}}],
|
||||
[{$rankFusion: {input: {pipelines: {field1: [{$sort: {foo: 1}}]}}}}],
|
||||
[{$score: {score: 10}}],
|
||||
|
||||
@ -104,26 +104,31 @@ const kNotAllowedInFacetErrorCode = 40600;
|
||||
kNotAllowedInUnionWithErrorCode,
|
||||
"Using $unionWith with $testFoo in sub-pipeline should be rejected",
|
||||
);
|
||||
// Outer $lookup may reject the inner $unionWith via strictness propagation before the
|
||||
// $unionWith's own validator runs, so accept either code.
|
||||
assertErrorCode(
|
||||
coll,
|
||||
[{$lookup: {from: other.getName(), pipeline: unionWithPipeline, as: "joined"}}],
|
||||
kNotAllowedInUnionWithErrorCode,
|
||||
[kNotAllowedInUnionWithErrorCode, kNotAllowedInLookupErrorCode],
|
||||
"Using $unionWith with $testFoo in sub-pipeline should be rejected",
|
||||
);
|
||||
// See comment above: $facet may reject the inner $unionWith via strictness propagation first.
|
||||
assertErrorCode(
|
||||
coll,
|
||||
[{$facet: {facetPipe: unionWithPipeline}}],
|
||||
kNotAllowedInUnionWithErrorCode,
|
||||
[kNotAllowedInUnionWithErrorCode, kNotAllowedInFacetErrorCode],
|
||||
"Using $unionWith with $testFoo in sub-pipeline should be rejected",
|
||||
);
|
||||
assert.commandFailedWithCode(
|
||||
// View creation may validate pre-desugar (lenient) and defer the real rejection to query time.
|
||||
assert.commandWorkedOrFailedWithCode(
|
||||
db.runCommand({
|
||||
create: jsTestName() + "_view",
|
||||
viewOn: coll.getName(),
|
||||
pipeline: unionWithPipeline,
|
||||
}),
|
||||
kNotAllowedInUnionWithErrorCode,
|
||||
[kNotAllowedInUnionWithErrorCode, kNotAllowedInLookupErrorCode, kNotAllowedInFacetErrorCode],
|
||||
);
|
||||
db[jsTestName() + "_view"].drop();
|
||||
{
|
||||
const viewName = "testFoo_in_def";
|
||||
assert.commandWorked(db.createView(viewName, other.getName(), [{$testFoo: {}}]));
|
||||
|
||||
@ -1048,7 +1048,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnJoinedNamespa
|
||||
|
||||
authzSession->assumePrivilegesForDB(Privilege(rsrcFoo, ActionType::find), nssFoo.dbName());
|
||||
|
||||
BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssBar.coll())));
|
||||
BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssBar.coll() << "as"
|
||||
<< "out")));
|
||||
auto aggReq = buildAggReq(nssFoo, pipeline);
|
||||
PrivilegeVector privileges = uassertStatusOK(auth::getPrivilegesForAggregate(
|
||||
_opCtx.get(), authzSession.get(), nssFoo, aggReq, false));
|
||||
@ -1070,7 +1071,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnJoinedNamespace) {
|
||||
{Privilege(rsrcFoo, ActionType::find), Privilege(rsrcBar, ActionType::find)},
|
||||
nssFoo.dbName());
|
||||
|
||||
BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssBar.coll())));
|
||||
BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssBar.coll() << "as"
|
||||
<< "out")));
|
||||
auto aggReq = buildAggReq(nssFoo, pipeline);
|
||||
PrivilegeVector privileges = uassertStatusOK(auth::getPrivilegesForAggregate(
|
||||
_opCtx.get(), authzSession.get(), nssFoo, aggReq, true));
|
||||
@ -1094,9 +1096,11 @@ TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnNestedJoinedN
|
||||
{Privilege(rsrcFoo, ActionType::find), Privilege(rsrcBar, ActionType::find)},
|
||||
nssFoo.dbName());
|
||||
|
||||
BSONArray nestedPipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssQux.coll())));
|
||||
BSONArray nestedPipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssQux.coll() << "as"
|
||||
<< "out")));
|
||||
BSONArray pipeline = BSON_ARRAY(
|
||||
BSON("$lookup" << BSON("from" << nssBar.coll() << "pipeline" << nestedPipeline)));
|
||||
BSON("$lookup" << BSON("from" << nssBar.coll() << "pipeline" << nestedPipeline << "as"
|
||||
<< "out")));
|
||||
auto aggReq = buildAggReq(nssFoo, pipeline);
|
||||
PrivilegeVector privileges = uassertStatusOK(auth::getPrivilegesForAggregate(
|
||||
_opCtx.get(), authzSession.get(), nssFoo, aggReq, false));
|
||||
@ -1121,9 +1125,11 @@ TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnNestedJoinedNamespa
|
||||
Privilege(rsrcQux, ActionType::find)},
|
||||
nssFoo.dbName());
|
||||
|
||||
BSONArray nestedPipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssQux.coll())));
|
||||
BSONArray nestedPipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << nssQux.coll() << "as"
|
||||
<< "out")));
|
||||
BSONArray pipeline = BSON_ARRAY(
|
||||
BSON("$lookup" << BSON("from" << nssBar.coll() << "pipeline" << nestedPipeline)));
|
||||
BSON("$lookup" << BSON("from" << nssBar.coll() << "pipeline" << nestedPipeline << "as"
|
||||
<< "out")));
|
||||
auto aggReq = buildAggReq(nssFoo, pipeline);
|
||||
PrivilegeVector privileges = uassertStatusOK(auth::getPrivilegesForAggregate(
|
||||
_opCtx.get(), authzSession.get(), nssFoo, aggReq, false));
|
||||
@ -1241,9 +1247,9 @@ TEST_F(AuthorizationSessionTest,
|
||||
// We only have find on the aggregation namespace.
|
||||
authzSession->assumePrivilegesForDB(Privilege(rsrcFoo, ActionType::find), nssFoo.dbName());
|
||||
|
||||
BSONArray pipeline =
|
||||
BSON_ARRAY(fromjson("{$facet: {lookup: [{$lookup: {from: 'bar'}}], graphLookup: "
|
||||
"[{$graphLookup: {from: 'qux'}}]}}"));
|
||||
BSONArray pipeline = BSON_ARRAY(
|
||||
fromjson("{$facet: {lookup: [{$lookup: {from: 'bar', as: 'out'}}], graphLookup: "
|
||||
"[{$graphLookup: {from: 'qux'}}]}}"));
|
||||
auto aggReq = buildAggReq(nssFoo, pipeline);
|
||||
PrivilegeVector privileges = uassertStatusOK(auth::getPrivilegesForAggregate(
|
||||
_opCtx.get(), authzSession.get(), nssFoo, aggReq, false));
|
||||
@ -1281,9 +1287,9 @@ TEST_F(AuthorizationSessionTest,
|
||||
Privilege(rsrcQux, ActionType::find)},
|
||||
nssFoo.dbName());
|
||||
|
||||
BSONArray pipeline =
|
||||
BSON_ARRAY(fromjson("{$facet: {lookup: [{$lookup: {from: 'bar'}}], graphLookup: "
|
||||
"[{$graphLookup: {from: 'qux'}}]}}"));
|
||||
BSONArray pipeline = BSON_ARRAY(
|
||||
fromjson("{$facet: {lookup: [{$lookup: {from: 'bar', as: 'out'}}], graphLookup: "
|
||||
"[{$graphLookup: {from: 'qux'}}]}}"));
|
||||
|
||||
auto aggReq = buildAggReq(nssFoo, pipeline);
|
||||
PrivilegeVector privileges = uassertStatusOK(auth::getPrivilegesForAggregate(
|
||||
|
||||
@ -249,7 +249,7 @@ protected:
|
||||
NamespaceString nss = NamespaceString::createNamespaceString_forTest("test", main);
|
||||
NamespaceString nss2 = NamespaceString::createNamespaceString_forTest("test", secondary);
|
||||
|
||||
BSONObj lookup = BSON("$lookup" << BSON("from" << nss2.coll()));
|
||||
BSONObj lookup = BSON("$lookup" << BSON("from" << nss2.coll() << "as" << "out"));
|
||||
BSONArray pipeline = BSON_ARRAY(lookup);
|
||||
_cmdObj = BSON("aggregate" << main << "pipeline" << pipeline << "cursor" << BSONObj{});
|
||||
_request =
|
||||
|
||||
@ -995,7 +995,8 @@ SecondParseRequirement maybeApplyViewPipeline(const AggExState& aggExState,
|
||||
desugaredLPP,
|
||||
aggExState.getResolvedView(),
|
||||
aggExState.getOriginalNss(),
|
||||
uassertStatusOK(aggCatalogState.resolveInvolvedNamespaces(aggExState.getOpCtx())));
|
||||
uassertStatusOK(aggCatalogState.resolveInvolvedNamespaces(aggExState.getOpCtx())),
|
||||
LiteParserOptions{.ifrContext = aggExState.getIfrContext()});
|
||||
return SecondParseRequirement::kReparseFromLPP;
|
||||
}
|
||||
|
||||
|
||||
@ -430,6 +430,7 @@ mongo_cc_library(
|
||||
"group_processor_base.cpp",
|
||||
"hybrid_search_pipeline_builder.cpp",
|
||||
"lite_parsed_graph_lookup.cpp",
|
||||
"lite_parsed_lookup.cpp",
|
||||
"lite_parsed_rank_fusion.cpp",
|
||||
"lite_parsed_score_fusion.cpp",
|
||||
"lite_parsed_union_with.cpp",
|
||||
@ -1377,6 +1378,7 @@ mongo_cc_unit_test(
|
||||
"document_source_test.cpp",
|
||||
"document_source_union_with_test.cpp",
|
||||
"document_source_unwind_test.cpp",
|
||||
"lite_parsed_lookup_test.cpp",
|
||||
"lite_parsed_rank_fusion_test.cpp",
|
||||
"lite_parsed_score_fusion_test.cpp",
|
||||
"serverless_aggregation_context_fixture.cpp",
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
#include "mongo/db/pipeline/document_source_queue.h"
|
||||
#include "mongo/db/pipeline/document_source_unwind.h"
|
||||
#include "mongo/db/pipeline/expression_context_builder.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_desugarer.h"
|
||||
#include "mongo/db/pipeline/optimization/optimize.h"
|
||||
#include "mongo/db/pipeline/pipeline.h"
|
||||
#include "mongo/db/pipeline/pipeline_factory.h"
|
||||
@ -61,33 +62,16 @@
|
||||
#include "mongo/db/pipeline/sort_reorder_helpers.h"
|
||||
#include "mongo/db/pipeline/variable_validation.h"
|
||||
#include "mongo/db/query/allowed_contexts.h"
|
||||
#include "mongo/db/query/query_feature_flags_gen.h"
|
||||
#include "mongo/db/shard_role/shard_catalog/raw_data_operation.h"
|
||||
#include "mongo/db/stats/counters.h"
|
||||
#include "mongo/db/topology/sharding_state.h"
|
||||
#include "mongo/db/views/pipeline_resolver.h"
|
||||
#include "mongo/db/views/resolved_view.h"
|
||||
#include "mongo/idl/idl_parser.h"
|
||||
#include "mongo/util/str.h"
|
||||
|
||||
namespace mongo {
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Constructs a query of the following shape:
|
||||
* {$or: [
|
||||
* {'fieldName': {$eq: 'values[0]'}},
|
||||
* {'fieldName': {$eq: 'values[1]'}},
|
||||
* ...
|
||||
* ]}
|
||||
*/
|
||||
BSONObj buildEqualityOrQuery(const std::string& fieldName, const BSONArray& values) {
|
||||
BSONObjBuilder orBuilder;
|
||||
{
|
||||
BSONArrayBuilder orPredicatesBuilder(orBuilder.subarrayStart("$or"));
|
||||
for (auto&& value : values) {
|
||||
orPredicatesBuilder.append(BSON(fieldName << BSON("$eq" << value)));
|
||||
}
|
||||
}
|
||||
return orBuilder.obj();
|
||||
}
|
||||
|
||||
// Parses $lookup 'from' field. The 'from' field must be a string or one of the following
|
||||
// exceptions:
|
||||
@ -98,8 +82,8 @@ NamespaceString parseLookupFromAndResolveNamespace(const BSONElement& elem,
|
||||
bool allowGenericForeignDbLookup,
|
||||
// usingMongos is assumed false any time there is
|
||||
// no expCtx available.
|
||||
bool usingMongos = false,
|
||||
bool isParsingViewDefinition = false) {
|
||||
bool usingMongos,
|
||||
bool isParsingViewDefinition) {
|
||||
// The 'from' field must be a string or an object. Since we now support the object form
|
||||
// any time we are connected directly to mongod, we include it in the error message.
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
@ -150,10 +134,31 @@ NamespaceString parseLookupFromAndResolveNamespace(const BSONElement& elem,
|
||||
return nss;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Constructs a query of the following shape:
|
||||
* {$or: [
|
||||
* {'fieldName': {$eq: 'values[0]'}},
|
||||
* {'fieldName': {$eq: 'values[1]'}},
|
||||
* ...
|
||||
* ]}
|
||||
*/
|
||||
BSONObj buildEqualityOrQuery(const std::string& fieldName, const BSONArray& values) {
|
||||
BSONObjBuilder orBuilder;
|
||||
{
|
||||
BSONArrayBuilder orPredicatesBuilder(orBuilder.subarrayStart("$or"));
|
||||
for (auto&& value : values) {
|
||||
orPredicatesBuilder.append(BSON(fieldName << BSON("$eq" << value)));
|
||||
}
|
||||
}
|
||||
return orBuilder.obj();
|
||||
}
|
||||
|
||||
// Creates the conditions for joining the local and foreign fields inside of a $match.
|
||||
static BSONObj createMatchStageJoinObj(const Document& input,
|
||||
const FieldPath& localFieldPath,
|
||||
const std::string& foreignFieldName) {
|
||||
BSONObj createMatchStageJoinObj(const Document& input,
|
||||
const FieldPath& localFieldPath,
|
||||
const std::string& foreignFieldName) {
|
||||
// Add the 'localFieldPath' of 'input' into 'localFieldList'. If 'localFieldPath' references a
|
||||
// field with an array in its path, we may need to join on multiple values, so we add each
|
||||
// element to 'localFieldList'.
|
||||
@ -369,15 +374,7 @@ DocumentSourceLookUp::DocumentSourceLookUp(
|
||||
|
||||
_userPipeline = std::move(pipeline);
|
||||
|
||||
for (auto&& varElem : letVariables) {
|
||||
const auto varName = varElem.fieldNameStringData();
|
||||
variableValidation::validateNameForUserWrite(varName);
|
||||
|
||||
_letVariables.emplace_back(
|
||||
std::string{varName},
|
||||
Expression::parseOperand(expCtx.get(), varElem, expCtx->variablesParseState),
|
||||
_variablesParseState.defineVariable(varName));
|
||||
}
|
||||
parseAndDefineLetVariables(letVariables, expCtx);
|
||||
|
||||
// Initialize the introspection pipeline before we insert the $match (if applicable). This is
|
||||
// okay because we only use the introspection pipeline for reference while doing query analysis
|
||||
@ -388,12 +385,158 @@ DocumentSourceLookUp::DocumentSourceLookUp(
|
||||
// $match.
|
||||
initializeResolvedIntrospectionPipeline();
|
||||
|
||||
// Finally, insert the $match placeholder if we need it.
|
||||
if (_fieldMatchPipelineIdx) {
|
||||
_sharedState->resolvedPipeline.insert(_sharedState->resolvedPipeline.begin() +
|
||||
*_fieldMatchPipelineIdx,
|
||||
BSON("$match" << BSONObj()));
|
||||
insertFieldMatchPlaceholder();
|
||||
}
|
||||
|
||||
DocumentSourceLookUp::DocumentSourceLookUp(
|
||||
NamespaceString fromNs,
|
||||
std::string as,
|
||||
std::vector<BSONObj> userPipeline,
|
||||
LiteParsedPipeline desugaredPipeline,
|
||||
BSONObj letVariables,
|
||||
boost::optional<std::pair<std::string, std::string>> localForeignFields,
|
||||
boost::optional<BSONObj> unwindSpec,
|
||||
const boost::intrusive_ptr<ExpressionContext>& pExpCtx)
|
||||
: DocumentSourceLookUp(fromNs, as, pExpCtx) {
|
||||
resolvedPipelineHelper(fromNs, userPipeline, localForeignFields, pExpCtx);
|
||||
|
||||
parseAndDefineLetVariables(letVariables, pExpCtx);
|
||||
|
||||
_variables.copyToExpCtx(_variablesParseState, _fromExpCtx.get());
|
||||
_fromExpCtx->startExpressionCounters();
|
||||
const auto& resolvedNamespaces = pExpCtx->getResolvedNamespaces();
|
||||
auto it = resolvedNamespaces.find(_fromNs);
|
||||
if (it != resolvedNamespaces.end() && !it->second.pipeline.empty()) {
|
||||
_sharedState->resolvedIntrospectionPipeline = parsePipelineFromLPPWithMaybeViewDefinition(
|
||||
_fromExpCtx, it->second, desugaredPipeline, userPipeline, _fromNs);
|
||||
} else {
|
||||
_sharedState->resolvedIntrospectionPipeline =
|
||||
Pipeline::parseFromLiteParsed(desugaredPipeline, _fromExpCtx, lookupPipeValidator);
|
||||
}
|
||||
_fromExpCtx->stopExpressionCounters();
|
||||
|
||||
_userPipeline = std::move(userPipeline);
|
||||
|
||||
insertFieldMatchPlaceholder();
|
||||
|
||||
// Absorb an $unwind spec if one was provided via LookUpStageParams.
|
||||
if (unwindSpec && !unwindSpec->isEmpty()) {
|
||||
_unwindSrc = boost::dynamic_pointer_cast<DocumentSourceUnwind>(
|
||||
DocumentSourceUnwind::createFromBson(unwindSpec->firstElement(), pExpCtx));
|
||||
}
|
||||
}
|
||||
|
||||
DocumentSourceContainer DocumentSourceLookUp::createFromStageParams(
|
||||
LookUpStageParams& params, const boost::intrusive_ptr<ExpressionContext>& expCtx) {
|
||||
if (params.hasForeignDB) {
|
||||
// TODO SERVER-125518 Remove this validation when we can bind to an involved namespace in
|
||||
// LiteParsedLookup. It should be moved to the validate() override.
|
||||
// Re-run the resolver now that expCtx is available. It encodes the
|
||||
// allow-list for cross-db $lookup (config.collections, config.chunks, oplog,
|
||||
// cache.chunks.*) and the mongos/view gating, matching the legacy createFromBson path.
|
||||
parseLookupFromAndResolveNamespace(params.getOriginalBson().Obj().getField("from"),
|
||||
expCtx->getNamespaceString().dbName(),
|
||||
expCtx->getAllowGenericForeignDbLookup(),
|
||||
expCtx->getInRouter() || expCtx->getFromRouter(),
|
||||
expCtx->getIsParsingViewDefinition());
|
||||
}
|
||||
|
||||
// TODO SERVER-121091 This can be removed once hybrid search desugars into the internal hybrid
|
||||
// search stage.
|
||||
if (params.isHybridSearch || hybrid_scoring_util::isHybridSearchPipeline(params.pipeline)) {
|
||||
hybrid_scoring_util::assertForeignCollectionIsNotTimeseries(params.fromNss, expCtx);
|
||||
}
|
||||
|
||||
const bool hasLocal = params.localField.has_value();
|
||||
const bool hasForeign = params.foreignField.has_value();
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
"$lookup requires both or neither of 'localField' and 'foreignField' to be specified",
|
||||
hasLocal == hasForeign);
|
||||
|
||||
boost::optional<std::pair<std::string, std::string>> localForeignFields;
|
||||
if (hasLocal) {
|
||||
localForeignFields =
|
||||
std::pair(std::move(*params.localField), std::move(*params.foreignField));
|
||||
}
|
||||
|
||||
// Without a subpipeline there is no desugared LPP to forward.
|
||||
if (!params.liteParsedPipeline) {
|
||||
return {DocumentSourceLookUp::createFromBson(params.getOriginalBson(), expCtx)};
|
||||
}
|
||||
|
||||
return {make_intrusive<DocumentSourceLookUp>(std::move(params.fromNss),
|
||||
std::move(params.as),
|
||||
std::move(params.pipeline),
|
||||
std::move(*params.liteParsedPipeline),
|
||||
std::move(params.letVariables),
|
||||
std::move(localForeignFields),
|
||||
std::move(params.unwindSpec),
|
||||
expCtx)};
|
||||
}
|
||||
|
||||
DocumentSourceContainer lookupStageParamsToDocumentSourceFn(
|
||||
const std::unique_ptr<StageParams>& stageParams,
|
||||
const boost::intrusive_ptr<ExpressionContext>& expCtx) {
|
||||
auto* typedParams = dynamic_cast<LookUpStageParams*>(stageParams.get());
|
||||
tassert(11786210, "Expected LookUpStageParams for lookup stage", typedParams != nullptr);
|
||||
|
||||
// TODO SERVER-121094 Remove when feature flag is removed.
|
||||
auto ifrCtx = expCtx->getIfrContext();
|
||||
auto hybridSearchFlagEnabled = ifrCtx &&
|
||||
ifrCtx->getSavedFlagValue(feature_flags::gFeatureFlagExtensionsInsideHybridSearch);
|
||||
if (!hybridSearchFlagEnabled) {
|
||||
return {DocumentSourceLookUp::createFromBson(typedParams->getOriginalBson(), expCtx)};
|
||||
}
|
||||
|
||||
return DocumentSourceLookUp::createFromStageParams(*typedParams, expCtx);
|
||||
}
|
||||
|
||||
ALLOCATE_AND_REGISTER_STAGE_PARAMS(lookup, LookUpStageParams)
|
||||
|
||||
// TODO SERVER-125518 Move this function into LiteParsed.
|
||||
std::unique_ptr<Pipeline> DocumentSourceLookUp::parsePipelineFromLPPWithMaybeViewDefinition(
|
||||
const boost::intrusive_ptr<ExpressionContext>& expCtx,
|
||||
const ResolvedNamespace& resolvedNs,
|
||||
LiteParsedPipeline& desugaredPipeline,
|
||||
const std::vector<BSONObj>& rawPipeline,
|
||||
const NamespaceString& userNss) {
|
||||
|
||||
if (resolvedNs.ns.isTimeseriesBucketsCollection() &&
|
||||
isRawDataOperation(expCtx->getOperationContext())) {
|
||||
// Raw Data operations on timeseries collections operate without the timeseries view.
|
||||
return Pipeline::parseFromLiteParsed(desugaredPipeline, expCtx, lookupPipeValidator);
|
||||
}
|
||||
|
||||
if (resolvedNs.pipeline.empty()) {
|
||||
return Pipeline::parseFromLiteParsed(desugaredPipeline, expCtx, lookupPipeValidator);
|
||||
}
|
||||
|
||||
// For search views, fall back to the BSON-based path since search view handling
|
||||
// requires raw BSON pipeline inspection.
|
||||
if (search_helper_bson_obj::isMongotPipeline(expCtx->getIfrContext(), rawPipeline)) {
|
||||
auto opts = pipeline_factory::kDesugarOnly;
|
||||
opts.validator = lookupPipeValidator;
|
||||
return pipeline_factory::makePipelineFromViewDefinition(
|
||||
expCtx, resolvedNs, std::vector<BSONObj>(rawPipeline), opts, userNss);
|
||||
}
|
||||
|
||||
{
|
||||
// Add resolved namespaces from view pipeline.
|
||||
LiteParsedPipeline viewLiteParsedPipeline(resolvedNs.ns, resolvedNs.pipeline);
|
||||
expCtx->addResolvedNamespaces(viewLiteParsedPipeline.getInvolvedNamespaces());
|
||||
}
|
||||
|
||||
// Apply the view to the desugared LPP.
|
||||
const ResolvedView resolvedView{resolvedNs.ns, resolvedNs.pipeline, BSONObj()};
|
||||
PipelineResolver::applyViewToLiteParsed(
|
||||
&desugaredPipeline,
|
||||
resolvedView,
|
||||
userNss,
|
||||
expCtx->getResolvedNamespaces(),
|
||||
LiteParserOptions{.ifrContext = expCtx->getIfrContext()});
|
||||
|
||||
// Parse from the modified LiteParsedPipeline (already desugared, view already applied).
|
||||
return Pipeline::parseFromLiteParsed(desugaredPipeline, expCtx, lookupPipeValidator);
|
||||
}
|
||||
|
||||
DocumentSourceLookUp::DocumentSourceLookUp(const DocumentSourceLookUp& original,
|
||||
@ -445,87 +588,6 @@ void DocumentSourceLookUp::copyLetVariablesWithNewExpCtx(const std::vector<LetVa
|
||||
}
|
||||
}
|
||||
|
||||
void validateLookupCollectionlessPipeline(const std::vector<BSONObj>& pipeline) {
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
"$lookup stage without explicit collection must have a pipeline with $documents as "
|
||||
"first stage",
|
||||
pipeline.size() > 0 &&
|
||||
!pipeline[0].getField(DocumentSourceDocuments::kStageName).eoo());
|
||||
}
|
||||
|
||||
void validateLookupCollectionlessPipeline(const BSONElement& pipeline) {
|
||||
uassert(ErrorCodes::FailedToParse, "must specify 'pipeline' when 'from' is empty", pipeline);
|
||||
auto parsedPipeline = parsePipelineFromBSON(pipeline);
|
||||
validateLookupCollectionlessPipeline(parsedPipeline);
|
||||
}
|
||||
|
||||
std::unique_ptr<DocumentSourceLookUp::LiteParsed> DocumentSourceLookUp::LiteParsed::parse(
|
||||
const NamespaceString& nss, const BSONElement& spec, const LiteParserOptions& options) {
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
str::stream() << "the $lookup stage specification must be an object, but found "
|
||||
<< typeName(spec.type()),
|
||||
spec.type() == BSONType::object);
|
||||
|
||||
auto specObj = spec.Obj();
|
||||
auto fromElement = specObj["from"];
|
||||
auto pipelineElem = specObj["pipeline"];
|
||||
NamespaceString fromNss;
|
||||
if (!fromElement) {
|
||||
validateLookupCollectionlessPipeline(pipelineElem);
|
||||
fromNss = NamespaceString::makeCollectionlessAggregateNSS(nss.dbName());
|
||||
} else {
|
||||
fromNss = parseLookupFromAndResolveNamespace(
|
||||
fromElement, nss.dbName(), options.allowGenericForeignDbLookup);
|
||||
}
|
||||
uassert(ErrorCodes::InvalidNamespace,
|
||||
str::stream() << "invalid $lookup namespace: " << fromNss.toStringForErrorMsg(),
|
||||
fromNss.isValid());
|
||||
|
||||
// Recursively lite parse the nested pipeline, if one exists.
|
||||
boost::optional<LiteParsedPipeline> liteParsedPipeline;
|
||||
if (pipelineElem) {
|
||||
auto pipeline = parsePipelineFromBSON(pipelineElem);
|
||||
auto optsCopy = options;
|
||||
optsCopy.makeSubpipelineOwned = true;
|
||||
liteParsedPipeline = LiteParsedPipeline(fromNss, pipeline, false, optsCopy);
|
||||
}
|
||||
|
||||
return std::make_unique<DocumentSourceLookUp::LiteParsed>(
|
||||
spec, std::move(fromNss), std::move(liteParsedPipeline));
|
||||
}
|
||||
|
||||
PrivilegeVector DocumentSourceLookUp::LiteParsed::requiredPrivileges(
|
||||
bool isMongos, bool bypassDocumentValidation) const {
|
||||
PrivilegeVector requiredPrivileges;
|
||||
tassert(11282983,
|
||||
str::stream() << "$lookup only supports 1 subpipeline, got " << _pipelines.size(),
|
||||
_pipelines.size() <= 1);
|
||||
tassert(11282982, "Missing foreignNss", _foreignNss);
|
||||
|
||||
// If no pipeline is specified or the local/foreignField syntax was used, then assume that we're
|
||||
// reading directly from the collection.
|
||||
if (_pipelines.empty() || !_pipelines[0].startsWithInitialSource()) {
|
||||
Privilege::addPrivilegeToPrivilegeVector(
|
||||
&requiredPrivileges,
|
||||
Privilege(ResourcePattern::forExactNamespace(*_foreignNss), ActionType::find));
|
||||
}
|
||||
|
||||
// Add the sub-pipeline privileges, if one was specified.
|
||||
if (!_pipelines.empty()) {
|
||||
const LiteParsedPipeline& pipeline = _pipelines[0];
|
||||
Privilege::addPrivilegesToPrivilegeVector(
|
||||
&requiredPrivileges, pipeline.requiredPrivileges(isMongos, bypassDocumentValidation));
|
||||
}
|
||||
|
||||
return requiredPrivileges;
|
||||
}
|
||||
|
||||
REGISTER_LITE_PARSED_DOCUMENT_SOURCE(lookup,
|
||||
DocumentSourceLookUp::LiteParsed::parse,
|
||||
AllowedWithApiStrict::kConditionally);
|
||||
|
||||
REGISTER_DOCUMENT_SOURCE_WITH_STAGE_PARAMS_DEFAULT(lookup, DocumentSourceLookUp, LookUpStageParams);
|
||||
|
||||
ALLOCATE_DOCUMENT_SOURCE_ID(lookup, DocumentSourceLookUp::id)
|
||||
|
||||
const char* DocumentSourceLookUp::getSourceName() const {
|
||||
@ -813,6 +875,27 @@ BSONObj DocumentSourceLookUp::makeMatchStageFromInput(const Document& input,
|
||||
return match.obj();
|
||||
}
|
||||
|
||||
void DocumentSourceLookUp::parseAndDefineLetVariables(
|
||||
const BSONObj& letVariables, const boost::intrusive_ptr<ExpressionContext>& expCtx) {
|
||||
for (auto&& varElem : letVariables) {
|
||||
const auto varName = varElem.fieldNameStringData();
|
||||
variableValidation::validateNameForUserWrite(varName);
|
||||
|
||||
_letVariables.emplace_back(
|
||||
std::string{varName},
|
||||
Expression::parseOperand(expCtx.get(), varElem, expCtx->variablesParseState),
|
||||
_variablesParseState.defineVariable(varName));
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentSourceLookUp::insertFieldMatchPlaceholder() {
|
||||
if (_fieldMatchPipelineIdx) {
|
||||
_sharedState->resolvedPipeline.insert(_sharedState->resolvedPipeline.begin() +
|
||||
*_fieldMatchPipelineIdx,
|
||||
BSON("$match" << BSONObj()));
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentSourceLookUp::initializeResolvedIntrospectionPipeline() {
|
||||
_variables.copyToExpCtx(_variablesParseState, _fromExpCtx.get());
|
||||
_fromExpCtx->startExpressionCounters();
|
||||
@ -1160,7 +1243,7 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceLookUp::createFromBson(
|
||||
}
|
||||
|
||||
if (fromNs.isEmpty()) {
|
||||
validateLookupCollectionlessPipeline(pipeline);
|
||||
LiteParsedLookUp::validateLookupCollectionlessPipeline(pipeline);
|
||||
fromNs =
|
||||
NamespaceString::makeCollectionlessAggregateNSS(pExpCtx->getNamespaceString().dbName());
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
#include "mongo/bson/bsonelement.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/db/auth/privilege.h"
|
||||
#include "mongo/db/database_name.h"
|
||||
#include "mongo/db/exec/agg/exec_pipeline.h"
|
||||
#include "mongo/db/exec/document_value/document.h"
|
||||
#include "mongo/db/exec/document_value/value.h"
|
||||
@ -47,6 +48,7 @@
|
||||
#include "mongo/db/pipeline/field_path.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_document_source.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_document_source_nested_pipelines.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_lookup.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_pipeline.h"
|
||||
#include "mongo/db/pipeline/pipeline.h"
|
||||
#include "mongo/db/pipeline/stage_constraints.h"
|
||||
@ -92,7 +94,14 @@ struct LookUpSharedState {
|
||||
|
||||
void lookupPipeValidator(const Pipeline& pipeline);
|
||||
|
||||
DECLARE_STAGE_PARAMS_DERIVED_DEFAULT(LookUp);
|
||||
// Parses $lookup's 'from' field. Accepts a string or a '{db, coll}' object with specific
|
||||
// exceptions. `usingMongos` and `isParsingViewDefinition` tighten validation; lite-parse omits
|
||||
// them because expCtx is unavailable.
|
||||
NamespaceString parseLookupFromAndResolveNamespace(const BSONElement& elem,
|
||||
const DatabaseName& defaultDb,
|
||||
bool allowGenericForeignDbLookup,
|
||||
bool usingMongos = false,
|
||||
bool isParsingViewDefinition = false);
|
||||
|
||||
/**
|
||||
* Queries separate collection for equality matches with documents in the pipeline collection.
|
||||
@ -107,59 +116,6 @@ public:
|
||||
static constexpr StringData kPipelineField = "pipeline"_sd;
|
||||
static constexpr StringData kAsField = "as"_sd;
|
||||
|
||||
class LiteParsed final : public LiteParsedDocumentSourceNestedPipelines<LiteParsed> {
|
||||
public:
|
||||
static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss,
|
||||
const BSONElement& spec,
|
||||
const LiteParserOptions& options);
|
||||
|
||||
LiteParsed(const BSONElement& spec,
|
||||
NamespaceString foreignNss,
|
||||
boost::optional<LiteParsedPipeline> pipeline)
|
||||
: LiteParsedDocumentSourceNestedPipelines(
|
||||
spec, std::move(foreignNss), std::move(pipeline)) {}
|
||||
|
||||
/**
|
||||
* Lookup from a sharded collection may not be allowed.
|
||||
*/
|
||||
Status checkShardedForeignCollAllowed(const NamespaceString& nss,
|
||||
bool inMultiDocumentTransaction) const final {
|
||||
const auto fcvSnapshot = serverGlobalParams.mutableFCV.acquireFCVSnapshot();
|
||||
if (!inMultiDocumentTransaction ||
|
||||
gFeatureFlagAllowAdditionalParticipants.isEnabled(fcvSnapshot)) {
|
||||
return Status::OK();
|
||||
}
|
||||
auto involvedNss = getInvolvedNamespaces();
|
||||
if (involvedNss.find(nss) == involvedNss.end()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
return Status(ErrorCodes::NamespaceCannotBeSharded,
|
||||
"Sharded $lookup is not allowed within a multi-document transaction");
|
||||
}
|
||||
|
||||
void getForeignExecutionNamespaces(
|
||||
stdx::unordered_set<NamespaceString>& nssSet) const final {
|
||||
tassert(6235100, "Expected foreignNss to be initialized for $lookup", _foreignNss);
|
||||
nssSet.emplace(*_foreignNss);
|
||||
}
|
||||
|
||||
PrivilegeVector requiredPrivileges(bool isMongos,
|
||||
bool bypassDocumentValidation) const final;
|
||||
|
||||
bool requiresAuthzChecks() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasExtensionSearchStage() const override {
|
||||
return !_pipelines.empty() && _pipelines[0].hasExtensionSearchStage();
|
||||
}
|
||||
|
||||
std::unique_ptr<StageParams> getStageParams() const override {
|
||||
return std::make_unique<LookUpStageParams>(_originalBson);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy constructor used for clone().
|
||||
*/
|
||||
@ -206,6 +162,33 @@ public:
|
||||
static boost::intrusive_ptr<DocumentSource> createFromBson(
|
||||
BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx);
|
||||
|
||||
// Build a $lookup from pre-parsed StageParams. Performs expCtx-dependent validation
|
||||
// (cross-db on mongos / view definition, hybrid-search timeseries) and forwards the
|
||||
// desugared LPP into the LPP-accepting constructor when a subpipeline is present.
|
||||
static DocumentSourceContainer createFromStageParams(
|
||||
LookUpStageParams& params, const boost::intrusive_ptr<ExpressionContext>& expCtx);
|
||||
|
||||
/**
|
||||
* Constructor accepting a pre-desugared LiteParsedPipeline. Avoids the per-construction
|
||||
* re-parse of the subpipeline's BSON that createFromBson does. Used by createFromStageParams
|
||||
* when LookUpStageParams::liteParsedPipeline is present.
|
||||
*/
|
||||
DocumentSourceLookUp(NamespaceString fromNs,
|
||||
std::string as,
|
||||
std::vector<BSONObj> userPipeline,
|
||||
LiteParsedPipeline desugaredPipeline,
|
||||
BSONObj letVariables,
|
||||
boost::optional<std::pair<std::string, std::string>> localForeignFields,
|
||||
boost::optional<BSONObj> unwindSpec,
|
||||
const boost::intrusive_ptr<ExpressionContext>& pExpCtx);
|
||||
|
||||
static std::unique_ptr<Pipeline> parsePipelineFromLPPWithMaybeViewDefinition(
|
||||
const boost::intrusive_ptr<ExpressionContext>& expCtx,
|
||||
const ResolvedNamespace& resolvedNs,
|
||||
LiteParsedPipeline& desugaredPipeline,
|
||||
const std::vector<BSONObj>& rawPipeline,
|
||||
const NamespaceString& userNss);
|
||||
|
||||
void resolvedPipelineHelper(
|
||||
NamespaceString fromNs,
|
||||
std::vector<BSONObj> pipeline,
|
||||
@ -279,6 +262,10 @@ public:
|
||||
return _variablesParseState;
|
||||
}
|
||||
|
||||
inline const std::vector<BSONObj>& getResolvedPipelineForTest() const {
|
||||
return _sharedState->resolvedPipeline;
|
||||
}
|
||||
|
||||
const DocumentSourceContainer* getSubPipeline() const final {
|
||||
tassert(6080015,
|
||||
"$lookup expected to have a resolved pipeline, but didn't",
|
||||
@ -396,6 +383,19 @@ private:
|
||||
*/
|
||||
void initializeResolvedIntrospectionPipeline();
|
||||
|
||||
/**
|
||||
* Validates each name in 'letVariables' and appends a corresponding entry to '_letVariables'
|
||||
* (parsed expression + variable id from '_variablesParseState').
|
||||
*/
|
||||
void parseAndDefineLetVariables(const BSONObj& letVariables,
|
||||
const boost::intrusive_ptr<ExpressionContext>& expCtx);
|
||||
|
||||
/**
|
||||
* If '_fieldMatchPipelineIdx' is set, inserts an empty $match placeholder into
|
||||
* '_sharedState->resolvedPipeline' at that index.
|
||||
*/
|
||||
void insertFieldMatchPlaceholder();
|
||||
|
||||
/**
|
||||
* Given a mutable document, appends execution stats such as 'totalDocsExamined',
|
||||
* 'totalKeysExamined', 'collectionScans', 'indexesUsed', etc. to it.
|
||||
|
||||
@ -263,7 +263,7 @@ TEST_F(DocumentSourceLookUpTest, LiteParsedDocumentSourceLookupContainsExpectedN
|
||||
auto expCtx = getExpCtx();
|
||||
|
||||
std::vector<BSONObj> pipeline;
|
||||
auto liteParsedLookup = DocumentSourceLookUp::LiteParsed::parse(
|
||||
auto liteParsedLookup = LiteParsedLookUp::parse(
|
||||
expCtx->getNamespaceString(), stageSpec.firstElement(), LiteParserOptions{});
|
||||
auto namespaceSet = liteParsedLookup->getInvolvedNamespaces();
|
||||
|
||||
@ -1066,7 +1066,7 @@ TEST_F(DocumentSourceLookUpTest, AllowsPipelineFromDBAndCollWithContextFlag) {
|
||||
{
|
||||
// Lite parsing
|
||||
LiteParserOptions options{.allowGenericForeignDbLookup = true};
|
||||
auto liteParsedLookup = DocumentSourceLookUp::LiteParsed::parse(
|
||||
auto liteParsedLookup = LiteParsedLookUp::parse(
|
||||
expCtx->getNamespaceString(), stageSpec.firstElement(), options);
|
||||
auto namespaceSet = liteParsedLookup->getInvolvedNamespaces();
|
||||
|
||||
|
||||
@ -272,6 +272,11 @@ DocumentSourceUnionWith::DocumentSourceUnionWith(
|
||||
bool hasForeignDB)
|
||||
: DocumentSource(kStageName, expCtx) {
|
||||
_hasForeignDB = hasForeignDB;
|
||||
// TODO SERVER-121094 Remove when feature flag is removed.
|
||||
auto ifrContext = expCtx->getIfrContext();
|
||||
if (!ifrContext->getSavedFlagValue(feature_flags::gFeatureFlagExtensionsInsideHybridSearch)) {
|
||||
LiteParsedDesugarer::desugar(&desugaredPipeline, expCtx->getIfrContext());
|
||||
}
|
||||
boost::optional<ResolvedNamespace> resolvedUnionNs;
|
||||
try {
|
||||
auto resolvedNamespaces = expCtx->getResolvedNamespaces();
|
||||
@ -694,15 +699,10 @@ std::unique_ptr<Pipeline> DocumentSourceUnionWith::parsePipelineFromLPPWithMaybe
|
||||
LiteParsedPipeline& desugaredPipeline,
|
||||
const std::vector<BSONObj>& rawPipeline,
|
||||
const NamespaceString& userNss) {
|
||||
|
||||
boost::intrusive_ptr<ExpressionContext> subExpCtx = makeCopyForSubPipelineFromExpressionContext(
|
||||
expCtx, resolvedNs.ns, resolvedNs.uuid, userNss);
|
||||
subExpCtx->setInUnionWith(true);
|
||||
|
||||
// TODO SERVER-117882 Remove this call once $lookup forwards its desugared LPP subpipeline in
|
||||
// StageParams.
|
||||
LiteParsedDesugarer::desugar(&desugaredPipeline, expCtx->getIfrContext());
|
||||
|
||||
if (resolvedNs.ns.isTimeseriesBucketsCollection() &&
|
||||
isRawDataOperation(expCtx->getOperationContext())) {
|
||||
// Raw Data operations on timeseries collections operate without the timeseries view.
|
||||
|
||||
232
src/mongo/db/pipeline/lite_parsed_lookup.cpp
Normal file
232
src/mongo/db/pipeline/lite_parsed_lookup.cpp
Normal file
@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Copyright (C) 2026-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/lite_parsed_lookup.h"
|
||||
|
||||
#include "mongo/db/pipeline/aggregation_request_helper.h"
|
||||
#include "mongo/db/pipeline/document_source_documents.h"
|
||||
#include "mongo/db/pipeline/document_source_lookup.h" // parseLookupFromAndResolveNamespace
|
||||
#include "mongo/db/pipeline/document_source_lookup_gen.h" // DocumentSourceLookupSpec IDL
|
||||
#include "mongo/db/pipeline/document_source_queue.h"
|
||||
#include "mongo/db/pipeline/stage_params_to_document_source_registry.h"
|
||||
#include "mongo/db/query/allowed_contexts.h"
|
||||
#include "mongo/db/query/query_feature_flags_gen.h"
|
||||
#include "mongo/db/server_options.h"
|
||||
#include "mongo/idl/idl_parser.h"
|
||||
#include "mongo/logv2/log.h"
|
||||
#include "mongo/util/str.h"
|
||||
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
|
||||
|
||||
namespace mongo {
|
||||
|
||||
REGISTER_LITE_PARSED_DOCUMENT_SOURCE(lookup,
|
||||
LiteParsedLookUp::parse,
|
||||
AllowedWithApiStrict::kConditionally);
|
||||
|
||||
LiteParsedLookUp::LiteParsedLookUp(const BSONElement& spec,
|
||||
NamespaceString foreignNss,
|
||||
boost::optional<LiteParsedPipeline> pipeline,
|
||||
std::vector<BSONObj> rawPipeline,
|
||||
std::string as,
|
||||
BSONObj letVariables,
|
||||
boost::optional<std::string> localField,
|
||||
boost::optional<std::string> foreignField,
|
||||
boost::optional<BSONObj> unwindSpec,
|
||||
bool hasForeignDB,
|
||||
bool isHybridSearch)
|
||||
: LiteParsedDocumentSourceNestedPipelines(spec, std::move(foreignNss), std::move(pipeline)),
|
||||
_rawPipeline(std::move(rawPipeline)),
|
||||
_as(std::move(as)),
|
||||
_letVariables(std::move(letVariables)),
|
||||
_localField(std::move(localField)),
|
||||
_foreignField(std::move(foreignField)),
|
||||
_unwindSpec(std::move(unwindSpec)),
|
||||
_hasForeignDB(hasForeignDB),
|
||||
_isHybridSearch(isHybridSearch) {}
|
||||
|
||||
std::unique_ptr<LiteParsedLookUp> LiteParsedLookUp::parse(const NamespaceString& nss,
|
||||
const BSONElement& spec,
|
||||
const LiteParserOptions& options) {
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
str::stream() << "the $lookup stage specification must be an object, but found "
|
||||
<< typeName(spec.type()),
|
||||
spec.type() == BSONType::object);
|
||||
|
||||
auto specObj = spec.Obj();
|
||||
auto lookupSpec = DocumentSourceLookupSpec::parse(specObj, IDLParserContext(kStageName));
|
||||
|
||||
NamespaceString fromNss;
|
||||
bool hasForeignDB = false;
|
||||
if (lookupSpec.getFrom().has_value()) {
|
||||
auto fromElem = lookupSpec.getFrom().value().getElement();
|
||||
fromNss = parseLookupFromAndResolveNamespace(
|
||||
fromElem, nss.dbName(), options.allowGenericForeignDbLookup);
|
||||
if (fromElem.type() == BSONType::object) {
|
||||
auto specAsNs = NamespaceSpec::parse(fromElem.embeddedObject(),
|
||||
IDLParserContext{fromElem.fieldNameStringData()});
|
||||
hasForeignDB = specAsNs.getDb().has_value();
|
||||
}
|
||||
} else {
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
"must specify 'pipeline' when 'from' is empty",
|
||||
lookupSpec.getPipeline().has_value());
|
||||
validateLookupCollectionlessPipeline(lookupSpec.getPipeline().value());
|
||||
fromNss = NamespaceString::makeCollectionlessAggregateNSS(nss.dbName());
|
||||
}
|
||||
uassert(ErrorCodes::InvalidNamespace,
|
||||
str::stream() << "invalid $lookup namespace: " << fromNss.toStringForErrorMsg(),
|
||||
fromNss.isValid());
|
||||
|
||||
boost::optional<LiteParsedPipeline> liteParsedPipeline;
|
||||
std::vector<BSONObj> rawPipeline;
|
||||
if (lookupSpec.getPipeline().has_value()) {
|
||||
rawPipeline = lookupSpec.getPipeline().value();
|
||||
auto optsCopy = options;
|
||||
optsCopy.makeSubpipelineOwned = true;
|
||||
liteParsedPipeline = LiteParsedPipeline(fromNss, rawPipeline, false, optsCopy);
|
||||
}
|
||||
|
||||
std::string as = std::string{lookupSpec.getAs()};
|
||||
BSONObj letVariables =
|
||||
lookupSpec.getLetVars().has_value() ? lookupSpec.getLetVars()->getOwned() : BSONObj();
|
||||
|
||||
boost::optional<std::string> localField;
|
||||
boost::optional<std::string> foreignField;
|
||||
if (lookupSpec.getLocalField().has_value()) {
|
||||
localField = std::string{*lookupSpec.getLocalField()};
|
||||
}
|
||||
if (lookupSpec.getForeignField().has_value()) {
|
||||
foreignField = std::string{*lookupSpec.getForeignField()};
|
||||
}
|
||||
|
||||
boost::optional<BSONObj> unwindSpec;
|
||||
if (lookupSpec.getUnwindSpec().has_value()) {
|
||||
unwindSpec = lookupSpec.getUnwindSpec()->getOwned();
|
||||
}
|
||||
|
||||
const bool isHybridSearch = lookupSpec.getIsHybridSearch().value_or(false);
|
||||
|
||||
return std::make_unique<LiteParsedLookUp>(spec,
|
||||
std::move(fromNss),
|
||||
std::move(liteParsedPipeline),
|
||||
std::move(rawPipeline),
|
||||
std::move(as),
|
||||
std::move(letVariables),
|
||||
std::move(localField),
|
||||
std::move(foreignField),
|
||||
std::move(unwindSpec),
|
||||
hasForeignDB,
|
||||
isHybridSearch);
|
||||
}
|
||||
|
||||
PrivilegeVector LiteParsedLookUp::requiredPrivileges(bool isMongos,
|
||||
bool bypassDocumentValidation) const {
|
||||
PrivilegeVector requiredPrivileges;
|
||||
tassert(11282983,
|
||||
str::stream() << "$lookup only supports 1 subpipeline, got " << _pipelines.size(),
|
||||
_pipelines.size() <= 1);
|
||||
tassert(11282982, "Missing foreignNss", _foreignNss);
|
||||
|
||||
if (_pipelines.empty() || !_pipelines[0].startsWithInitialSource()) {
|
||||
Privilege::addPrivilegeToPrivilegeVector(
|
||||
&requiredPrivileges,
|
||||
Privilege(ResourcePattern::forExactNamespace(*_foreignNss), ActionType::find));
|
||||
}
|
||||
|
||||
if (!_pipelines.empty()) {
|
||||
const LiteParsedPipeline& pipeline = _pipelines[0];
|
||||
Privilege::addPrivilegesToPrivilegeVector(
|
||||
&requiredPrivileges, pipeline.requiredPrivileges(isMongos, bypassDocumentValidation));
|
||||
}
|
||||
|
||||
return requiredPrivileges;
|
||||
}
|
||||
|
||||
Status LiteParsedLookUp::checkShardedForeignCollAllowed(const NamespaceString& nss,
|
||||
bool inMultiDocumentTransaction) const {
|
||||
const auto fcvSnapshot = serverGlobalParams.mutableFCV.acquireFCVSnapshot();
|
||||
if (!inMultiDocumentTransaction ||
|
||||
gFeatureFlagAllowAdditionalParticipants.isEnabled(fcvSnapshot)) {
|
||||
return Status::OK();
|
||||
}
|
||||
auto involvedNss = getInvolvedNamespaces();
|
||||
if (involvedNss.find(nss) == involvedNss.end()) {
|
||||
return Status::OK();
|
||||
}
|
||||
return Status(ErrorCodes::NamespaceCannotBeSharded,
|
||||
"Sharded $lookup is not allowed within a multi-document transaction");
|
||||
}
|
||||
|
||||
void LiteParsedLookUp::getForeignExecutionNamespaces(
|
||||
stdx::unordered_set<NamespaceString>& nssSet) const {
|
||||
tassert(6235100, "Expected foreignNss to be initialized for $lookup", _foreignNss);
|
||||
nssSet.emplace(*_foreignNss);
|
||||
}
|
||||
|
||||
bool LiteParsedLookUp::hasExtensionSearchStage() const {
|
||||
return !_pipelines.empty() && _pipelines[0].hasExtensionSearchStage();
|
||||
}
|
||||
|
||||
std::unique_ptr<StageParams> LiteParsedLookUp::getStageParams() const {
|
||||
boost::optional<LiteParsedPipeline> lpp;
|
||||
if (!_pipelines.empty()) {
|
||||
lpp = _pipelines[0].clone();
|
||||
}
|
||||
return std::make_unique<LookUpStageParams>(*_foreignNss,
|
||||
_as,
|
||||
_rawPipeline,
|
||||
_letVariables,
|
||||
_localField,
|
||||
_foreignField,
|
||||
_unwindSpec,
|
||||
_hasForeignDB,
|
||||
_isHybridSearch,
|
||||
getOriginalBson(),
|
||||
std::move(lpp));
|
||||
}
|
||||
|
||||
void LiteParsedLookUp::validateLookupCollectionlessPipeline(const std::vector<BSONObj>& pipeline) {
|
||||
uassert(ErrorCodes::FailedToParse,
|
||||
"$lookup stage without explicit collection must have a pipeline with $documents as "
|
||||
"first stage",
|
||||
pipeline.size() > 0 &&
|
||||
!pipeline[0].getField(DocumentSourceDocuments::kStageName).eoo());
|
||||
}
|
||||
|
||||
void LiteParsedLookUp::validateLookupCollectionlessPipeline(const BSONElement& pipeline) {
|
||||
uassert(ErrorCodes::FailedToParse, "must specify 'pipeline' when 'from' is empty", pipeline);
|
||||
auto parsedPipeline = parsePipelineFromBSON(pipeline);
|
||||
validateLookupCollectionlessPipeline(parsedPipeline);
|
||||
}
|
||||
|
||||
} // namespace mongo
|
||||
153
src/mongo/db/pipeline/lite_parsed_lookup.h
Normal file
153
src/mongo/db/pipeline/lite_parsed_lookup.h
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Copyright (C) 2026-present MongoDB, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* As a special exception, the copyright holders give permission to link the
|
||||
* code of portions of this program with the OpenSSL library under certain
|
||||
* conditions as described in each individual source file and distribute
|
||||
* linked combinations including the program with the OpenSSL library. You
|
||||
* must comply with the Server Side Public License in all respects for
|
||||
* all of the code used other than as permitted herein. If you modify file(s)
|
||||
* with this exception, you may extend this exception to your version of the
|
||||
* file(s), but you are not obligated to do so. If you do not wish to do so,
|
||||
* delete this exception statement from your version. If you delete this
|
||||
* exception statement from all source files in the program, then also delete
|
||||
* it in the license file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/bson/bsonelement.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/db/auth/privilege.h"
|
||||
#include "mongo/db/namespace_string.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_document_source.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_document_source_nested_pipelines.h"
|
||||
#include "mongo/db/pipeline/lite_parsed_pipeline.h"
|
||||
#include "mongo/db/pipeline/stage_params.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/none.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace MONGO_MOD_NEEDS_REPLACEMENT mongo {
|
||||
|
||||
class LookUpStageParams : public DefaultStageParams {
|
||||
public:
|
||||
LookUpStageParams(NamespaceString fromNss,
|
||||
std::string as,
|
||||
std::vector<BSONObj> pipeline,
|
||||
BSONObj letVariables,
|
||||
boost::optional<std::string> localField,
|
||||
boost::optional<std::string> foreignField,
|
||||
boost::optional<BSONObj> unwindSpec,
|
||||
bool hasForeignDB,
|
||||
bool isHybridSearch,
|
||||
// TODO SERVER-121262 Have the StageParams be owner of the BSONObj instead.
|
||||
BSONElement originalBson,
|
||||
boost::optional<LiteParsedPipeline> liteParsedPipeline = boost::none)
|
||||
: DefaultStageParams(originalBson),
|
||||
fromNss(std::move(fromNss)),
|
||||
as(std::move(as)),
|
||||
pipeline(std::move(pipeline)),
|
||||
letVariables(std::move(letVariables)),
|
||||
localField(std::move(localField)),
|
||||
foreignField(std::move(foreignField)),
|
||||
unwindSpec(std::move(unwindSpec)),
|
||||
hasForeignDB(hasForeignDB),
|
||||
isHybridSearch(isHybridSearch),
|
||||
liteParsedPipeline(std::move(liteParsedPipeline)) {}
|
||||
|
||||
static const Id& id;
|
||||
Id getId() const final {
|
||||
return id;
|
||||
}
|
||||
|
||||
NamespaceString fromNss;
|
||||
std::string as;
|
||||
std::vector<BSONObj> pipeline;
|
||||
BSONObj letVariables;
|
||||
boost::optional<std::string> localField;
|
||||
boost::optional<std::string> foreignField;
|
||||
boost::optional<BSONObj> unwindSpec;
|
||||
bool hasForeignDB;
|
||||
|
||||
// TODO SERVER-121091 This can be removed once hybrid search desugars into the internal hybrid
|
||||
// search stage.
|
||||
bool isHybridSearch;
|
||||
|
||||
// The desugared LiteParsedPipeline for the subpipeline. Absent when $lookup is used without a
|
||||
// 'pipeline' field (local/foreignField-only form).
|
||||
boost::optional<LiteParsedPipeline> liteParsedPipeline;
|
||||
};
|
||||
|
||||
class LiteParsedLookUp final : public LiteParsedDocumentSourceNestedPipelines<LiteParsedLookUp> {
|
||||
public:
|
||||
static constexpr StringData kStageName = "$lookup"_sd;
|
||||
|
||||
static std::unique_ptr<LiteParsedLookUp> parse(const NamespaceString& nss,
|
||||
const BSONElement& spec,
|
||||
const LiteParserOptions& options);
|
||||
|
||||
LiteParsedLookUp(const BSONElement& spec,
|
||||
NamespaceString foreignNss,
|
||||
boost::optional<LiteParsedPipeline> pipeline,
|
||||
std::vector<BSONObj> rawPipeline,
|
||||
std::string as,
|
||||
BSONObj letVariables,
|
||||
boost::optional<std::string> localField,
|
||||
boost::optional<std::string> foreignField,
|
||||
boost::optional<BSONObj> unwindSpec,
|
||||
bool hasForeignDB,
|
||||
bool isHybridSearch);
|
||||
|
||||
Status checkShardedForeignCollAllowed(const NamespaceString& nss,
|
||||
bool inMultiDocumentTransaction) const final;
|
||||
|
||||
void getForeignExecutionNamespaces(stdx::unordered_set<NamespaceString>& nssSet) const final;
|
||||
|
||||
PrivilegeVector requiredPrivileges(bool isMongos, bool bypassDocumentValidation) const final;
|
||||
|
||||
bool requiresAuthzChecks() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasExtensionSearchStage() const override;
|
||||
|
||||
std::unique_ptr<StageParams> getStageParams() const override;
|
||||
|
||||
// Moved from document_source_lookup.cpp so createFromBson can still call it.
|
||||
static void validateLookupCollectionlessPipeline(const std::vector<BSONObj>& pipeline);
|
||||
static void validateLookupCollectionlessPipeline(const BSONElement& pipelineElem);
|
||||
|
||||
private:
|
||||
std::vector<BSONObj> _rawPipeline;
|
||||
std::string _as;
|
||||
BSONObj _letVariables;
|
||||
boost::optional<std::string> _localField;
|
||||
boost::optional<std::string> _foreignField;
|
||||
boost::optional<BSONObj> _unwindSpec;
|
||||
bool _hasForeignDB;
|
||||
bool _isHybridSearch;
|
||||
};
|
||||
|
||||
} // namespace MONGO_MOD_NEEDS_REPLACEMENT mongo
|
||||
150
src/mongo/db/pipeline/lite_parsed_lookup_test.cpp
Normal file
150
src/mongo/db/pipeline/lite_parsed_lookup_test.cpp
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (C) 2026-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/lite_parsed_lookup.h"
|
||||
|
||||
#include "mongo/bson/json.h"
|
||||
#include "mongo/db/pipeline/aggregation_context_fixture.h"
|
||||
#include "mongo/unittest/unittest.h"
|
||||
|
||||
namespace mongo {
|
||||
namespace {
|
||||
|
||||
class LiteParsedLookUpTest : public AggregationContextFixture {
|
||||
protected:
|
||||
const NamespaceString nss = NamespaceString::createNamespaceString_forTest("test.local");
|
||||
|
||||
std::unique_ptr<LiteParsedLookUp> parse(StringData json,
|
||||
LiteParserOptions options = LiteParserOptions{}) {
|
||||
auto spec = fromjson(json);
|
||||
return LiteParsedLookUp::parse(nss, spec.firstElement(), options);
|
||||
}
|
||||
|
||||
LookUpStageParams* parseAndGetParams(StringData json,
|
||||
LiteParserOptions options = LiteParserOptions{}) {
|
||||
_lastParsed = parse(json, options);
|
||||
_lastParams = _lastParsed->getStageParams();
|
||||
auto* typed = dynamic_cast<LookUpStageParams*>(_lastParams.get());
|
||||
ASSERT_TRUE(typed);
|
||||
return typed;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<LiteParsedLookUp> _lastParsed;
|
||||
std::unique_ptr<StageParams> _lastParams;
|
||||
};
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, ParsesPipelineOnlyForm) {
|
||||
auto lp = parse(R"({$lookup: {from: "foreign", as: "a", pipeline: [{$match: {x: 1}}]}})");
|
||||
auto involved = lp->getInvolvedNamespaces();
|
||||
ASSERT_EQ(involved.size(), 1u);
|
||||
ASSERT_TRUE(involved.contains(NamespaceString::createNamespaceString_forTest("test.foreign")));
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, ParsesLocalForeignFieldForm) {
|
||||
auto lp = parse(R"({$lookup: {from: "foreign", as: "a", localField: "x", foreignField: "y"}})");
|
||||
ASSERT_EQ(lp->getInvolvedNamespaces().size(), 1u);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, ParsesCrossDbForm) {
|
||||
LiteParserOptions opts;
|
||||
opts.allowGenericForeignDbLookup = true;
|
||||
auto* typed = parseAndGetParams(
|
||||
R"({$lookup: {from: {db: "other", coll: "c"}, as: "a", pipeline: []}})", opts);
|
||||
ASSERT_TRUE(typed->hasForeignDB);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, ParsesCollectionlessForm) {
|
||||
auto lp = parse(R"({$lookup: {as: "a", pipeline: [{$documents: [{}]}]}})");
|
||||
ASSERT_FALSE(lp->getInvolvedNamespaces().empty());
|
||||
ASSERT_TRUE(lp->getInvolvedNamespaces().begin()->isCollectionlessAggregateNS());
|
||||
}
|
||||
|
||||
// ---- Parse failures ----
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, RejectsNonObjectSpec) {
|
||||
ASSERT_THROWS_CODE(
|
||||
parse(R"({$lookup: "just-a-string"})"), AssertionException, ErrorCodes::FailedToParse);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, RejectsCollectionlessWithoutDocuments) {
|
||||
ASSERT_THROWS_CODE(parse(R"({$lookup: {as: "a", pipeline: [{$match: {}}]}})"),
|
||||
AssertionException,
|
||||
ErrorCodes::FailedToParse);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, StageParamsCarriesDesugaredPipelineWhenPresent) {
|
||||
auto* typed =
|
||||
parseAndGetParams(R"({$lookup: {from: "foreign", as: "a", pipeline: [{$match: {}}]}})");
|
||||
ASSERT_TRUE(typed->liteParsedPipeline.has_value());
|
||||
ASSERT_EQ(typed->pipeline.size(), 1u);
|
||||
ASSERT_EQ(typed->as, "a");
|
||||
ASSERT_FALSE(typed->localField.has_value());
|
||||
ASSERT_FALSE(typed->foreignField.has_value());
|
||||
ASSERT_FALSE(typed->hasForeignDB);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, StageParamsOmitsLppForLocalForeignFieldForm) {
|
||||
auto* typed = parseAndGetParams(
|
||||
R"({$lookup: {from: "foreign", as: "a", localField: "x", foreignField: "y"}})");
|
||||
ASSERT_FALSE(typed->liteParsedPipeline.has_value());
|
||||
ASSERT_TRUE(typed->localField.has_value());
|
||||
ASSERT_EQ(*typed->localField, "x");
|
||||
ASSERT_TRUE(typed->foreignField.has_value());
|
||||
ASSERT_EQ(*typed->foreignField, "y");
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, StageParamsForwardsIsHybridSearchFlag) {
|
||||
auto* typed = parseAndGetParams(R"({
|
||||
$lookup: {from: "foreign", as: "a", pipeline: [{$match: {}}], $_internalIsHybridSearch: true}
|
||||
})");
|
||||
ASSERT_TRUE(typed->isHybridSearch);
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, StageParamsCarriesLetVarsAndUnwindSpec) {
|
||||
auto* typed = parseAndGetParams(R"({
|
||||
$lookup: {
|
||||
from: "foreign", as: "a", pipeline: [{$match: {}}],
|
||||
let: {v: "$x", w: "$y"},
|
||||
$_internalUnwind: {$unwind: "$a"}
|
||||
}
|
||||
})");
|
||||
ASSERT_BSONOBJ_EQ(typed->letVariables, fromjson(R"({v: "$x", w: "$y"})"));
|
||||
ASSERT_TRUE(typed->unwindSpec.has_value());
|
||||
ASSERT_BSONOBJ_EQ(*typed->unwindSpec, fromjson(R"({$unwind: "$a"})"));
|
||||
}
|
||||
|
||||
TEST_F(LiteParsedLookUpTest, RequiredPrivilegesIncludesForeignFind) {
|
||||
auto lp = parse(R"({$lookup: {from: "foreign", as: "a", pipeline: []}})");
|
||||
auto privs = lp->requiredPrivileges(false /*isMongos*/, false /*bypass*/);
|
||||
ASSERT_EQ(privs.size(), 1u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mongo
|
||||
@ -561,7 +561,7 @@ std::unique_ptr<Pipeline> parsePipelineAndRegisterQueryStats(
|
||||
// optimize out the reparse.
|
||||
auto clonedLPP = liteParsedPipeline.clone();
|
||||
const auto wasDesugaredHere =
|
||||
!alreadyDesugared && LiteParsedDesugarer::desugar(&clonedLPP, ifrContext);
|
||||
!alreadyDesugared && LiteParsedDesugarer::desugar(&clonedLPP, expCtx->getIfrContext());
|
||||
|
||||
auto ifrCtx = expCtx->getIfrContext();
|
||||
auto hybridSearchFlagEnabled = ifrCtx &&
|
||||
|
||||
Loading…
Reference in New Issue
Block a user