From dfd2ee200414150ecf805a08827bfcdd9893e36d Mon Sep 17 00:00:00 2001
From: Rui Zhao <zhaorui126@gmail.com>
Date: Thu, 30 Jul 2026 23:02:56 +0800
Subject: [PATCH 1/2] Don't take a local Var's non-nullness as proof about an
 upper-level Var

find_nonnullable_vars() only reports Vars of the current query level, and
the multibitmapset it returns identifies them by varno and varattno alone.
A NOT IN whose left-hand side is an upper-level Var can therefore collide
with a local Var that happens to have the same varno and varattno, and be
proven non-nullable by a qual that says nothing about it.  The NOT IN is
then converted to an anti-join and rows the original would have discarded
come back.

Restrict the qual-based proof to level-zero Vars, and add a test.

Reported-by: Ayush Tiwari
---
 src/backend/optimizer/plan/subselect.c  |  8 ++++-
 src/test/regress/expected/subselect.out | 40 +++++++++++++++++++++++++
 src/test/regress/sql/subselect.sql      | 23 ++++++++++++++
 3 files changed, 70 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index f6afcff740..94b6a2d8f6 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1601,11 +1601,17 @@ sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink,
 		 * on which this SubLink is evaluated.  Compute the list of Vars they
 		 * force non-null if we didn't already.
 		 *
+		 * find_nonnullable_vars only reports Vars of the current query level,
+		 * and identifies them by varno and varattno alone, so an upper-level
+		 * Var could be mistaken for a local one with the same varno/varattno.
+		 * Hence the varlevelsup test.
+		 *
 		 * Note that the quals must be run through flatten_join_alias_vars,
 		 * just as the outer expressions were above, so that the Vars match
 		 * up.
 		 */
-		if (nonnullable_quals != NIL && IsA(expr, Var))
+		if (nonnullable_quals != NIL && IsA(expr, Var) &&
+			((Var *) expr)->varlevelsup == 0)
 		{
 			Var		   *var = (Var *) expr;
 
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 68fcb02ac7..bfba1d532f 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -3759,6 +3759,46 @@ ORDER BY a;
  3 | 7
 (1 row)
 
+-- No ANTI JOIN: one left-hand expression is an upper-level Var.  A qual on a
+-- local Var with the same varno and varattno must not be taken as proof about
+-- it.
+CREATE TEMP TABLE upper_outer (a int);
+CREATE TEMP TABLE upper_local (a int);
+CREATE TEMP TABLE upper_inner (x int NOT NULL, y int NOT NULL);
+INSERT INTO upper_outer VALUES (NULL), (10);
+INSERT INTO upper_local VALUES (1);
+INSERT INTO upper_inner VALUES (1, 999);
+EXPLAIN (COSTS OFF)
+SELECT o.a,
+       ARRAY(SELECT l.a FROM upper_local l
+             WHERE l.a IS NOT NULL
+               AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i))
+FROM upper_outer o
+ORDER BY o.a NULLS FIRST;
+                                                              QUERY PLAN                                                               
+---------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: o.a NULLS FIRST
+   ->  Seq Scan on upper_outer o
+         SubPlan array_1
+           ->  Seq Scan on upper_local l
+                 Filter: ((a IS NOT NULL) AND (NOT (ANY ((a = (hashed SubPlan any_1).col1) AND (o.a = (hashed SubPlan any_1).col2)))))
+                 SubPlan any_1
+                   ->  Seq Scan on upper_inner i
+(8 rows)
+
+SELECT o.a,
+       ARRAY(SELECT l.a FROM upper_local l
+             WHERE l.a IS NOT NULL
+               AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i))
+FROM upper_outer o
+ORDER BY o.a NULLS FIRST;
+ a  | array 
+----+-------
+    | {}
+ 10 | {1}
+(2 rows)
+
 -- ANTI JOIN: outer side is defined NOT NULL, inner side is defined NOT NULL
 -- and is not nulled by outer join
 EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index 6486ed92da..4d00786ee3 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -1643,6 +1643,29 @@ SELECT * FROM qual_outer
 WHERE b IS NOT NULL AND b NOT IN (SELECT y FROM qual_inner)
 ORDER BY a;
 
+-- No ANTI JOIN: one left-hand expression is an upper-level Var.  A qual on a
+-- local Var with the same varno and varattno must not be taken as proof about
+-- it.
+CREATE TEMP TABLE upper_outer (a int);
+CREATE TEMP TABLE upper_local (a int);
+CREATE TEMP TABLE upper_inner (x int NOT NULL, y int NOT NULL);
+INSERT INTO upper_outer VALUES (NULL), (10);
+INSERT INTO upper_local VALUES (1);
+INSERT INTO upper_inner VALUES (1, 999);
+EXPLAIN (COSTS OFF)
+SELECT o.a,
+       ARRAY(SELECT l.a FROM upper_local l
+             WHERE l.a IS NOT NULL
+               AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i))
+FROM upper_outer o
+ORDER BY o.a NULLS FIRST;
+SELECT o.a,
+       ARRAY(SELECT l.a FROM upper_local l
+             WHERE l.a IS NOT NULL
+               AND (l.a, o.a) NOT IN (SELECT i.x, i.y FROM upper_inner i))
+FROM upper_outer o
+ORDER BY o.a NULLS FIRST;
+
 -- ANTI JOIN: outer side is defined NOT NULL, inner side is defined NOT NULL
 -- and is not nulled by outer join
 EXPLAIN (COSTS OFF)
-- 
2.43.7

