From 0f75f4f08dec9b9ac301166a5a79b416246d87ec Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Tue, 21 Jul 2026 12:14:52 -0700 Subject: [PATCH 1/3] Expand correlated subquery testing for $lookup stage Co-authored-by: PatersonProjects Signed-off-by: Daniel Frankcom --- .../lookup/test_lookup_correlated_subquery.py | 140 +++++++++++++ .../test_lookup_correlated_system_vars.py | 194 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_system_vars.py diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py index fcb2d3d5b..e0ab025ed 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py @@ -55,6 +55,71 @@ " to the sub-pipeline via $$variableName syntax" ), ), + LookupTestCase( + "per_doc_variation_distinct_let_values", + foreign_docs=[ + {"_id": 10, "type": "A", "val": 1}, + {"_id": 11, "type": "B", "val": 2}, + {"_id": 12, "type": "B", "val": 3}, + ], + docs=[ + {"_id": 1, "cat": "A"}, + {"_id": 2, "cat": "B"}, + {"_id": 3, "cat": "C"}, + ], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"c": "$cat"}, + "pipeline": [{"$match": {"$expr": {"$eq": ["$type", "$$c"]}}}], + "as": "joined", + } + } + ], + expected=[ + {"_id": 1, "cat": "A", "joined": [{"_id": 10, "type": "A", "val": 1}]}, + { + "_id": 2, + "cat": "B", + "joined": [ + {"_id": 11, "type": "B", "val": 2}, + {"_id": 12, "type": "B", "val": 3}, + ], + }, + {"_id": 3, "cat": "C", "joined": []}, + ], + msg=( + "$lookup correlated join should produce different results per outer" + " document based on each document's let variable value" + ), + ), + LookupTestCase( + "per_doc_duplicate_let_values_same_result", + foreign_docs=[{"_id": 10, "type": "A"}], + docs=[ + {"_id": 1, "cat": "A"}, + {"_id": 2, "cat": "A"}, + ], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"c": "$cat"}, + "pipeline": [{"$match": {"$expr": {"$eq": ["$type", "$$c"]}}}], + "as": "joined", + } + } + ], + expected=[ + {"_id": 1, "cat": "A", "joined": [{"_id": 10, "type": "A"}]}, + {"_id": 2, "cat": "A", "joined": [{"_id": 10, "type": "A"}]}, + ], + msg=( + "$lookup correlated join with duplicate let values should produce" + " identical joined results for both outer documents" + ), + ), LookupTestCase( "let_variable_in_match_without_expr_is_literal_string", docs=[{"_id": 1, "val": "a"}], @@ -390,6 +455,81 @@ " expressions evaluated against the input document" ), ), + LookupTestCase( + "let_variable_values_can_be_constants", + foreign_docs=[ + {"_id": 10, "n": 7, "s": "hello"}, + {"_id": 11, "n": 7, "s": "other"}, + ], + docs=[{"_id": 1}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"num": 7, "word": "hello"}, + "pipeline": [ + { + "$match": { + "$expr": { + "$and": [ + {"$eq": ["$n", "$$num"]}, + {"$eq": ["$s", "$$word"]}, + ] + } + } + } + ], + "as": "joined", + } + } + ], + expected=[{"_id": 1, "joined": [{"_id": 10, "n": 7, "s": "hello"}]}], + msg=( + "$lookup let variable values should support literal constants," + " including a string treated as a literal rather than a field path" + ), + ), + LookupTestCase( + "let_mixed_forms_constant_field_expression", + foreign_docs=[ + {"_id": 10, "c": 5, "f": 7, "e": 3}, + {"_id": 11, "c": 5, "f": 8, "e": 3}, + ], + docs=[{"_id": 1, "x": 7}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": { + "konst": 5, + "from_field": "$x", + "expr": {"$add": [1, 2]}, + }, + "pipeline": [ + { + "$match": { + "$expr": { + "$and": [ + {"$eq": ["$c", "$$konst"]}, + {"$eq": ["$f", "$$from_field"]}, + {"$eq": ["$e", "$$expr"]}, + ] + } + } + } + ], + "as": "joined", + } + } + ], + expected=[ + {"_id": 1, "x": 7, "joined": [{"_id": 10, "c": 5, "f": 7, "e": 3}]}, + ], + msg=( + "$lookup should resolve a let document that mixes constant, field" + " reference, and expression values in a single binding" + ), + ), LookupTestCase( "let_null_behaves_like_omitting_let", docs=[{"_id": 1}], diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_system_vars.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_system_vars.py new file mode 100644 index 000000000..0b3e62306 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_system_vars.py @@ -0,0 +1,194 @@ +"""Tests for $$ROOT and $$CURRENT context inside a $lookup correlated sub-pipeline. + +Inside the sub-pipeline, $$ROOT and $$CURRENT refer to the FOREIGN document +context, not the outer document. Outer context must be captured via let. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.stages.lookup.utils.lookup_common import ( + FOREIGN, + LookupTestCase, + build_lookup_command, + setup_lookup, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Correlated Subquery System Variables]: inside the sub-pipeline +# $$ROOT and $$CURRENT refer to the foreign document context; outer context must +# be captured via let before the sub-pipeline executes. +LOOKUP_SYSTEM_VARS_TESTS: list[LookupTestCase] = [ + LookupTestCase( + "ROOT_inside_sub_pipeline_refers_to_foreign", + foreign_docs=[{"_id": 10, "name": "foreign_doc"}], + docs=[{"_id": 1, "name": "outer_doc"}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "pipeline": [{"$addFields": {"rootName": "$$ROOT.name"}}], + "as": "joined", + } + } + ], + expected=[ + { + "_id": 1, + "name": "outer_doc", + "joined": [{"_id": 10, "name": "foreign_doc", "rootName": "foreign_doc"}], + } + ], + msg="$$ROOT inside sub-pipeline should refer to the foreign document, not outer", + ), + LookupTestCase( + "CURRENT_inside_sub_pipeline_refers_to_foreign", + foreign_docs=[{"_id": 10, "name": "foreign_doc"}], + docs=[{"_id": 1, "name": "outer_doc"}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "pipeline": [{"$addFields": {"currentName": "$$CURRENT.name"}}], + "as": "joined", + } + } + ], + expected=[ + { + "_id": 1, + "name": "outer_doc", + "joined": [{"_id": 10, "name": "foreign_doc", "currentName": "foreign_doc"}], + } + ], + msg="$$CURRENT inside sub-pipeline should refer to the foreign document, not outer", + ), + LookupTestCase( + "let_captures_outer_ROOT_vs_inner_ROOT", + foreign_docs=[{"_id": 10, "name": "foreign"}], + docs=[{"_id": 1, "name": "outer"}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"outerRoot": "$$ROOT"}, + "pipeline": [ + { + "$addFields": { + "outerName": "$$outerRoot.name", + "innerName": "$$ROOT.name", + } + } + ], + "as": "joined", + } + } + ], + expected=[ + { + "_id": 1, + "name": "outer", + "joined": [ + { + "_id": 10, + "name": "foreign", + "outerName": "outer", + "innerName": "foreign", + } + ], + } + ], + msg=( + "let capturing $$ROOT should preserve outer doc context while" + " $$ROOT inside pipeline refers to foreign doc" + ), + ), + LookupTestCase( + "nested_ROOT_refers_to_innermost_foreign", + foreign_docs=[{"_id": 10, "name": "L1"}], + docs=[{"_id": 1, "name": "L0"}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"outerRoot": "$$ROOT"}, + "pipeline": [ + { + "$lookup": { + "from": FOREIGN, + "pipeline": [ + { + "$addFields": { + "deepRoot": "$$ROOT.name", + "fromOuter": "$$outerRoot.name", + } + } + ], + "as": "inner", + } + } + ], + "as": "joined", + } + } + ], + expected=[ + { + "_id": 1, + "name": "L0", + "joined": [ + { + "_id": 10, + "name": "L1", + "inner": [ + { + "_id": 10, + "name": "L1", + "deepRoot": "L1", + "fromOuter": "L0", + } + ], + } + ], + } + ], + msg=( + "In nested $lookup, $$ROOT in innermost pipeline refers to" + " that level's foreign doc while outer let preserves L0 context" + ), + ), + LookupTestCase( + "NOW_accessible_inside_sub_pipeline", + foreign_docs=[{"_id": 10}], + docs=[{"_id": 1}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "pipeline": [{"$addFields": {"nowType": {"$type": "$$NOW"}}}], + "as": "joined", + } + } + ], + expected=[{"_id": 1, "joined": [{"_id": 10, "nowType": "date"}]}], + msg="$$NOW should be accessible inside $lookup sub-pipeline as date type", + ), +] + + +@pytest.mark.aggregate +@pytest.mark.parametrize("test_case", pytest_params(LOOKUP_SYSTEM_VARS_TESTS)) +def test_lookup_correlated_system_vars(collection, test_case: LookupTestCase): + """Test $lookup correlated subquery $$ROOT/$$CURRENT/$$NOW context semantics.""" + with setup_lookup(collection, test_case) as foreign_name: + command = build_lookup_command(collection, test_case, foreign_name) + result = execute_command(collection, command) + assertResult( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) From 3b9a2d5253963854640a1f8e4aa4c6010e2f91c9 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Wed, 22 Jul 2026 11:35:13 -0700 Subject: [PATCH 2/3] Cover all BSON types as variables Signed-off-by: Daniel Frankcom --- .../lookup/test_lookup_correlated_subquery.py | 118 +++++++++++++----- 1 file changed, 86 insertions(+), 32 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py index e0ab025ed..5f80499b2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py @@ -2,7 +2,10 @@ from __future__ import annotations +import datetime + import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp from documentdb_tests.compatibility.tests.core.operator.stages.lookup.utils.lookup_common import ( FOREIGN, @@ -340,13 +343,23 @@ docs=[ { "_id": 1, - "v_int": 42, - "v_str": "hello", - "v_bool": True, "v_double": 3.14, + "v_int32": 42, + "v_int64": Int64(2**40), + "v_decimal": Decimal128("123.456"), + "v_string": "hello", + "v_bool": True, "v_null": None, - "v_arr": [1, 2], - "v_doc": {"n": 1}, + "v_date": datetime.datetime(2024, 6, 15, 12, 0, 0), + "v_oid": ObjectId("507f1f77bcf86cd799439011"), + "v_binary": Binary(b"\x00\x01\x02", 0), + "v_regex": Regex("^abc", "i"), + "v_code": Code("function() {}"), + "v_timestamp": Timestamp(1000, 1), + "v_minkey": MinKey(), + "v_maxkey": MaxKey(), + "v_arr": [1, "two", 3], + "v_doc": {"nested": "doc"}, } ], foreign_docs=[{"_id": 10}], @@ -355,23 +368,43 @@ "$lookup": { "from": FOREIGN, "let": { - "vi": "$v_int", - "vs": "$v_str", - "vb": "$v_bool", - "vd": "$v_double", - "vn": "$v_null", - "va": "$v_arr", + "vdouble": "$v_double", + "vint32": "$v_int32", + "vint64": "$v_int64", + "vdecimal": "$v_decimal", + "vstring": "$v_string", + "vbool": "$v_bool", + "vnull": "$v_null", + "vdate": "$v_date", + "void": "$v_oid", + "vbinary": "$v_binary", + "vregex": "$v_regex", + "vcode": "$v_code", + "vtimestamp": "$v_timestamp", + "vminkey": "$v_minkey", + "vmaxkey": "$v_maxkey", + "varr": "$v_arr", "vdoc": "$v_doc", }, "pipeline": [ { "$addFields": { - "ri": "$$vi", - "rs": "$$vs", - "rb": "$$vb", - "rd": "$$vd", - "rn": "$$vn", - "ra": "$$va", + "rdouble": "$$vdouble", + "rint32": "$$vint32", + "rint64": "$$vint64", + "rdecimal": "$$vdecimal", + "rstring": "$$vstring", + "rbool": "$$vbool", + "rnull": "$$vnull", + "rdate": "$$vdate", + "roid": "$$void", + "rbinary": "$$vbinary", + "rregex": "$$vregex", + "rcode": "$$vcode", + "rtimestamp": "$$vtimestamp", + "rminkey": "$$vminkey", + "rmaxkey": "$$vmaxkey", + "rarr": "$$varr", "rdoc": "$$vdoc", } } @@ -383,31 +416,52 @@ expected=[ { "_id": 1, - "v_int": 42, - "v_str": "hello", - "v_bool": True, "v_double": 3.14, + "v_int32": 42, + "v_int64": Int64(2**40), + "v_decimal": Decimal128("123.456"), + "v_string": "hello", + "v_bool": True, "v_null": None, - "v_arr": [1, 2], - "v_doc": {"n": 1}, + "v_date": datetime.datetime(2024, 6, 15, 12, 0, 0, tzinfo=datetime.timezone.utc), + "v_oid": ObjectId("507f1f77bcf86cd799439011"), + "v_binary": b"\x00\x01\x02", + "v_regex": Regex("^abc", 2), + "v_code": Code("function() {}"), + "v_timestamp": Timestamp(1000, 1), + "v_minkey": MinKey(), + "v_maxkey": MaxKey(), + "v_arr": [1, "two", 3], + "v_doc": {"nested": "doc"}, "joined": [ { "_id": 10, - "ri": 42, - "rs": "hello", - "rb": True, - "rd": 3.14, - "rn": None, - "ra": [1, 2], - "rdoc": {"n": 1}, + "rdouble": 3.14, + "rint32": 42, + "rint64": Int64(2**40), + "rdecimal": Decimal128("123.456"), + "rstring": "hello", + "rbool": True, + "rnull": None, + "rdate": datetime.datetime( + 2024, 6, 15, 12, 0, 0, tzinfo=datetime.timezone.utc + ), + "roid": ObjectId("507f1f77bcf86cd799439011"), + "rbinary": b"\x00\x01\x02", + "rregex": Regex("^abc", 2), + "rcode": Code("function() {}"), + "rtimestamp": Timestamp(1000, 1), + "rminkey": MinKey(), + "rmaxkey": MaxKey(), + "rarr": [1, "two", 3], + "rdoc": {"nested": "doc"}, } ], }, ], msg=( - "$lookup let variable values should support any BSON type" - " including int, string, bool, double, null, array, and" - " document" + "$lookup let variable values should carry every BSON type through" + " to the sub-pipeline unchanged" ), ), LookupTestCase( From 91ef16c459b2ea8e4bb0c0f1dbd92002fcb159ec Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Wed, 22 Jul 2026 11:55:29 -0700 Subject: [PATCH 3/3] Import additional coverage from #673 Co-authored-by: PatersonProjects Signed-off-by: Daniel Frankcom --- .../lookup/test_lookup_correlated_subquery.py | 135 +++++++++++++++++- .../lookup/test_lookup_join_semantics.py | 30 ++++ .../stages/lookup/test_lookup_self_join.py | 41 +++++- 3 files changed, 203 insertions(+), 3 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py index 5f80499b2..87b6173c0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_correlated_subquery.py @@ -14,7 +14,7 @@ setup_lookup, ) from documentdb_tests.framework.assertions import assertResult -from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, LET_UNDEFINED_VARIABLE_ERROR from documentdb_tests.framework.executor import execute_command from documentdb_tests.framework.parametrize import pytest_params @@ -543,6 +543,94 @@ " including a string treated as a literal rather than a field path" ), ), + LookupTestCase( + "let_constant_values_all_bson_types", + docs=[{"_id": 1}], + foreign_docs=[{"_id": 10}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": { + "k_double": 3.14, + "k_int32": 42, + "k_int64": Int64(2**40), + "k_decimal": Decimal128("123.456"), + "k_string": "hello", + "k_bool": True, + "k_null": None, + "k_date": datetime.datetime(2024, 6, 15, 12, 0, 0), + "k_oid": ObjectId("507f1f77bcf86cd799439011"), + "k_binary": Binary(b"\x00\x01\x02", 0), + "k_regex": Regex("^abc", "i"), + "k_code": Code("function() {}"), + "k_timestamp": Timestamp(1000, 1), + "k_minkey": MinKey(), + "k_maxkey": MaxKey(), + "k_arr": [1, "two", 3], + "k_doc": {"nested": "doc"}, + }, + "pipeline": [ + { + "$addFields": { + "r_double": "$$k_double", + "r_int32": "$$k_int32", + "r_int64": "$$k_int64", + "r_decimal": "$$k_decimal", + "r_string": "$$k_string", + "r_bool": "$$k_bool", + "r_null": "$$k_null", + "r_date": "$$k_date", + "r_oid": "$$k_oid", + "r_binary": "$$k_binary", + "r_regex": "$$k_regex", + "r_code": "$$k_code", + "r_timestamp": "$$k_timestamp", + "r_minkey": "$$k_minkey", + "r_maxkey": "$$k_maxkey", + "r_arr": "$$k_arr", + "r_doc": "$$k_doc", + } + } + ], + "as": "joined", + } + } + ], + expected=[ + { + "_id": 1, + "joined": [ + { + "_id": 10, + "r_double": 3.14, + "r_int32": 42, + "r_int64": Int64(2**40), + "r_decimal": Decimal128("123.456"), + "r_string": "hello", + "r_bool": True, + "r_null": None, + "r_date": datetime.datetime( + 2024, 6, 15, 12, 0, 0, tzinfo=datetime.timezone.utc + ), + "r_oid": ObjectId("507f1f77bcf86cd799439011"), + "r_binary": b"\x00\x01\x02", + "r_regex": Regex("^abc", 2), + "r_code": Code("function() {}"), + "r_timestamp": Timestamp(1000, 1), + "r_minkey": MinKey(), + "r_maxkey": MaxKey(), + "r_arr": [1, "two", 3], + "r_doc": {"nested": "doc"}, + } + ], + } + ], + msg=( + "$lookup let variable values should support literal constants of every" + " BSON type, carried through to the sub-pipeline unchanged" + ), + ), LookupTestCase( "let_mixed_forms_constant_field_expression", foreign_docs=[ @@ -790,10 +878,53 @@ error_code=BAD_VALUE_ERROR, msg="$lookup should propagate expression evaluation errors in let values", ), + LookupTestCase( + "let_expr_error_for_some_docs_fails_all", + docs=[{"_id": 1, "x": 10}, {"_id": 2, "x": 0}], + foreign_docs=[{"_id": 10}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"inv": {"$divide": [1, "$x"]}}, + "pipeline": [{"$addFields": {"val": "$$inv"}}], + "as": "joined", + } + } + ], + error_code=BAD_VALUE_ERROR, + msg="$lookup should fail the entire aggregate when a let expression errors " + "for any single outer document", + ), +] + +# Property [Correlated Subquery Undefined Variable]: referencing a variable that +# is not defined in let produces an undefined variable error. +LOOKUP_CORRELATED_SUBQUERY_UNDEFINED_VAR_TESTS: list[LookupTestCase] = [ + LookupTestCase( + "undefined_var_in_addFields_errors", + docs=[{"_id": 1}], + foreign_docs=[{"_id": 10}], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "let": {"x": "$_id"}, + "pipeline": [{"$addFields": {"val": "$$undefined_var"}}], + "as": "joined", + } + } + ], + error_code=LET_UNDEFINED_VARIABLE_ERROR, + msg="$lookup should reject a sub-pipeline reference to a variable that is " + "not defined in let", + ), ] LOOKUP_CORRELATED_SUBQUERY_ALL_TESTS: list[LookupTestCase] = ( - LOOKUP_CORRELATED_SUBQUERY_TESTS + LOOKUP_CORRELATED_SUBQUERY_ERROR_TESTS + LOOKUP_CORRELATED_SUBQUERY_TESTS + + LOOKUP_CORRELATED_SUBQUERY_ERROR_TESTS + + LOOKUP_CORRELATED_SUBQUERY_UNDEFINED_VAR_TESTS ) diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_join_semantics.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_join_semantics.py index 7489804eb..cef7a6ead 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_join_semantics.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_join_semantics.py @@ -145,6 +145,36 @@ " documents with all fields preserved" ), ), + LookupTestCase( + "outer_document_order_preserved", + docs=[ + {"_id": 3, "lf": "c"}, + {"_id": 1, "lf": "a"}, + {"_id": 2, "lf": "b"}, + ], + foreign_docs=[ + {"_id": 10, "ff": "a"}, + {"_id": 11, "ff": "b"}, + {"_id": 12, "ff": "c"}, + ], + pipeline=[ + { + "$lookup": { + "from": FOREIGN, + "localField": "lf", + "foreignField": "ff", + "as": "joined", + } + } + ], + expected=[ + {"_id": 3, "lf": "c", "joined": [{"_id": 12, "ff": "c"}]}, + {"_id": 1, "lf": "a", "joined": [{"_id": 10, "ff": "a"}]}, + {"_id": 2, "lf": "b", "joined": [{"_id": 11, "ff": "b"}]}, + ], + msg="$lookup should preserve the input document order in the output, not " + "sort by _id or join result", + ), ] # Property [Null and Missing Field Matching]: missing fields and explicit null diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_self_join.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_self_join.py index 2ac85be43..4d01f4fd2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_self_join.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_self_join.py @@ -146,9 +146,48 @@ ), ] +# Property [Correlated Self-Join]: a collection can correlate to itself through a +# let variable and sub-pipeline, resolving each outer document's field per row. +LOOKUP_SELF_JOIN_CORRELATED_TESTS: list[LookupTestCase] = [ + LookupTestCase( + "self_join_correlated_by_let_var", + docs=[ + {"_id": 1, "name": "root", "parent": None}, + {"_id": 2, "name": "child", "parent": 1}, + ], + pipeline=[ + { + "$lookup": { + "from": SELF, + "let": {"pid": "$_id"}, + "pipeline": [{"$match": {"$expr": {"$eq": ["$parent", "$$pid"]}}}], + "as": "children", + } + } + ], + expected=[ + { + "_id": 1, + "name": "root", + "parent": None, + "children": [{"_id": 2, "name": "child", "parent": 1}], + }, + {"_id": 2, "name": "child", "parent": 1, "children": []}, + ], + msg=( + "$lookup correlated self-join should find each document's children" + " where the foreign parent equals the outer _id" + ), + ), +] + +LOOKUP_SELF_JOIN_ALL_TESTS: list[LookupTestCase] = ( + LOOKUP_SELF_JOIN_TESTS + LOOKUP_SELF_JOIN_CORRELATED_TESTS +) + @pytest.mark.aggregate -@pytest.mark.parametrize("test_case", pytest_params(LOOKUP_SELF_JOIN_TESTS)) +@pytest.mark.parametrize("test_case", pytest_params(LOOKUP_SELF_JOIN_ALL_TESTS)) def test_lookup_self_join(collection, test_case: LookupTestCase): """Test $lookup self-join behavior.""" collection.database.create_collection(collection.name)