SERVER-114508 Allow localField override in JOO (#44495)

GitOrigin-RevId: de696c3697c672467836ae046e6c70ef1e7e4432
This commit is contained in:
Alexander Ignatyev
2025-11-27 14:26:08 +00:00
committed by MongoDB Bot
parent 3c4569fd99
commit 9ea21d7909
4 changed files with 198 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
/**
* Verifies that we correcly process overrding local fields by foreign documents.
* @tags: [
* requires_fcv_83,
* ]
*/
import {assertArrayEq} from "jstests/aggregation/extras/utils.js";
const docs = [
{_id: "first", a: 1, b: 1},
{_id: "second", a: 1, b: 2},
];
const config = {
setParameter: {
internalEnableJoinOptimization: true,
},
};
const conn = MongoRunner.runMongod(config);
const db = conn.getDB(jsTestName());
db.coll.drop();
assert.commandWorked(db.coll.insertMany(docs));
const pipeline = [
{$lookup: {from: "coll", localField: "_id", foreignField: "_id", as: "_id"}},
{$unwind: "$_id"},
{$lookup: {from: "coll", localField: "a", foreignField: "b", as: "a"}},
{$unwind: "$a"},
{$lookup: {from: "coll", localField: "b", foreignField: "b", as: "b"}},
{$unwind: "$b"},
];
const actual = db.coll.aggregate(pipeline).toArray();
MongoRunner.stopMongod(conn);
const expected = [
{
"_id": {
"_id": "first",
"a": 1,
"b": 1,
},
"a": {
"_id": "first",
"a": 1,
"b": 1,
},
"b": {
"_id": "first",
"a": 1,
"b": 1,
},
},
{
"_id": {
"_id": "second",
"a": 1,
"b": 2,
},
"a": {
"_id": "first",
"a": 1,
"b": 1,
},
"b": {
"_id": "second",
"a": 1,
"b": 2,
},
},
];
assertArrayEq({actual, expected});

View File

@@ -192,13 +192,13 @@ StatusWith<AggJoinModel> AggJoinModel::constructJoinModel(const Pipeline& pipeli
return Status(ErrorCodes::BadValue, "Graph is too big: too many nodes");
}
pathResolver.addNode(*foreignNodeId, lookup->getAsField());
if (lookup->hasLocalFieldForeignFieldJoin()) {
// The order of resolving the paths are important here: localPathId shouln't be
// resolved to the foreign collection even if it is prefixed by the foreign
// collection's embedPath.
auto localPathId = pathResolver.resolve(*lookup->getLocalField());
pathResolver.addNode(*foreignNodeId, lookup->getAsField());
auto foreignPathId =
pathResolver.addPath(*foreignNodeId, *lookup->getForeignField());
@@ -208,6 +208,8 @@ StatusWith<AggJoinModel> AggJoinModel::constructJoinModel(const Pipeline& pipeli
// Cannot add an edge for existing nodes.
return Status(ErrorCodes::BadValue, "Graph is too big: too many edges");
}
} else {
pathResolver.addNode(*foreignNodeId, lookup->getAsField());
}
// TODO SERVER-111164: add edges from $expr's

View File

@@ -411,4 +411,23 @@ TEST_F(PipelineAnalyzerTest, LongPrefix) {
auto& joinModel = swJoinModel.getValue();
goldenCtx.outStream() << joinModel.toString(true) << std::endl;
}
TEST_F(PipelineAnalyzerTest, LocalFieldOverride) {
unittest::GoldenTestContext goldenCtx(&goldenTestConfig);
const auto query = R"([
{$lookup: {from: "A", localField: "a", foreignField: "b", as: "a"}},
{$unwind: "$a"},
{$lookup: {from: "B", localField: "b", foreignField: "b", as: "b"}},
{$unwind: "$b"}
])";
auto pipeline = makePipeline(query, {"A", "B"});
ASSERT_TRUE(AggJoinModel::pipelineEligibleForJoinReordering(*pipeline));
auto swJoinModel = AggJoinModel::constructJoinModel(*pipeline);
ASSERT_OK(swJoinModel);
auto& joinModel = swJoinModel.getValue();
goldenCtx.outStream() << joinModel.toString(true) << std::endl;
}
} // namespace mongo::join_ordering

View File

@@ -0,0 +1,98 @@
{
"graph": {
"nodes": [
{
"collectionName": "test.pipeline_test",
"accessPath": {
"filter": {}
},
"embedPath": ""
},
{
"collectionName": "test.A",
"accessPath": {
"filter": {}
},
"embedPath": "a"
},
{
"collectionName": "test.B",
"accessPath": {
"filter": {}
},
"embedPath": "b"
}
],
"edges": [
{
"predicates": [
{
"op": "eq",
"left": {"$numberInt":"0"},
"right": {"$numberInt":"1"}
}
],
"left": "0000000000000000000000000000000000000000000000000000000000000001",
"right": "0000000000000000000000000000000000000000000000000000000000000010"
},
{
"predicates": [
{
"op": "eq",
"left": {"$numberInt":"2"},
"right": {"$numberInt":"3"}
}
],
"left": "0000000000000000000000000000000000000000000000000000000000000001",
"right": "0000000000000000000000000000000000000000000000000000000000000100"
}
]
},
"resolvedPaths": [
{
"nodeId": {"$numberInt":"0"},
"fieldName": "a"
},
{
"nodeId": {"$numberInt":"1"},
"fieldName": "b"
},
{
"nodeId": {"$numberInt":"0"},
"fieldName": "b"
},
{
"nodeId": {"$numberInt":"2"},
"fieldName": "b"
}
],
"prefix": [
{
"$lookup": {
"from": "A",
"as": "a",
"localField": "a",
"foreignField": "b"
}
},
{
"$unwind": {
"path": "$a"
}
},
{
"$lookup": {
"from": "B",
"as": "b",
"localField": "b",
"foreignField": "b"
}
},
{
"$unwind": {
"path": "$b"
}
}
],
"suffix": []
}