From 7a1bb98154c2e9bd9f8bf0b69a2b55bd43fcde56 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Tue, 2 Dec 2025 16:27:26 +0900
Subject: [PATCH v4 3/3] Teach expr_is_nonnullable() to handle more expression
 types

Currently, the function expr_is_nonnullable() checks only Const and
Var expressions to determine if an expression is non-nullable.  This
patch extends the detection logic to handle more expression types.

This can enable several downstream optimizations, such as reducing
NullTest quals to constant truth values (e.g., "COALESCE(var, 1) IS
NULL" becomes FALSE) and converting "COUNT(expr)" to the more
efficient "COUNT(*)" when the expression is proven non-nullable.

This breaks a test case in test_predtest.sql, since we now simplify
"ARRAY[] IS NULL" to constant FALSE, preventing it from weakly
refuting a strict ScalarArrayOpExpr ("x = any(ARRAY[])").  To ensure
the refutation logic is still exercised as intended, wrap the array
argument in opaque_array().
---
 src/backend/optimizer/util/clauses.c          | 123 +++++++++++++++++-
 .../test_predtest/expected/test_predtest.out  |   2 +-
 .../test_predtest/sql/test_predtest.sql       |   2 +-
 src/test/regress/expected/predicate.out       | 117 ++++++++++++++++-
 src/test/regress/sql/predicate.sql            |  51 +++++++-
 5 files changed, 285 insertions(+), 10 deletions(-)

diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index ceb53c98726..11c42a06b50 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4341,16 +4341,127 @@ var_is_nonnullable(PlannerInfo *root, Var *var, bool use_rel_info)
  * nullability information before RelOptInfos are generated.  These should
  * pass 'use_rel_info' as false.
  *
- * For now, we only support Var and Const.  Support for other node types may
- * be possible.
+ * For now, we support only a limited set of expression types.  Support for
+ * additional node types can be added in the future.
  */
 bool
 expr_is_nonnullable(PlannerInfo *root, Expr *expr, bool use_rel_info)
 {
-	if (IsA(expr, Var) && root)
-		return var_is_nonnullable(root, (Var *) expr, use_rel_info);
-	if (IsA(expr, Const))
-		return !castNode(Const, expr)->constisnull;
+	/* since this function recurses, it could be driven to stack overflow */
+	check_stack_depth();
+
+	switch (nodeTag(expr))
+	{
+		case T_Var:
+			{
+				if (root)
+					return var_is_nonnullable(root, (Var *) expr, use_rel_info);
+			}
+			break;
+		case T_Const:
+			return !((Const *) expr)->constisnull;
+		case T_CoalesceExpr:
+			{
+				/*
+				 * A CoalesceExpr returns NULL if and only if all its
+				 * arguments are NULL.  Therefore, we can determine that a
+				 * CoalesceExpr cannot be NULL if at least one of its
+				 * arguments can be proven non-nullable.
+				 */
+				CoalesceExpr *coalesceexpr = (CoalesceExpr *) expr;
+
+				foreach_ptr(Expr, arg, coalesceexpr->args)
+				{
+					if (expr_is_nonnullable(root, arg, use_rel_info))
+						return true;
+				}
+			}
+			break;
+		case T_MinMaxExpr:
+			{
+				/*
+				 * Like CoalesceExpr, a MinMaxExpr returns NULL only if all
+				 * its arguments evaluate to NULL.
+				 */
+				MinMaxExpr *minmaxexpr = (MinMaxExpr *) expr;
+
+				foreach_ptr(Expr, arg, minmaxexpr->args)
+				{
+					if (expr_is_nonnullable(root, arg, use_rel_info))
+						return true;
+				}
+			}
+			break;
+		case T_CaseExpr:
+			{
+				/*
+				 * A CASE expression is non-nullable if all branch results are
+				 * non-nullable.  We must also verify that the default result
+				 * (ELSE) exists and is non-nullable.
+				 */
+				CaseExpr   *caseexpr = (CaseExpr *) expr;
+
+				/* The default result must be present and non-nullable */
+				if (caseexpr->defresult == NULL ||
+					!expr_is_nonnullable(root, caseexpr->defresult, use_rel_info))
+					return false;
+
+				/* All branch results must be non-nullable */
+				foreach_ptr(CaseWhen, casewhen, caseexpr->args)
+				{
+					if (!expr_is_nonnullable(root, casewhen->result, use_rel_info))
+						return false;
+				}
+
+				return true;
+			}
+			break;
+		case T_ArrayExpr:
+			{
+				/*
+				 * An ARRAY[] expression always returns a valid Array object,
+				 * even if it is empty (ARRAY[]) or contains NULLs
+				 * (ARRAY[NULL]).  It never evaluates to a SQL NULL.
+				 */
+				return true;
+			}
+		case T_NullTest:
+			{
+				/*
+				 * An IS NULL / IS NOT NULL expression always returns a
+				 * boolean value.  It never returns SQL NULL.
+				 */
+				return true;
+			}
+		case T_BooleanTest:
+			{
+				/*
+				 * A BooleanTest expression always evaluates to a boolean
+				 * value.  It never returns SQL NULL.
+				 */
+				return true;
+			}
+		case T_DistinctExpr:
+			{
+				/*
+				 * IS DISTINCT FROM never returns NULL, effectively acting as
+				 * though NULL were a normal data value.
+				 */
+				return true;
+			}
+		case T_RelabelType:
+			{
+				/*
+				 * RelabelType does not change the nullability of the data.
+				 * The result is non-nullable if and only if the argument is
+				 * non-nullable.
+				 */
+				return expr_is_nonnullable(root, ((RelabelType *) expr)->arg,
+										   use_rel_info);
+			}
+		default:
+			break;
+	}
 
 	return false;
 }
diff --git a/src/test/modules/test_predtest/expected/test_predtest.out b/src/test/modules/test_predtest/expected/test_predtest.out
index 6d21bcd603e..ad82b4f8f91 100644
--- a/src/test/modules/test_predtest/expected/test_predtest.out
+++ b/src/test/modules/test_predtest/expected/test_predtest.out
@@ -1066,7 +1066,7 @@ w_r_holds         | t
 
 -- as does nullness of the array
 select * from test_predtest($$
-select x = any(opaque_array(array[y])), array[y] is null
+select x = any(opaque_array(array[y])), opaque_array(array[y]) is null
 from integers
 $$);
 -[ RECORD 1 ]-----+--
diff --git a/src/test/modules/test_predtest/sql/test_predtest.sql b/src/test/modules/test_predtest/sql/test_predtest.sql
index 072eb5b0d50..dc59f0c22f0 100644
--- a/src/test/modules/test_predtest/sql/test_predtest.sql
+++ b/src/test/modules/test_predtest/sql/test_predtest.sql
@@ -431,7 +431,7 @@ $$);
 
 -- as does nullness of the array
 select * from test_predtest($$
-select x = any(opaque_array(array[y])), array[y] is null
+select x = any(opaque_array(array[y])), opaque_array(array[y]) is null
 from integers
 $$);
 
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 9f9f795e892..0c739d33cdb 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -443,11 +443,11 @@ SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
 DROP TABLE pred_tab2;
+CREATE TABLE pred_tab (a int NOT NULL, b int, c int);
 --
 -- Test that COALESCE expressions in predicates are simplified using
 -- non-nullable arguments.
 --
-CREATE TABLE pred_tab (a int NOT NULL, b int);
 -- Ensure that constant NULL arguments are dropped
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1;
@@ -475,4 +475,119 @@ SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
    Filter: (a > 1)
 (2 rows)
 
+--
+-- Test detection of non-nullable expressions in predicates
+--
+-- CoalesceExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL;
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on pred_tab
+   Filter: (COALESCE(b, c) IS NULL)
+(2 rows)
+
+-- MinMaxExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL;
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on pred_tab
+   Filter: (GREATEST(b, c) IS NULL)
+(2 rows)
+
+-- CaseExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Seq Scan on pred_tab
+   Filter: (CASE WHEN (c > 0) THEN b ELSE a END IS NULL)
+(2 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Seq Scan on pred_tab
+   Filter: (CASE WHEN (c > 0) THEN a ELSE NULL::integer END IS NULL)
+(2 rows)
+
+-- ArrayExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- NullTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- BooleanTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- DistinctExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- RelabelType
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
 DROP TABLE pred_tab;
diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql
index 0c7e4b974ff..1cbe398cf45 100644
--- a/src/test/regress/sql/predicate.sql
+++ b/src/test/regress/sql/predicate.sql
@@ -222,11 +222,12 @@ RESET constraint_exclusion;
 DROP TABLE pred_tab1;
 DROP TABLE pred_tab2;
 
+CREATE TABLE pred_tab (a int NOT NULL, b int, c int);
+
 --
 -- Test that COALESCE expressions in predicates are simplified using
 -- non-nullable arguments.
 --
-CREATE TABLE pred_tab (a int NOT NULL, b int);
 
 -- Ensure that constant NULL arguments are dropped
 EXPLAIN (COSTS OFF)
@@ -240,4 +241,52 @@ SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1;
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
 
+--
+-- Test detection of non-nullable expressions in predicates
+--
+
+-- CoalesceExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL;
+
+-- MinMaxExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL;
+
+-- CaseExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL;
+
+-- ArrayExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL;
+
+-- NullTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL;
+
+-- BooleanTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL;
+
+-- DistinctExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL;
+
+-- RelabelType
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
+
 DROP TABLE pred_tab;
-- 
2.39.5 (Apple Git-154)

