From 4e850e37e89a6e8d217403956a043d24ec37b6f2 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Thu, 26 Feb 2026 09:51:57 +0900
Subject: [PATCH v1] Convert ALL SubLinks to ANY SubLinks

---
 src/backend/optimizer/prep/prepjointree.c | 169 +++++++++++++++++++++-
 src/test/regress/expected/subselect.out   |  28 ++++
 src/test/regress/sql/subselect.sql        |  12 ++
 3 files changed, 206 insertions(+), 3 deletions(-)

diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index c90f4b32733..008bb469d2d 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -44,6 +44,7 @@
 #include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 
@@ -113,6 +114,7 @@ static Node *pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
 static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 										   Node **jtlink1, Relids available_rels1,
 										   Node **jtlink2, Relids available_rels2);
+static Node *negate_sublink_testexpr(Node *testexpr);
 static Node *pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 										JoinExpr *lowest_outer_join,
 										AppendRelInfo *containing_appendrel);
@@ -621,7 +623,7 @@ replace_empty_jointree(Query *parse)
 
 /*
  * pull_up_sublinks
- *		Attempt to pull up ANY and EXISTS SubLinks to be treated as
+ *		Attempt to pull up ANY, EXISTS and ALL SubLinks to be treated as
  *		semijoins or anti-semijoins.
  *
  * A clause "foo op ANY (sub-SELECT)" can be processed by pulling the
@@ -639,6 +641,12 @@ replace_empty_jointree(Query *parse)
  * Under similar conditions, EXISTS and NOT EXISTS clauses can be handled
  * by pulling up the sub-SELECT and creating a semijoin or anti-semijoin.
  *
+ * A clause "foo op ALL (sub-SELECT)" can be logically rewritten into "NOT (foo
+ * negator_op ANY (sub-SELECT))".  This conversion makes it possible for the
+ * executor to evaluate the unflattened sublink using a hashed SubPlan.
+ * Furthermore, it exposes the sublink to the standard pull-up machinery,
+ * potentially flattening it into a semijoin or anti-semijoin.
+ *
  * This routine searches for such clauses and does the necessary parsetree
  * transformations if any are found.
  *
@@ -844,7 +852,7 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 		JoinExpr   *j;
 		Relids		child_rels;
 
-		/* Is it a convertible ANY or EXISTS clause? */
+		/* Is it a convertible ANY, EXISTS or ALL clause? */
 		if (sublink->subLinkType == ANY_SUBLINK)
 		{
 			ScalarArrayOpExpr *saop;
@@ -965,12 +973,37 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 				return NULL;
 			}
 		}
+		else if (sublink->subLinkType == ALL_SUBLINK)
+		{
+			Node	   *negated_expr = negate_sublink_testexpr(sublink->testexpr);
+
+			if (negated_expr != NULL)
+			{
+				SubLink    *any_sublink = copyObject(sublink);
+				Node	   *not_expr;
+
+				any_sublink->testexpr = negated_expr;
+				any_sublink->subLinkType = ANY_SUBLINK;
+				/* XXX should we update operName accordingly */
+
+				not_expr = (Node *) makeBoolExpr(NOT_EXPR,
+												 list_make1(any_sublink),
+												 any_sublink->location);
+
+				return pull_up_sublinks_qual_recurse(root,
+													 not_expr,
+													 jtlink1,
+													 available_rels1,
+													 jtlink2,
+													 available_rels2);
+			}
+		}
 		/* Else return it unmodified */
 		return node;
 	}
 	if (is_notclause(node))
 	{
-		/* If the immediate argument of NOT is EXISTS, try to convert */
+		/* If the immediate argument of NOT is EXISTS or ALL, try to convert */
 		SubLink    *sublink = (SubLink *) get_notclausearg((Expr *) node);
 		JoinExpr   *j;
 		Relids		child_rels;
@@ -1031,6 +1064,26 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 					return NULL;
 				}
 			}
+			else if (sublink->subLinkType == ALL_SUBLINK)
+			{
+				Node	   *negated_expr = negate_sublink_testexpr(sublink->testexpr);
+
+				if (negated_expr != NULL)
+				{
+					SubLink    *any_sublink = copyObject(sublink);
+
+					any_sublink->testexpr = negated_expr;
+					any_sublink->subLinkType = ANY_SUBLINK;
+					/* XXX should we update operName accordingly */
+
+					return pull_up_sublinks_qual_recurse(root,
+														 (Node *) any_sublink,
+														 jtlink1,
+														 available_rels1,
+														 jtlink2,
+														 available_rels2);
+				}
+			}
 		}
 		/* Else return it unmodified */
 		return node;
@@ -1067,6 +1120,116 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
 	return node;
 }
 
+/*
+ * negate_sublink_testexpr
+ *		Attempt to logically negate the testexpr of an ALL_SUBLINK.
+ *
+ * This helper is used to transform ALL sublinks into ANY sublinks.  It returns
+ * a newly allocated negated expression tree, or NULL if negation is not
+ * possible.
+ */
+static Node *
+negate_sublink_testexpr(Node *testexpr)
+{
+	if (testexpr == NULL)
+		return NULL;
+	if (IsA(testexpr, OpExpr))
+	{
+		/* single-column comparison */
+		OpExpr	   *opexpr = (OpExpr *) testexpr;
+		Oid			negator = get_negator(opexpr->opno);
+
+		if (OidIsValid(negator))
+		{
+			OpExpr	   *newopexpr = copyObject(opexpr);
+
+			newopexpr->opno = negator;
+			newopexpr->opfuncid = InvalidOid;
+
+			return (Node *) newopexpr;
+		}
+	}
+	else if (is_andclause(testexpr) || is_orclause(testexpr))
+	{
+		/* multi-column equality or inequality checks */
+		BoolExpr   *bexpr = (BoolExpr *) testexpr;
+		List	   *nargs = NIL;
+		ListCell   *lc;
+
+		/*--------------------
+		 * Apply DeMorgan's Laws:
+		 *		(NOT (AND A B)) => (OR (NOT A) (NOT B))
+		 *		(NOT (OR A B))	=> (AND (NOT A) (NOT B))
+		 * i.e., swap AND for OR and negate each subclause.
+		 *--------------------
+		 */
+		foreach(lc, bexpr->args)
+		{
+			Node	   *negated_arg = negate_sublink_testexpr((Node *) lfirst(lc));
+
+			if (negated_arg == NULL)
+				return NULL;
+
+			nargs = lappend(nargs, negated_arg);
+		}
+
+		return (bexpr->boolop == AND_EXPR) ?
+			(Node *) makeBoolExpr(OR_EXPR, nargs, bexpr->location) :
+			(Node *) makeBoolExpr(AND_EXPR, nargs, bexpr->location);
+	}
+	else if (IsA(testexpr, RowCompareExpr))
+	{
+		/* multi-column ordering checks */
+		RowCompareExpr *rcexpr = (RowCompareExpr *) testexpr;
+		RowCompareExpr *newrcexpr;
+		List	   *negated_opnos = NIL;
+		CompareType negated_cmptype;
+
+		foreach_oid(opno, rcexpr->opnos)
+		{
+			Oid			negator = get_negator(opno);
+
+			if (!OidIsValid(negator))
+				return NULL;
+
+			negated_opnos = lappend_oid(negated_opnos, negator);
+		}
+
+		switch (rcexpr->cmptype)
+		{
+			case COMPARE_LT:
+				negated_cmptype = COMPARE_GE;
+				break;
+			case COMPARE_LE:
+				negated_cmptype = COMPARE_GT;
+				break;
+			case COMPARE_EQ:
+				negated_cmptype = COMPARE_NE;
+				break;
+			case COMPARE_GE:
+				negated_cmptype = COMPARE_LT;
+				break;
+			case COMPARE_GT:
+				negated_cmptype = COMPARE_LE;
+				break;
+			case COMPARE_NE:
+				negated_cmptype = COMPARE_EQ;
+				break;
+			default:
+				return NULL;
+				break;
+		}
+
+		newrcexpr = copyObject(rcexpr);
+		newrcexpr->opnos = negated_opnos;
+		newrcexpr->cmptype = negated_cmptype;
+
+		return (Node *) newrcexpr;
+	}
+
+	return NULL;
+}
+
 /*
  * preprocess_function_rtes
  *		Constant-simplify any FUNCTION RTEs in the FROM clause, and then
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 2135d82884d..654422ca952 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -3323,3 +3323,31 @@ SELECT ten FROM onek t WHERE 1.0::integer IN ((VALUES (1), (3)));
  Seq Scan on onek t
 (1 row)
 
+--
+-- Test ALL SubLink to ANY SubLink transformation
+--
+-- Ensure we get a hashed ANY-SubPlan
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 t1 WHERE two <> ALL (SELECT two FROM tenk1 t2);
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Seq Scan on tenk1 t1
+   Filter: (NOT (ANY (two = (hashed SubPlan any_1).col1)))
+   SubPlan any_1
+     ->  Seq Scan on tenk1 t2
+(4 rows)
+
+-- Ensure we get a join
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 t1 WHERE NOT two <> ALL (SELECT two FROM tenk1 t2);
+               QUERY PLAN               
+----------------------------------------
+ Hash Join
+   Hash Cond: (t1.two = t2.two)
+   ->  Seq Scan on tenk1 t1
+   ->  Hash
+         ->  HashAggregate
+               Group Key: t2.two
+               ->  Seq Scan on tenk1 t2
+(7 rows)
+
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index cadc3293687..58be939dd20 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -1448,3 +1448,15 @@ SELECT * FROM onek t1, lateral (SELECT * FROM onek t2 WHERE t2.ten IN (values (t
 -- VtA causes the whole expression to be evaluated as a constant
 EXPLAIN (COSTS OFF)
 SELECT ten FROM onek t WHERE 1.0::integer IN ((VALUES (1), (3)));
+
+--
+-- Test ALL SubLink to ANY SubLink transformation
+--
+
+-- Ensure we get a hashed ANY-SubPlan
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 t1 WHERE two <> ALL (SELECT two FROM tenk1 t2);
+
+-- Ensure we get a join
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 t1 WHERE NOT two <> ALL (SELECT two FROM tenk1 t2);
-- 
2.39.5 (Apple Git-154)

