From 5f0135fd8e9cf6d6b0305e213ce5fada1896a4e6 Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Fri, 24 Jul 2026 14:18:10 +0000
Subject: [WIP] Convert PHVs to constants if phnullringrels went to
 0

This approach dissolves constant PHVs immediately when they lose their
nullability (phnullingrels becomes empty) during outer join reduction or
useless relation removal. A safety guard "orig_size > 0 && size == 0"
ensures we only dissolve PHVs that were actually nullable before,
avoiding regressions in FULL JOIN queries with constant keys.

This is WIP because there is a comment warning agains it. The intention
is to do this only in the narrow case. But it perhaps requires more analysis.
---
 src/backend/rewrite/rewriteManip.c | 36 +++++++++++++++++++++++++-----
 src/test/regress/sql/join.sql      | 23 +++++++++++++++++++
 2 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
index 9c9d1cad33b..3e85b38cecb 100644
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -23,6 +23,7 @@
 #include "parser/parse_relation.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
+#include "optimizer/clauses.h"
 #include "utils/lsyscache.h"
 
 
@@ -1369,17 +1370,17 @@ remove_nulling_relids_mutator(Node *node,
 		if (phv->phlevelsup == context->sublevels_up &&
 			!bms_overlap(phv->phrels, context->except_relids))
 		{
-			/*
-			 * Note: it might seem desirable to remove the PHV altogether if
-			 * phnullingrels goes to empty.  Currently we dare not do that
-			 * because we use PHVs in some cases to enforce separate identity
-			 * of subexpressions; see wrap_option usages in prepjointree.c.
-			 */
+			int			orig_size;
+			int			size;
+
 			/* Copy the PlaceHolderVar and mutate what's below ... */
 			phv = (PlaceHolderVar *)
 				expression_tree_mutator(node,
 										remove_nulling_relids_mutator,
 										context);
+
+			orig_size = bms_num_members(phv->phnullingrels);
+
 			/* ... and replace the copy's phnullingrels field */
 			phv->phnullingrels = bms_difference(phv->phnullingrels,
 												context->removable_relids);
@@ -1387,6 +1388,29 @@ remove_nulling_relids_mutator(Node *node,
 			phv->phrels = bms_difference(phv->phrels,
 										 context->removable_relids);
 			Assert(!bms_is_empty(phv->phrels));
+
+			size = bms_num_members(phv->phnullingrels);
+
+			/*
+			 * Note: it might seem desirable to remove the PHV altogether if
+			 * phnullingrels goes to empty.  Currently we dare not do that
+			 * because we use PHVs in some cases to enforce separate identity
+			 * of subexpressions; see wrap_option usages in prepjointree.c.
+			 *
+			 * However, if the PHV was previously nullable but now has no nulling
+			 * relations, and its inner expression is pseudo-constant, it has
+			 * effectively become a constant.  We can dissolve it immediately
+			 * by returning the inner expression.
+			 *
+			 * TODO : Check and confirm if there are any cases where this also might be wrong.
+			 */
+			if (orig_size > 0 &&
+				size == 0 &&
+				is_pseudo_constant_clause((Node *) phv->phexpr))
+			{
+				return (Node *) phv->phexpr;
+			}
+
 			return (Node *) phv;
 		}
 		/* Otherwise fall through to copy the PlaceHolderVar normally */
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 85aed7bf704..a61ef6e4958 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -3920,3 +3920,26 @@ SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2
     ON (t2.thousand = t1.tenthous OR t2.thousand = t1.thousand);
 SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2
     ON (t2.thousand = t1.tenthous OR t2.thousand = t1.thousand);
+
+-- Test Bug 19560 (loss of qual after join removal)
+CREATE TEMP TABLE bug_19560_items (id text, owner text);
+CREATE TEMP TABLE bug_19560_follows (item_id text, user_id text, UNIQUE (user_id, item_id));
+INSERT INTO bug_19560_items VALUES ('1', 'alice');
+INSERT INTO bug_19560_follows VALUES ('1', 'bob');
+
+-- should return 0
+SELECT COUNT(*)
+FROM bug_19560_items items
+LEFT JOIN bug_19560_follows follows ON follows.item_id = items.id AND follows.user_id = 'bob'
+LEFT JOIN (SELECT 'bob' AS id) viewer ON TRUE
+WHERE items.owner = viewer.id;
+
+-- explain should show the filter items.owner = 'bob'
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*)
+FROM bug_19560_items items
+LEFT JOIN bug_19560_follows follows ON follows.item_id = items.id AND follows.user_id = 'bob'
+LEFT JOIN (SELECT 'bob' AS id) viewer ON TRUE
+WHERE items.owner = viewer.id;
+
+DROP TABLE bug_19560_items, bug_19560_follows;
-- 
2.55.0.229.g6434b31f56-goog

