From 1de54c9a1edef2e821729f14396dc86a18735078 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Mon, 19 Oct 2026 16:02:31 +0300
Subject: [PATCH v7] Fold never-true scalar-array comparisons to false in quals

A strict "x op ALL (array)" expression cannot be true if the array
contains a NULL element. A NULL array produces NULL for both ANY
and ALL, regardless of operator strictness. In qual contexts, these
expressions can therefore be replaced with constant false.

Add eval_const_expressions_qual() to perform this simplification
after ordinary constant folding. The separate pass handles expressions
produced by NOT simplification, boolean equality simplification and
SQL function inlining. Recurse through AND/OR and fold IS TRUE when
its argument cannot be true. Also simplify aggregate and window
function FILTER clauses.

Leave IS NOT TRUE unchanged, since folding it to true can remove
quals needed to prove partial index predicates. Avoid rewriting
scalar expressions and ON CONFLICT inference predicates, preserving
matches against catalog expressions. Exclude multidimensional
ArrayExpr nodes, where NULL sub-arrays do not imply NULL elements.
---
 src/backend/commands/copy.c                   |   2 +-
 src/backend/optimizer/plan/planner.c          |  32 +-
 src/backend/optimizer/plan/subselect.c        |   2 +-
 src/backend/optimizer/util/clauses.c          | 226 +++++++
 src/backend/optimizer/util/inherit.c          |   2 +-
 src/include/optimizer/optimizer.h             |   1 +
 src/test/regress/expected/create_index.out    |  16 +-
 src/test/regress/expected/insert_conflict.out |  25 +
 src/test/regress/expected/partition_prune.out |  16 +-
 src/test/regress/expected/planner_est.out     |  24 +-
 src/test/regress/expected/predicate.out       | 605 ++++++++++++++++++
 src/test/regress/expected/stats_ext.out       |  15 +
 src/test/regress/sql/create_index.sql         |  12 +-
 src/test/regress/sql/insert_conflict.sql      |  21 +
 src/test/regress/sql/predicate.sql            | 231 +++++++
 src/test/regress/sql/stats_ext.sql            |  11 +
 16 files changed, 1203 insertions(+), 38 deletions(-)

diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 95b58cd2c84..a0b0238d6da 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -206,7 +206,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 edaa42d713a..a73d565a10a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -99,6 +99,7 @@ create_upper_paths_hook_type create_upper_paths_hook = NULL;
 #define EXPRKIND_TABLEFUNC			12
 #define EXPRKIND_TABLEFUNC_LATERAL	13
 #define EXPRKIND_GROUPEXPR			14
+#define EXPRKIND_ARBITER_WHERE		15
 
 /*
  * Data specific to grouping sets
@@ -1058,10 +1059,21 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 			preprocess_expression(root,
 								  (Node *) parse->onConflict->arbiterElems,
 								  EXPRKIND_ARBITER_ELEM);
+
+		/*
+		 * arbiterWhere is never evaluated as a runtime qual: it is only used
+		 * to match a partial index's predicate via predicate_implied_by() in
+		 * infer_arbiter_indexes().  Use EXPRKIND_ARBITER_WHERE (not
+		 * EXPRKIND_QUAL) so it still gets the usual qual-shaped preprocessing
+		 * (AND/OR flattening, canonicalize_qual, implicit-AND format) but is
+		 * not subject to the qual-only constant folding in
+		 * eval_const_expressions_qual(), which could replace it with a bare
+		 * Const and break the structural predicate match.
+		 */
 		parse->onConflict->arbiterWhere =
 			preprocess_expression(root,
 								  parse->onConflict->arbiterWhere,
-								  EXPRKIND_QUAL);
+								  EXPRKIND_ARBITER_WHERE);
 		parse->onConflict->onConflictSet = (List *)
 			preprocess_expression(root,
 								  (Node *) parse->onConflict->onConflictSet,
@@ -1444,12 +1456,17 @@ 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.
 	 */
-	if (kind == EXPRKIND_QUAL)
+	if (kind == EXPRKIND_QUAL || kind == EXPRKIND_ARBITER_WHERE)
 	{
 		expr = (Node *) canonicalize_qual((Expr *) expr, false);
 
@@ -1464,7 +1481,8 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind)
 	 * hashfuncid of any that might execute more quickly by using hash lookups
 	 * instead of a linear search.
 	 */
-	if (kind == EXPRKIND_QUAL || kind == EXPRKIND_TARGET)
+	if (kind == EXPRKIND_QUAL || kind == EXPRKIND_TARGET ||
+		kind == EXPRKIND_ARBITER_WHERE)
 	{
 		convert_saop_to_hashed_saop(expr);
 	}
@@ -1480,7 +1498,9 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind)
 
 	/* Expand SubLinks to SubPlans */
 	if (root->parse->hasSubLinks)
-		expr = SS_process_sublinks(root, expr, (kind == EXPRKIND_QUAL));
+		expr = SS_process_sublinks(root, expr,
+								   (kind == EXPRKIND_QUAL ||
+									kind == EXPRKIND_ARBITER_WHERE));
 
 	/*
 	 * XXX do not insert anything here unless you have grokked the comments in
@@ -1497,7 +1517,7 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind)
 	 * would be unable to simplify a top-level AND correctly. Also,
 	 * SS_process_sublinks expects explicit-AND format.)
 	 */
-	if (kind == EXPRKIND_QUAL)
+	if (kind == EXPRKIND_QUAL || kind == EXPRKIND_ARBITER_WHERE)
 		expr = (Node *) make_ands_implicit((Expr *) expr);
 
 	return expr;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 1b66906ad11..fabcb19c4ea 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2040,7 +2040,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 95134bfeda3..b035a9178e5 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -142,6 +142,8 @@ static Node *eval_const_expressions_mutator(Node *node,
 static bool contain_non_const_walker(Node *node, void *context);
 static bool ece_function_is_safe(Oid funcid,
 								 eval_const_expressions_context *context);
+static bool saop_never_true(ScalarArrayOpExpr *saop);
+static Node *simplify_qual_null_saops(Node *node);
 static List *simplify_or_arguments(List *args,
 								   eval_const_expressions_context *context,
 								   bool *haveNull, bool *forceTrue);
@@ -2650,6 +2652,220 @@ eval_const_expressions(PlannerInfo *root, Node *node)
 	return eval_const_expressions_mutator(node, &context);
 }
 
+/*--------------------
+ * eval_const_expressions_qual
+ *
+ * Same as eval_const_expressions, but for an expression that is used as a
+ * qual, i.e. in a context where a NULL result has the same effect as FALSE.
+ * This allows additional simplifications; see simplify_qual_null_saops.
+ *
+ * The input should be in explicit-AND format: the extra simplification does
+ * not look into the elements of an implicit-AND List.
+ *--------------------
+ */
+Node *
+eval_const_expressions_qual(PlannerInfo *root, Node *node)
+{
+	node = eval_const_expressions(root, node);
+	return simplify_qual_null_saops(node);
+}
+
+/*
+ * saop_never_true
+ *		Can this ScalarArrayOpExpr never yield TRUE?
+ *
+ * That is so if the array is a NULL constant, in which case the result is
+ * always NULL, or if it is "x op ALL (array)" with a strict operator and an
+ * array known to contain a NULL element, in which case each comparison
+ * against the NULL element yields NULL, so the result is FALSE or NULL.
+ */
+static bool
+saop_never_true(ScalarArrayOpExpr *saop)
+{
+	Node	   *arrayarg = lsecond(saop->args);
+
+	/*
+	 * If the array is NULL, the result is NULL whatever the operator and the
+	 * scalar argument are; see ExecEvalScalarArrayOp.  That holds for ANY as
+	 * well as ALL, and for a non-strict operator too.
+	 */
+	if (IsA(arrayarg, Const) && ((Const *) arrayarg)->constisnull)
+		return true;
+
+	if (saop->useOr)
+		return false;
+
+	set_sa_opfuncid(saop);
+	if (!func_strict(saop->opfuncid))
+		return false;
+
+	if (IsA(arrayarg, Const))
+	{
+		Const	   *arrayconst = (Const *) arrayarg;
+
+		return array_contains_nulls(DatumGetArrayTypeP(arrayconst->constvalue));
+	}
+	else if (IsA(arrayarg, ArrayExpr))
+	{
+		ArrayExpr  *arrayexpr = (ArrayExpr *) arrayarg;
+		ListCell   *lc;
+
+		/*
+		 * In a multidimensional ARRAY[] the elements are sub-arrays, and a
+		 * NULL sub-array doesn't produce a NULL element: e.g.
+		 * ARRAY[NULL::int[], NULL::int[]] is an empty array, over which ALL
+		 * is TRUE.
+		 */
+		if (arrayexpr->multidims)
+			return false;
+
+		foreach(lc, arrayexpr->elements)
+		{
+			Node	   *elem = (Node *) lfirst(lc);
+
+			if (IsA(elem, Const) && ((Const *) elem)->constisnull)
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * simplify_qual_null_saops
+ *		Simplify an already const-simplified boolean expression that is used
+ *		in a context where NULL has the same effect as FALSE, such as a WHERE
+ *		or JOIN qual or an aggregate FILTER clause.
+ *
+ * In such a context "x op ALL (array)" is replaced by constant FALSE if
+ * saop_never_true() says it can never yield TRUE.  We also look through
+ * AND and OR, since replacing a NULL argument by FALSE cannot change whether
+ * an AND or OR yields TRUE.  An AND with a FALSE (or NULL) argument then
+ * reduces to FALSE, and such arguments are dropped from an OR.  We must not
+ * look through NOT, since NOT NULL is NULL but NOT FALSE is TRUE.
+ *
+ * "expr IS TRUE" is replaced by constant FALSE if expr reduces to constant
+ * FALSE this way.  We do not otherwise rewrite its argument.  In particular,
+ * we must not fold "expr IS NOT TRUE" to TRUE: removing that qual can prevent
+ * the planner from proving an identical partial index predicate, which is
+ * simplified using plain eval_const_expressions().
+ *
+ * In particular, this is not applied to CASE WHEN conditions or to IS [NOT]
+ * TRUE outside of a qual, although NULL and FALSE are equivalent there too.
+ * Such a rewrite would change the structure of a scalar expression without
+ * changing its value, so the expression would no longer match an equal
+ * expression stored in the catalogs and simplified without it, such as an
+ * index expression, a partition key or an extended statistics expression.
+ * Nor can those be simplified the same way, since a partition key is checked
+ * for being constant when it is defined, and existing schemas must still be
+ * restorable.  An aggregate or window function FILTER clause cannot appear
+ * in any such expression, so eval_const_expressions_mutator applies this to
+ * it when planning a query.
+ *
+ * This is done as a separate pass over the output of
+ * eval_const_expressions_mutator, rather than while simplifying the
+ * ScalarArrayOpExpr node itself, because NOTs pushed down by negate_clause(),
+ * simplified boolean equalities such as "(expr) = true", and inlined SQL
+ * functions can all produce such a node only after its own simplification
+ * is complete.
+ */
+static Node *
+simplify_qual_null_saops(Node *node)
+{
+	/* This function recurses through boolean expressions. */
+	check_stack_depth();
+
+	if (node == NULL)
+		return NULL;
+
+	if (IsA(node, ScalarArrayOpExpr))
+	{
+		if (saop_never_true((ScalarArrayOpExpr *) node))
+			return makeBoolConst(false, false);
+	}
+	else if (IsA(node, BooleanTest))
+	{
+		BooleanTest *btest = (BooleanTest *) node;
+
+		if (btest->booltesttype == IS_TRUE)
+		{
+			Node	   *arg = simplify_qual_null_saops((Node *) btest->arg);
+
+			/* FALSE and NULL are equivalent here */
+			if (arg && IsA(arg, Const) &&
+				(((Const *) arg)->constisnull ||
+				 !DatumGetBool(((Const *) arg)->constvalue)))
+				return makeBoolConst(false, false);
+		}
+	}
+	else if (is_andclause(node) || is_orclause(node))
+	{
+		BoolExpr   *expr = (BoolExpr *) node;
+		bool		isand = (expr->boolop == AND_EXPR);
+		List	   *newargs = NIL;
+		bool		changed = false;
+		ListCell   *lc;
+
+		foreach(lc, expr->args)
+		{
+			Node	   *arg = (Node *) lfirst(lc);
+			Node	   *newarg = simplify_qual_null_saops(arg);
+
+			/* Copy the unchanged prefix only when a rewrite is needed. */
+			if (!changed &&
+				(newarg != arg || IsA(newarg, Const) ||
+				 (isand && is_andclause(newarg)) ||
+				 (!isand && is_orclause(newarg))))
+			{
+				newargs = list_copy_head(expr->args,
+										 foreach_current_index(lc));
+				changed = true;
+			}
+
+			if (!changed)
+				continue;
+
+			if (IsA(newarg, Const))
+			{
+				Const	   *c = (Const *) newarg;
+
+				/* FALSE and NULL are equivalent here */
+				if (c->constisnull || !DatumGetBool(c->constvalue))
+				{
+					if (isand)
+						return makeBoolConst(false, false);
+					/* drop it from the OR */
+				}
+				else
+				{
+					if (!isand)
+						return makeBoolConst(true, false);
+					/* drop it from the AND */
+				}
+				changed = true;
+				continue;
+			}
+
+			/* Keep the result flat, as eval_const_expressions does */
+			if ((isand && is_andclause(newarg)) ||
+				(!isand && is_orclause(newarg)))
+				newargs = list_concat(newargs, ((BoolExpr *) newarg)->args);
+			else
+				newargs = lappend(newargs, newarg);
+		}
+
+		if (!changed)
+			return node;
+		if (newargs == NIL)
+			return makeBoolConst(isand, false);
+		if (list_length(newargs) == 1)
+			return (Node *) linitial(newargs);
+		return (Node *) makeBoolExpr(expr->boolop, newargs, expr->location);
+	}
+
+	return node;
+}
+
 #define MIN_ARRAY_SIZE_FOR_HASHED_SAOP 9
 /*--------------------
  * convert_saop_to_hashed_saop
@@ -2944,6 +3160,10 @@ eval_const_expressions_mutator(Node *node,
 				aggfilter = (Expr *)
 					eval_const_expressions_mutator((Node *) expr->aggfilter,
 												   context);
+				/* NULL and FALSE are equivalent in a FILTER clause */
+				if (context->root != NULL)
+					aggfilter = (Expr *)
+						simplify_qual_null_saops((Node *) aggfilter);
 
 				/* And build the replacement WindowFunc node */
 				newexpr = makeNode(WindowFunc);
@@ -3010,7 +3230,12 @@ eval_const_expressions_mutator(Node *node,
 		case T_Aggref:
 			node = ece_generic_processing(node);
 			if (context->root != NULL)
+			{
+				/* NULL and FALSE are equivalent in a FILTER clause */
+				((Aggref *) node)->aggfilter = (Expr *)
+					simplify_qual_null_saops((Node *) ((Aggref *) node)->aggfilter);
 				return simplify_aggref((Aggref *) node, context);
+			}
 			return node;
 		case T_OpExpr:
 			{
@@ -4159,6 +4384,7 @@ eval_const_expressions_mutator(Node *node,
 
 				arg = eval_const_expressions_mutator((Node *) btest->arg,
 													 context);
+
 				if (arg && IsA(arg, Const))
 				{
 					/*
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/create_index.out b/src/test/regress/expected/create_index.out
index 7b2640f0e04..53a860620e8 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2413,19 +2413,25 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 ---------
 (0 rows)
 
+-- Use a generic plan, since a constant NULL array is folded away by the planner
+SET plan_cache_mode = force_generic_plan;
+PREPARE null_array_key(int[]) AS
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY($1);
 explain (costs off)
-SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
-                   QUERY PLAN                    
--------------------------------------------------
+EXECUTE null_array_key(NULL);
+                  QUERY PLAN                  
+----------------------------------------------
  Index Only Scan using tenk1_unique1 on tenk1
-   Index Cond: (unique1 = ANY (NULL::integer[]))
+   Index Cond: (unique1 = ANY ($1))
 (2 rows)
 
-SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+EXECUTE null_array_key(NULL);
  unique1 
 ---------
 (0 rows)
 
+DEALLOCATE null_array_key;
+RESET plan_cache_mode;
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
                           QUERY PLAN                           
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 0dd8bc734a0..680d7915e20 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -503,6 +503,31 @@ ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT spec
 insert into insertconflicttest values (23, 'Blackberry') on conflict (fruit) where fruit like '%berry' do update set fruit = excluded.fruit;
 ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
 drop index partial_key_index;
+-- The inference WHERE clause must not be reduced to constant-FALSE even
+-- though it can never be true, else it no longer matches the index predicate
+create unique index null_saop_key_index on insertconflicttest(key) where fruit <> all (array['Apple', null]);
+-- Succeeds
+explain (costs off)
+insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Insert on insertconflicttest
+   Conflict Resolution: NOTHING
+   Conflict Arbiter Indexes: null_saop_key_index
+   ->  Result
+(4 rows)
+
+insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing;
+insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) and fruit like '%berry' do nothing;
+drop index null_saop_key_index;
+delete from insertconflicttest where key = 1001;
+-- Nor may an expression in the inference specification be rewritten so that
+-- it no longer matches the index expression
+create unique index null_saop_expr_index on insertconflicttest
+  ((case when fruit not in ('Apple', null) then 1001 else key end));
+insert into insertconflicttest values (1002, 'Lime') on conflict ((case when fruit not in ('Apple', null) then 1001 else key end)) do nothing;
+drop index null_saop_expr_index;
+delete from insertconflicttest where key = 1002;
 --
 -- Test that wholerow references to ON CONFLICT's EXCLUDED work
 --
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 3506ecec477..485fb097c9c 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -3642,16 +3642,12 @@ select * from stable_qual_pruning
 explain (analyze, costs off, summary off, timing off, buffers off)
 select * from stable_qual_pruning
   where a = any(null::timestamptz[]);
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
- Append (actual rows=0.00 loops=1)
-   ->  Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0.00 loops=1)
-         Filter: (a = ANY (NULL::timestamp with time zone[]))
-   ->  Seq Scan on stable_qual_pruning2 stable_qual_pruning_2 (actual rows=0.00 loops=1)
-         Filter: (a = ANY (NULL::timestamp with time zone[]))
-   ->  Seq Scan on stable_qual_pruning3 stable_qual_pruning_3 (actual rows=0.00 loops=1)
-         Filter: (a = ANY (NULL::timestamp with time zone[]))
-(7 rows)
+               QUERY PLAN                
+-----------------------------------------
+ Result (actual rows=0.00 loops=1)
+   Replaces: Scan on stable_qual_pruning
+   One-Time Filter: false
+(3 rows)
 
 drop table stable_qual_pruning;
 --
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
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 22fe4308af3..9b0649907f6 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -944,3 +944,608 @@ SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN;
 (1 row)
 
 DROP TABLE bool_tab;
+--
+-- Test that "x op ALL (array)" with a strict operator and a NULL array
+-- element is reduced to constant-FALSE where NULL and FALSE are equivalent
+--
+CREATE TABLE null_saop_tab (a int, b int, arr int[]);
+INSERT INTO null_saop_tab VALUES (1, 1, NULL), (42, 2, '{}'), (NULL, 3, NULL);
+ANALYZE null_saop_tab;
+-- Ensure NOT IN and <> ALL are reduced to constant-FALSE
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL);
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL);
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]);
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]);
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+-- Ensure equivalent forms are reduced to constant-FALSE as well
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL));
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL));
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL]));
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL]));
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL));
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL));
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true;
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true;
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+CREATE FUNCTION null_saop_func(int) RETURNS bool LANGUAGE sql IMMUTABLE
+AS $$ SELECT $1 NOT IN (42, NULL) $$;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE null_saop_func(a);
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE null_saop_func(a);
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+DROP FUNCTION null_saop_func(int);
+-- Ensure it is reduced under AND and OR
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1;
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1;
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1;
+        QUERY PLAN         
+---------------------------
+ Seq Scan on null_saop_tab
+   Filter: (b = 1)
+(2 rows)
+
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1;
+ a | b | arr 
+---+---+-----
+ 1 | 1 | 
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab
+WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL)));
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab
+WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL)));
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on null_saop_tab
+   Filter: ((a > 0) AND (b = 1))
+(2 rows)
+
+SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0;
+ a | b | arr 
+---+---+-----
+ 1 | 1 | 
+(1 row)
+
+-- Ensure it is reduced in a join clause
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2
+  ON t1.a = t2.a AND t2.b NOT IN (42, NULL);
+             QUERY PLAN             
+------------------------------------
+ Nested Loop Left Join
+   Join Filter: false
+   ->  Seq Scan on null_saop_tab t1
+   ->  Result
+         Replaces: Scan on t2
+         One-Time Filter: false
+(6 rows)
+
+SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2
+  ON t1.a = t2.a AND t2.b NOT IN (42, NULL)
+ORDER BY t1.b;
+ a  | b | arr | a | b | arr 
+----+---+-----+---+---+-----
+  1 | 1 |     |   |   | 
+ 42 | 2 | {}  |   |   | 
+    | 3 |     |   |   | 
+(3 rows)
+
+-- Ensure it is reduced in FILTER clauses
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab;
+               QUERY PLAN                
+-----------------------------------------
+ Aggregate
+   Output: count(*) FILTER (WHERE false)
+   ->  Seq Scan on public.null_saop_tab
+         Output: a, b, arr
+(4 rows)
+
+SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab;
+ count 
+-------
+     0
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER ()
+FROM null_saop_tab;
+                     QUERY PLAN                     
+----------------------------------------------------
+ WindowAgg
+   Output: b, count(*) FILTER (WHERE false) OVER w1
+   Window: w1 AS ()
+   ->  Seq Scan on public.null_saop_tab
+         Output: b
+(5 rows)
+
+SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER ()
+FROM null_saop_tab;
+ b | count 
+---+-------
+ 1 |     0
+ 2 |     0
+ 3 |     0
+(3 rows)
+
+-- Ensure it is not reduced under NOT
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL));
+                  QUERY PLAN                  
+----------------------------------------------
+ Seq Scan on null_saop_tab
+   Filter: (a = ANY ('{42,NULL}'::integer[]))
+(2 rows)
+
+SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL));
+ a  | b | arr 
+----+---+-----
+ 42 | 2 | {}
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END
+FROM null_saop_tab;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ Seq Scan on public.null_saop_tab
+   Output: b, CASE WHEN (a = ANY ('{42,NULL}'::integer[])) THEN 1 ELSE 0 END
+(2 rows)
+
+SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END
+FROM null_saop_tab;
+ b | case 
+---+------
+ 1 |    0
+ 2 |    1
+ 3 |    0
+(3 rows)
+
+-- Ensure it is not reduced where NULL and FALSE are not equivalent
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, a NOT IN (42, NULL) FROM null_saop_tab;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Seq Scan on public.null_saop_tab
+   Output: b, (a <> ALL ('{42,NULL}'::integer[]))
+(2 rows)
+
+SELECT b, a NOT IN (42, NULL) FROM null_saop_tab;
+ b | ?column? 
+---+----------
+ 1 | 
+ 2 | f
+ 3 | 
+(3 rows)
+
+-- Ensure it is not reduced in CASE WHEN conditions or under IS [NOT] TRUE
+-- outside of a qual, so that such expressions still match equal expressions
+-- stored in the catalogs
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END,
+  (a NOT IN (42, NULL)) IS TRUE, (a NOT IN (42, NULL)) IS NOT TRUE
+FROM null_saop_tab;
+                                                                                   QUERY PLAN                                                                                   
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on public.null_saop_tab
+   Output: b, CASE WHEN (a <> ALL ('{42,NULL}'::integer[])) THEN 1 ELSE 0 END, ((a <> ALL ('{42,NULL}'::integer[])) IS TRUE), ((a <> ALL ('{42,NULL}'::integer[])) IS NOT TRUE)
+(2 rows)
+
+SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END,
+  (a NOT IN (42, NULL)) IS TRUE, (a NOT IN (42, NULL)) IS NOT TRUE
+FROM null_saop_tab;
+ b | case | ?column? | ?column? 
+---+------+----------+----------
+ 1 |    0 | f        | t
+ 2 |    0 | f        | t
+ 3 |    0 | f        | t
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab
+WHERE (CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE b END) = 1;
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Seq Scan on null_saop_tab
+   Filter: (CASE WHEN (a <> ALL ('{42,NULL}'::integer[])) THEN 1 ELSE b END = 1)
+(2 rows)
+
+SELECT * FROM null_saop_tab
+WHERE (CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE b END) = 1;
+ a | b | arr 
+---+---+-----
+ 1 | 1 | 
+(1 row)
+
+-- Ensure it is not reduced for a multidimensional ARRAY[]: NULL sub-arrays
+-- don't add NULL elements, and here the array is empty, so ALL is true
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Sort
+   Sort Key: b
+   ->  Seq Scan on null_saop_tab
+         Filter: (1 <> ALL (ARRAY[NULL::integer[], arr]))
+(4 rows)
+
+SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b;
+ a  | b | arr 
+----+---+-----
+  1 | 1 | 
+ 42 | 2 | {}
+    | 3 | 
+(3 rows)
+
+-- Ensure IS TRUE is reduced to constant-FALSE, while IS NOT TRUE and
+-- IS FALSE are not reduced
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS TRUE;
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS TRUE;
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL) AND b > 0) IS NOT TRUE
+ORDER BY b;
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Sort
+   Sort Key: b
+   ->  Seq Scan on null_saop_tab
+         Filter: (((a <> ALL ('{42,NULL}'::integer[])) AND (b > 0)) IS NOT TRUE)
+(4 rows)
+
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL) AND b > 0) IS NOT TRUE
+ORDER BY b;
+ a  | b | arr 
+----+---+-----
+  1 | 1 | 
+ 42 | 2 | {}
+    | 3 | 
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS FALSE;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Seq Scan on null_saop_tab
+   Filter: ((a <> ALL ('{42,NULL}'::integer[])) IS FALSE)
+(2 rows)
+
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS FALSE;
+ a  | b | arr 
+----+---+-----
+ 42 | 2 | {}
+(1 row)
+
+-- Ensure a NULL array is reduced to constant-FALSE, for ANY as well as ALL
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a <> ALL (NULL::int[]);
+            QUERY PLAN             
+-----------------------------------
+ Result
+   Replaces: Scan on null_saop_tab
+   One-Time Filter: false
+(3 rows)
+
+SELECT * FROM null_saop_tab WHERE a <> ALL (NULL::int[]);
+ a | b | arr 
+---+---+-----
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a = ANY (NULL::int[]) OR b = 1;
+        QUERY PLAN         
+---------------------------
+ Seq Scan on null_saop_tab
+   Filter: (b = 1)
+(2 rows)
+
+SELECT * FROM null_saop_tab WHERE a = ANY (NULL::int[]) OR b = 1;
+ a | b | arr 
+---+---+-----
+ 1 | 1 | 
+(1 row)
+
+-- Ensure expressions checked for constness at DDL time are not reduced
+CREATE TABLE null_saop_part (a int)
+  PARTITION BY LIST ((CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END));
+DROP TABLE null_saop_part;
+-- Ensure a partition key containing such an expression still matches the
+-- query, for partition pruning, partitionwise aggregation and constraint
+-- exclusion
+CREATE TABLE null_saop_part (a int, b int)
+  PARTITION BY LIST ((CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END));
+CREATE TABLE null_saop_part0 PARTITION OF null_saop_part FOR VALUES IN (0);
+CREATE TABLE null_saop_part1 PARTITION OF null_saop_part FOR VALUES IN (1);
+INSERT INTO null_saop_part SELECT i % 2, i FROM generate_series(1, 10) i;
+ANALYZE null_saop_part;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_part
+WHERE (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) = 1;
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Seq Scan on null_saop_part1 null_saop_part
+   Filter: (CASE WHEN (b <> ALL ('{42,NULL}'::integer[])) THEN 0 ELSE a END = 1)
+(2 rows)
+
+SELECT * FROM null_saop_part
+WHERE (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) = 1
+ORDER BY b;
+ a | b 
+---+---
+ 1 | 1
+ 1 | 3
+ 1 | 5
+ 1 | 7
+ 1 | 9
+(5 rows)
+
+SET enable_partitionwise_aggregate = on;
+EXPLAIN (COSTS OFF)
+SELECT (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) AS k, count(*)
+FROM null_saop_part GROUP BY 1 ORDER BY 1;
+                                                         QUERY PLAN                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ Merge Append
+   Sort Key: (CASE WHEN (null_saop_part.b <> ALL ('{42,NULL}'::integer[])) THEN 0 ELSE null_saop_part.a END)
+   ->  GroupAggregate
+         Group Key: (CASE WHEN (null_saop_part.b <> ALL ('{42,NULL}'::integer[])) THEN 0 ELSE null_saop_part.a END)
+         ->  Sort
+               Sort Key: (CASE WHEN (null_saop_part.b <> ALL ('{42,NULL}'::integer[])) THEN 0 ELSE null_saop_part.a END)
+               ->  Seq Scan on null_saop_part0 null_saop_part
+   ->  GroupAggregate
+         Group Key: (CASE WHEN (null_saop_part_1.b <> ALL ('{42,NULL}'::integer[])) THEN 0 ELSE null_saop_part_1.a END)
+         ->  Sort
+               Sort Key: (CASE WHEN (null_saop_part_1.b <> ALL ('{42,NULL}'::integer[])) THEN 0 ELSE null_saop_part_1.a END)
+               ->  Seq Scan on null_saop_part1 null_saop_part_1
+(12 rows)
+
+SELECT (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) AS k, count(*)
+FROM null_saop_part GROUP BY 1 ORDER BY 1;
+ k | count 
+---+-------
+ 0 |     5
+ 1 |     5
+(2 rows)
+
+RESET enable_partitionwise_aggregate;
+SET constraint_exclusion = on;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_part1
+WHERE (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) = 0;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   Replaces: Scan on null_saop_part1
+   One-Time Filter: false
+(3 rows)
+
+RESET constraint_exclusion;
+DROP TABLE null_saop_part;
+DROP TABLE null_saop_tab;
+-- Keep IS NOT TRUE quals so identical partial index predicates remain provable,
+-- including when the BooleanTest is an arm of an OR.
+CREATE TABLE null_saop_pi (a int, b int, c int);
+INSERT INTO null_saop_pi
+SELECT i, i % 100, i % 1000 FROM generate_series(1, 100000) i;
+CREATE INDEX null_saop_pi_c ON null_saop_pi (c)
+WHERE (b NOT IN (42, NULL)) IS NOT TRUE;
+CREATE INDEX null_saop_pi_a ON null_saop_pi (a)
+WHERE b = 1 OR (b NOT IN (42, NULL)) IS NOT TRUE;
+ANALYZE null_saop_pi;
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_pi
+WHERE (b NOT IN (42, NULL)) IS NOT TRUE AND c = 5;
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Bitmap Heap Scan on null_saop_pi
+   Recheck Cond: ((c = 5) AND ((b <> ALL ('{42,NULL}'::integer[])) IS NOT TRUE))
+   ->  Bitmap Index Scan on null_saop_pi_c
+         Index Cond: (c = 5)
+(4 rows)
+
+SELECT count(*) FROM null_saop_pi
+WHERE (b NOT IN (42, NULL)) IS NOT TRUE AND c = 5;
+ count 
+-------
+   100
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_pi
+WHERE (b = 1 OR (b NOT IN (42, NULL)) IS NOT TRUE) AND a = 5;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Scan using null_saop_pi_a on null_saop_pi
+   Index Cond: (a = 5)
+(2 rows)
+
+SELECT * FROM null_saop_pi
+WHERE (b = 1 OR (b NOT IN (42, NULL)) IS NOT TRUE) AND a = 5;
+ a | b | c 
+---+---+---
+ 5 | 5 | 5
+(1 row)
+
+RESET enable_seqscan;
+DROP TABLE null_saop_pi;
+-- A non-strict operator can return TRUE for a NULL element, but a NULL
+-- array still yields NULL for both ANY and ALL.
+CREATE FUNCTION null_saop_nonstrict(int, int) RETURNS boolean
+LANGUAGE plpgsql IMMUTABLE CALLED ON NULL INPUT
+AS $$ BEGIN RETURN true; END $$;
+CREATE OPERATOR === (
+  LEFTARG = int, RIGHTARG = int, FUNCTION = null_saop_nonstrict
+);
+CREATE TABLE null_saop_ns (a int);
+INSERT INTO null_saop_ns VALUES (1), (NULL);
+SELECT count(*) FROM null_saop_ns WHERE a === ALL (ARRAY[42, NULL]);
+ count 
+-------
+     2
+(1 row)
+
+SELECT count(*) FROM null_saop_ns WHERE a === ANY (ARRAY[42, NULL]);
+ count 
+-------
+     2
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_ns WHERE a === ALL (NULL::int[]);
+            QUERY PLAN            
+----------------------------------
+ Result
+   Replaces: Scan on null_saop_ns
+   One-Time Filter: false
+(3 rows)
+
+SELECT count(*) FROM null_saop_ns WHERE a === ALL (NULL::int[]);
+ count 
+-------
+     0
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_ns WHERE a === ANY (NULL::int[]);
+            QUERY PLAN            
+----------------------------------
+ Result
+   Replaces: Scan on null_saop_ns
+   One-Time Filter: false
+(3 rows)
+
+SELECT count(*) FROM null_saop_ns WHERE a === ANY (NULL::int[]);
+ count 
+-------
+     0
+(1 row)
+
+-- Strict ANY must retain a possible TRUE match despite a NULL element.
+SELECT count(*) FROM null_saop_ns WHERE a = ANY (ARRAY[1, NULL]);
+ count 
+-------
+     1
+(1 row)
+
+DROP TABLE null_saop_ns;
+DROP OPERATOR === (int, int);
+DROP FUNCTION null_saop_nonstrict(int, int);
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 4512670d525..b92958c570b 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -310,6 +310,21 @@ SELECT * FROM check_estimated_rows('SELECT a + 1, b FROM ONLY stxdinp GROUP BY 1
 (1 row)
 
 DROP TABLE stxdinp;
+-- a statistics expression that the planner could simplify where NULL and
+-- FALSE are equivalent must still match the same expression in a query
+CREATE TABLE stxdnull (a int, b int, c int) WITH (autovacuum_enabled = off);
+INSERT INTO stxdnull SELECT i % 100, i % 7, i % 100
+  FROM generate_series(1, 10000) i;
+CREATE STATISTICS stxdnull_s (mcv)
+  ON (CASE WHEN b NOT IN (42, NULL) THEN 1 ELSE a END), c FROM stxdnull;
+ANALYZE stxdnull;
+SELECT * FROM check_estimated_rows('SELECT * FROM stxdnull WHERE (CASE WHEN b NOT IN (42, NULL) THEN 1 ELSE a END) = 1 AND c = 1');
+ estimated | actual 
+-----------+--------
+       100 |    100
+(1 row)
+
+DROP TABLE stxdnull;
 -- basic test for statistics on expressions
 CREATE TABLE ab1 (a INTEGER, b INTEGER, c TIMESTAMP, d TIMESTAMPTZ);
 -- expression stats may be built on a single expression column
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index 88ca3c80875..4ebb013a8e7 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -921,10 +921,18 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('
 
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{7, 14, 22}') and unique1 = ANY('{33, 44}'::bigint[]);
 
+-- Use a generic plan, since a constant NULL array is folded away by the planner
+SET plan_cache_mode = force_generic_plan;
+PREPARE null_array_key(int[]) AS
+SELECT unique1 FROM tenk1 WHERE unique1 = ANY($1);
+
 explain (costs off)
-SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+EXECUTE null_array_key(NULL);
+
+EXECUTE null_array_key(NULL);
 
-SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
+DEALLOCATE null_array_key;
+RESET plan_cache_mode;
 
 explain (costs off)
 SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql
index 8539c49c939..8835a398e11 100644
--- a/src/test/regress/sql/insert_conflict.sql
+++ b/src/test/regress/sql/insert_conflict.sql
@@ -264,6 +264,27 @@ insert into insertconflicttest values (23, 'Blackberry') on conflict (fruit) whe
 
 drop index partial_key_index;
 
+-- The inference WHERE clause must not be reduced to constant-FALSE even
+-- though it can never be true, else it no longer matches the index predicate
+create unique index null_saop_key_index on insertconflicttest(key) where fruit <> all (array['Apple', null]);
+
+-- Succeeds
+explain (costs off)
+insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing;
+insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing;
+insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) and fruit like '%berry' do nothing;
+
+drop index null_saop_key_index;
+delete from insertconflicttest where key = 1001;
+
+-- Nor may an expression in the inference specification be rewritten so that
+-- it no longer matches the index expression
+create unique index null_saop_expr_index on insertconflicttest
+  ((case when fruit not in ('Apple', null) then 1001 else key end));
+insert into insertconflicttest values (1002, 'Lime') on conflict ((case when fruit not in ('Apple', null) then 1001 else key end)) do nothing;
+drop index null_saop_expr_index;
+delete from insertconflicttest where key = 1002;
+
 --
 -- Test that wholerow references to ON CONFLICT's EXCLUDED work
 --
diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql
index 9dd236efb56..a1ff5cb9ed1 100644
--- a/src/test/regress/sql/predicate.sql
+++ b/src/test/regress/sql/predicate.sql
@@ -444,3 +444,234 @@ SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN;
 SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN;
 
 DROP TABLE bool_tab;
+
+--
+-- Test that "x op ALL (array)" with a strict operator and a NULL array
+-- element is reduced to constant-FALSE where NULL and FALSE are equivalent
+--
+CREATE TABLE null_saop_tab (a int, b int, arr int[]);
+INSERT INTO null_saop_tab VALUES (1, 1, NULL), (42, 2, '{}'), (NULL, 3, NULL);
+ANALYZE null_saop_tab;
+
+-- Ensure NOT IN and <> ALL are reduced to constant-FALSE
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL);
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL);
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]);
+SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]);
+
+-- Ensure equivalent forms are reduced to constant-FALSE as well
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL));
+SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL));
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL]));
+SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL]));
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL));
+SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL));
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true;
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true;
+
+CREATE FUNCTION null_saop_func(int) RETURNS bool LANGUAGE sql IMMUTABLE
+AS $$ SELECT $1 NOT IN (42, NULL) $$;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE null_saop_func(a);
+SELECT * FROM null_saop_tab WHERE null_saop_func(a);
+DROP FUNCTION null_saop_func(int);
+
+-- Ensure it is reduced under AND and OR
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1;
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1;
+SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab
+WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL)));
+SELECT * FROM null_saop_tab
+WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL)));
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0;
+SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0;
+
+-- Ensure it is reduced in a join clause
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2
+  ON t1.a = t2.a AND t2.b NOT IN (42, NULL);
+SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2
+  ON t1.a = t2.a AND t2.b NOT IN (42, NULL)
+ORDER BY t1.b;
+
+-- Ensure it is reduced in FILTER clauses
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab;
+SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER ()
+FROM null_saop_tab;
+SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER ()
+FROM null_saop_tab;
+
+-- Ensure it is not reduced under NOT
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL));
+SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL));
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END
+FROM null_saop_tab;
+SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END
+FROM null_saop_tab;
+
+-- Ensure it is not reduced where NULL and FALSE are not equivalent
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, a NOT IN (42, NULL) FROM null_saop_tab;
+SELECT b, a NOT IN (42, NULL) FROM null_saop_tab;
+
+-- Ensure it is not reduced in CASE WHEN conditions or under IS [NOT] TRUE
+-- outside of a qual, so that such expressions still match equal expressions
+-- stored in the catalogs
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END,
+  (a NOT IN (42, NULL)) IS TRUE, (a NOT IN (42, NULL)) IS NOT TRUE
+FROM null_saop_tab;
+SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END,
+  (a NOT IN (42, NULL)) IS TRUE, (a NOT IN (42, NULL)) IS NOT TRUE
+FROM null_saop_tab;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab
+WHERE (CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE b END) = 1;
+SELECT * FROM null_saop_tab
+WHERE (CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE b END) = 1;
+
+-- Ensure it is not reduced for a multidimensional ARRAY[]: NULL sub-arrays
+-- don't add NULL elements, and here the array is empty, so ALL is true
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b;
+SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b;
+
+-- Ensure IS TRUE is reduced to constant-FALSE, while IS NOT TRUE and
+-- IS FALSE are not reduced
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS TRUE;
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS TRUE;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL) AND b > 0) IS NOT TRUE
+ORDER BY b;
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL) AND b > 0) IS NOT TRUE
+ORDER BY b;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS FALSE;
+SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS FALSE;
+
+-- Ensure a NULL array is reduced to constant-FALSE, for ANY as well as ALL
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a <> ALL (NULL::int[]);
+SELECT * FROM null_saop_tab WHERE a <> ALL (NULL::int[]);
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_tab WHERE a = ANY (NULL::int[]) OR b = 1;
+SELECT * FROM null_saop_tab WHERE a = ANY (NULL::int[]) OR b = 1;
+
+-- Ensure expressions checked for constness at DDL time are not reduced
+CREATE TABLE null_saop_part (a int)
+  PARTITION BY LIST ((CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END));
+DROP TABLE null_saop_part;
+
+-- Ensure a partition key containing such an expression still matches the
+-- query, for partition pruning, partitionwise aggregation and constraint
+-- exclusion
+CREATE TABLE null_saop_part (a int, b int)
+  PARTITION BY LIST ((CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END));
+CREATE TABLE null_saop_part0 PARTITION OF null_saop_part FOR VALUES IN (0);
+CREATE TABLE null_saop_part1 PARTITION OF null_saop_part FOR VALUES IN (1);
+INSERT INTO null_saop_part SELECT i % 2, i FROM generate_series(1, 10) i;
+ANALYZE null_saop_part;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_part
+WHERE (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) = 1;
+SELECT * FROM null_saop_part
+WHERE (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) = 1
+ORDER BY b;
+
+SET enable_partitionwise_aggregate = on;
+EXPLAIN (COSTS OFF)
+SELECT (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) AS k, count(*)
+FROM null_saop_part GROUP BY 1 ORDER BY 1;
+SELECT (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) AS k, count(*)
+FROM null_saop_part GROUP BY 1 ORDER BY 1;
+RESET enable_partitionwise_aggregate;
+
+SET constraint_exclusion = on;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_part1
+WHERE (CASE WHEN b NOT IN (42, NULL) THEN 0 ELSE a END) = 0;
+RESET constraint_exclusion;
+
+DROP TABLE null_saop_part;
+
+DROP TABLE null_saop_tab;
+
+-- Keep IS NOT TRUE quals so identical partial index predicates remain provable,
+-- including when the BooleanTest is an arm of an OR.
+CREATE TABLE null_saop_pi (a int, b int, c int);
+INSERT INTO null_saop_pi
+SELECT i, i % 100, i % 1000 FROM generate_series(1, 100000) i;
+CREATE INDEX null_saop_pi_c ON null_saop_pi (c)
+WHERE (b NOT IN (42, NULL)) IS NOT TRUE;
+CREATE INDEX null_saop_pi_a ON null_saop_pi (a)
+WHERE b = 1 OR (b NOT IN (42, NULL)) IS NOT TRUE;
+ANALYZE null_saop_pi;
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_pi
+WHERE (b NOT IN (42, NULL)) IS NOT TRUE AND c = 5;
+SELECT count(*) FROM null_saop_pi
+WHERE (b NOT IN (42, NULL)) IS NOT TRUE AND c = 5;
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_pi
+WHERE (b = 1 OR (b NOT IN (42, NULL)) IS NOT TRUE) AND a = 5;
+SELECT * FROM null_saop_pi
+WHERE (b = 1 OR (b NOT IN (42, NULL)) IS NOT TRUE) AND a = 5;
+RESET enable_seqscan;
+DROP TABLE null_saop_pi;
+
+-- A non-strict operator can return TRUE for a NULL element, but a NULL
+-- array still yields NULL for both ANY and ALL.
+CREATE FUNCTION null_saop_nonstrict(int, int) RETURNS boolean
+LANGUAGE plpgsql IMMUTABLE CALLED ON NULL INPUT
+AS $$ BEGIN RETURN true; END $$;
+CREATE OPERATOR === (
+  LEFTARG = int, RIGHTARG = int, FUNCTION = null_saop_nonstrict
+);
+CREATE TABLE null_saop_ns (a int);
+INSERT INTO null_saop_ns VALUES (1), (NULL);
+SELECT count(*) FROM null_saop_ns WHERE a === ALL (ARRAY[42, NULL]);
+SELECT count(*) FROM null_saop_ns WHERE a === ANY (ARRAY[42, NULL]);
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_ns WHERE a === ALL (NULL::int[]);
+SELECT count(*) FROM null_saop_ns WHERE a === ALL (NULL::int[]);
+EXPLAIN (COSTS OFF)
+SELECT * FROM null_saop_ns WHERE a === ANY (NULL::int[]);
+SELECT count(*) FROM null_saop_ns WHERE a === ANY (NULL::int[]);
+-- Strict ANY must retain a possible TRUE match despite a NULL element.
+SELECT count(*) FROM null_saop_ns WHERE a = ANY (ARRAY[1, NULL]);
+DROP TABLE null_saop_ns;
+DROP OPERATOR === (int, int);
+DROP FUNCTION null_saop_nonstrict(int, int);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index a9176e74f3c..b0eb4a464e9 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -180,6 +180,17 @@ SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinp GROUP BY 1, 2');
 SELECT * FROM check_estimated_rows('SELECT a + 1, b FROM ONLY stxdinp GROUP BY 1, 2');
 DROP TABLE stxdinp;
 
+-- a statistics expression that the planner could simplify where NULL and
+-- FALSE are equivalent must still match the same expression in a query
+CREATE TABLE stxdnull (a int, b int, c int) WITH (autovacuum_enabled = off);
+INSERT INTO stxdnull SELECT i % 100, i % 7, i % 100
+  FROM generate_series(1, 10000) i;
+CREATE STATISTICS stxdnull_s (mcv)
+  ON (CASE WHEN b NOT IN (42, NULL) THEN 1 ELSE a END), c FROM stxdnull;
+ANALYZE stxdnull;
+SELECT * FROM check_estimated_rows('SELECT * FROM stxdnull WHERE (CASE WHEN b NOT IN (42, NULL) THEN 1 ELSE a END) = 1 AND c = 1');
+DROP TABLE stxdnull;
+
 -- basic test for statistics on expressions
 CREATE TABLE ab1 (a INTEGER, b INTEGER, c TIMESTAMP, d TIMESTAMPTZ);
 
-- 
2.34.1

