From c9ad071a4138954cdccd56cd102b1979e4b51795 Mon Sep 17 00:00:00 2001
From: Evdokimov Ilia <ilya.evdokimov@tantorlabs.com>
Date: Tue, 22 Sep 2026 16:19:36 +0500
Subject: [PATCH v3 1/2] Fold NOT IN / <> ALL with NULL array element to false
 in qual context

When a ScalarArrayOpExpr with useOr=false (NOT IN or <> ALL) appears in
a qual context and its array contains a NULL element, the expression can
never evaluate to true: with a strict operator, comparing any value to
NULL yields NULL, so the overall result is either false or NULL.  In a
qual, both mean the row is excluded, so the expression can be safely
folded to constant false during eval_const_expressions().  This allows
the planner to eliminate the scan entirely rather than performing it and
discarding all rows.

To inform eval_const_expressions() that an expression is used as a
qual, a new entry point eval_const_expressions_qual() is introduced.
It sets a new is_qual flag in eval_const_expressions_context.  The flag
is saved into a local variable and immediately reset to false at the
start of eval_const_expressions_mutator(), so it cannot leak into
sub-expressions where false and NULL are not interchangeable (e.g., an
argument to a non-strict function).  The folding checks
func_strict(saop->opfuncid) explicitly to confirm the operator is
strict before applying the optimization.
---
 src/backend/commands/copy.c               |   2 +-
 src/backend/optimizer/plan/planner.c      |   7 +-
 src/backend/optimizer/plan/subselect.c    |   2 +-
 src/backend/optimizer/util/clauses.c      | 106 ++++++++++++++++++++--
 src/backend/optimizer/util/inherit.c      |   2 +-
 src/include/optimizer/optimizer.h         |   1 +
 src/test/regress/expected/planner_est.out |  24 ++---
 7 files changed, 122 insertions(+), 22 deletions(-)

diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 003b70852bb..68cb535f73f 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -205,7 +205,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 			}
 
 			/* Reduce WHERE clause to standard list-of-AND-terms form */
-			whereClause = eval_const_expressions(NULL, whereClause);
+			whereClause = eval_const_expressions_qual(NULL, whereClause);
 
 			whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false);
 			whereClause = (Node *) make_ands_implicit((Expr *) whereClause);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 55a35aa3397..e98afd27efd 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1438,7 +1438,12 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind)
 	 * with AND directly under AND, nor OR directly under OR.
 	 */
 	if (kind != EXPRKIND_RTFUNC)
-		expr = eval_const_expressions(root, expr);
+	{
+		if (kind == EXPRKIND_QUAL)
+			expr = eval_const_expressions_qual(root, expr);
+		else
+			expr = eval_const_expressions(root, expr);
+	}
 
 	/*
 	 * If it's a qual or havingQual, canonicalize it.
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 5760b616813..f1b95041cee 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1984,7 +1984,7 @@ convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
 	subroot.type = T_PlannerInfo;
 	subroot.glob = root->glob;
 	subroot.parse = subselect;
-	whereClause = eval_const_expressions(&subroot, whereClause);
+	whereClause = eval_const_expressions_qual(&subroot, whereClause);
 	whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false);
 	whereClause = (Node *) make_ands_implicit((Expr *) whereClause);
 
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 3e1f210652d..53e189bf013 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -71,6 +71,7 @@ typedef struct
 	List	   *active_fns;
 	Node	   *case_val;
 	bool		estimate;
+	bool		is_qual;		/* true if simplifying a qual expression */
 } eval_const_expressions_context;
 
 typedef struct
@@ -144,10 +145,12 @@ static bool ece_function_is_safe(Oid funcid,
 								 eval_const_expressions_context *context);
 static List *simplify_or_arguments(List *args,
 								   eval_const_expressions_context *context,
-								   bool *haveNull, bool *forceTrue);
+								   bool *haveNull, bool *forceTrue,
+								   bool is_qual);
 static List *simplify_and_arguments(List *args,
 									eval_const_expressions_context *context,
-									bool *haveNull, bool *forceFalse);
+									bool *haveNull, bool *forceFalse,
+									bool is_qual);
 static Node *simplify_boolean_equality(Oid opno, List *args);
 static Expr *simplify_function(Oid funcid,
 							   Oid result_type, int32 result_typmod,
@@ -2647,6 +2650,34 @@ eval_const_expressions(PlannerInfo *root, Node *node)
 	context.active_fns = NIL;	/* nothing being recursively simplified */
 	context.case_val = NULL;	/* no CASE being examined */
 	context.estimate = false;	/* safe transformations only */
+	context.is_qual = false;	/* not a qual expression */
+	return eval_const_expressions_mutator(node, &context);
+}
+
+/*--------------------
+ * eval_const_expressions_qual
+ *
+ * Same as eval_const_expressions, but informs the simplifier that the
+ * expression is used as a qual (i.e., in a context where NULL and false have
+ * the same effect).  This enables additional simplifications, such as folding
+ * a NOT IN / <> ALL expression to constant false when the array contains a
+ * NULL element and the operator is strict.
+ *--------------------
+ */
+Node *
+eval_const_expressions_qual(PlannerInfo *root, Node *node)
+{
+	eval_const_expressions_context context;
+
+	if (root)
+		context.boundParams = root->glob->boundParams;	/* bound Params */
+	else
+		context.boundParams = NULL;
+	context.root = root;		/* for inlined-function dependencies */
+	context.active_fns = NIL;	/* nothing being recursively simplified */
+	context.case_val = NULL;	/* no CASE being examined */
+	context.estimate = false;	/* safe transformations only */
+	context.is_qual = true;		/* expression is used as a qual */
 	return eval_const_expressions_mutator(node, &context);
 }
 
@@ -2789,6 +2820,7 @@ estimate_expression_value(PlannerInfo *root, Node *node)
 	context.active_fns = NIL;	/* nothing being recursively simplified */
 	context.case_val = NULL;	/* no CASE being examined */
 	context.estimate = true;	/* unsafe transformations OK */
+	context.is_qual = false;	/* not a qual expression */
 	return eval_const_expressions_mutator(node, &context);
 }
 
@@ -2827,6 +2859,13 @@ static Node *
 eval_const_expressions_mutator(Node *node,
 							   eval_const_expressions_context *context)
 {
+	/*
+	 * Save and reset is_qual so that recursive calls don't inherit it by
+	 * default.
+	 */
+	bool		this_node_is_qual = context->is_qual;
+
+	context->is_qual = false;
 
 	/* since this function recurses, it could be driven to stack overflow */
 	check_stack_depth();
@@ -3293,6 +3332,44 @@ eval_const_expressions_mutator(Node *node,
 				/* Make sure we know underlying function */
 				set_sa_opfuncid(saop);
 
+				/*
+				 * When simplifying a qual expression (!useOr means NOT IN or
+				 * <> ALL), check whether the array contains a NULL element.
+				 * If the operator is strict, a NULL in the array means the
+				 * expression can never be true.
+				 */
+				if (this_node_is_qual && !saop->useOr &&
+					func_strict(saop->opfuncid))
+				{
+					Node	   *arrayarg = lsecond(saop->args);
+
+					if (IsA(arrayarg, Const) &&
+						!((Const *) arrayarg)->constisnull)
+					{
+						/* Constant array: check for NULLs using bitmap */
+						ArrayType  *arrayval =
+							DatumGetArrayTypeP(((Const *) arrayarg)->constvalue);
+
+						if (array_contains_nulls(arrayval))
+							return makeBoolConst(false, false);
+					}
+					else if (IsA(arrayarg, ArrayExpr) &&
+							 !((ArrayExpr *) arrayarg)->multidims)
+					{
+						/* Non-const array: check each element */
+						ListCell   *lc2;
+
+						foreach(lc2, ((ArrayExpr *) arrayarg)->elements)
+						{
+							Node	   *elem = (Node *) lfirst(lc2);
+
+							if (IsA(elem, Const) &&
+								((Const *) elem)->constisnull)
+								return makeBoolConst(false, false);
+						}
+					}
+				}
+
 				/*
 				 * If all arguments are Consts, and it's a safe function, we
 				 * can fold to a constant
@@ -3317,7 +3394,8 @@ eval_const_expressions_mutator(Node *node,
 							newargs = simplify_or_arguments(expr->args,
 															context,
 															&haveNull,
-															&forceTrue);
+															&forceTrue,
+															this_node_is_qual);
 							if (forceTrue)
 								return makeBoolConst(true, false);
 							if (haveNull)
@@ -3345,7 +3423,8 @@ eval_const_expressions_mutator(Node *node,
 							newargs = simplify_and_arguments(expr->args,
 															 context,
 															 &haveNull,
-															 &forceFalse);
+															 &forceFalse,
+															 this_node_is_qual);
 							if (forceFalse)
 								return makeBoolConst(false, false);
 							if (haveNull)
@@ -4411,11 +4490,19 @@ ece_function_is_safe(Oid funcid, eval_const_expressions_context *context)
  * The output arguments *haveNull and *forceTrue must be initialized false
  * by the caller.  They will be set true if a NULL constant or TRUE constant,
  * respectively, is detected anywhere in the argument list.
+ *
+ * is_qual should be true if this OR expression is itself being simplified
+ * in a context where FALSE and NULL are interchangeable (see is_qual in
+ * eval_const_expressions_context); it is passed down to each argument's
+ * own eval_const_expressions_mutator() call, since context->is_qual gets
+ * reset to false as a side effect of every such recursive call and so
+ * cannot simply be left set across the whole loop.
  */
 static List *
 simplify_or_arguments(List *args,
 					  eval_const_expressions_context *context,
-					  bool *haveNull, bool *forceTrue)
+					  bool *haveNull, bool *forceTrue,
+					  bool is_qual)
 {
 	List	   *newargs = NIL;
 	List	   *unprocessed_args;
@@ -4451,6 +4538,7 @@ simplify_or_arguments(List *args,
 		}
 
 		/* If it's not an OR, simplify it */
+		context->is_qual = is_qual;
 		arg = eval_const_expressions_mutator(arg, context);
 
 		/*
@@ -4517,11 +4605,16 @@ simplify_or_arguments(List *args,
  * The output arguments *haveNull and *forceFalse must be initialized false
  * by the caller.  They will be set true if a null constant or false constant,
  * respectively, is detected anywhere in the argument list.
+ *
+ * is_qual should be true if this AND expression is itself being simplified
+ * in a context where FALSE and NULL are interchangeable; see comments in
+ * simplify_or_arguments.
  */
 static List *
 simplify_and_arguments(List *args,
 					   eval_const_expressions_context *context,
-					   bool *haveNull, bool *forceFalse)
+					   bool *haveNull, bool *forceFalse,
+					   bool is_qual)
 {
 	List	   *newargs = NIL;
 	List	   *unprocessed_args;
@@ -4547,6 +4640,7 @@ simplify_and_arguments(List *args,
 		}
 
 		/* If it's not an AND, simplify it */
+		context->is_qual = is_qual;
 		arg = eval_const_expressions_mutator(arg, context);
 
 		/*
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index 6e1d2b14bc4..38074365a5c 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -863,7 +863,7 @@ apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel,
 		childqual = adjust_appendrel_attrs(root,
 										   (Node *) rinfo->clause,
 										   1, &appinfo);
-		childqual = eval_const_expressions(root, childqual);
+		childqual = eval_const_expressions_qual(root, childqual);
 		/* check for flat-out constant */
 		if (childqual && IsA(childqual, Const))
 		{
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index cb6241e2bdd..5be86a38a2f 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -145,6 +145,7 @@ extern bool contain_volatile_functions_after_planning(Expr *expr);
 extern bool contain_volatile_functions_not_nextval(Node *clause);
 
 extern Node *eval_const_expressions(PlannerInfo *root, Node *node);
+extern Node *eval_const_expressions_qual(PlannerInfo *root, Node *node);
 
 extern void convert_saop_to_hashed_saop(Node *node);
 
diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out
index 236cb274a78..806970eebac 100644
--- a/src/test/regress/expected/planner_est.out
+++ b/src/test/regress/expected/planner_est.out
@@ -192,23 +192,23 @@ false, true, false, true);
 SELECT explain_mask_costs($$
 SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 99, NULL]);$$,
 false, true, false, true);
-                   explain_mask_costs                    
----------------------------------------------------------
- Seq Scan on tenk1  (cost=N..N rows=1 width=N)
-   Filter: (unique1 <> ALL ('{1,2,99,NULL}'::integer[]))
-(2 rows)
+         explain_mask_costs         
+------------------------------------
+ Result  (cost=N..N rows=0 width=N)
+   Replaces: Scan on tenk1
+   One-Time Filter: false
+(3 rows)
 
 -- Try a non-const array containing a NULL
 SELECT explain_mask_costs($$
 SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 98, (SELECT 99), NULL]);$$,
 false, true, false, true);
-                                 explain_mask_costs                                  
--------------------------------------------------------------------------------------
- Seq Scan on tenk1  (cost=N..N rows=1 width=N)
-   Filter: (unique1 <> ALL (ARRAY[1, 2, 98, (InitPlan expr_1).col1, NULL::integer]))
-   InitPlan expr_1
-     ->  Result  (cost=N..N rows=1 width=N)
-(4 rows)
+         explain_mask_costs         
+------------------------------------
+ Result  (cost=N..N rows=0 width=N)
+   Replaces: Scan on tenk1
+   One-Time Filter: false
+(3 rows)
 
 -- Verify that scalarineqsel() works on "char" columns
 CREATE TEMP TABLE char_table_1 AS
-- 
2.43.0

