From b1e1b530f713e617a755d1b0ff20bda1e375dbad Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Tue, 28 Jul 2026 19:23:02 +0530
Subject: [PATCH] Fix SAOP strictness check for SQL function inlining

contain_nonstrict_functions() relies on check_functions_in_node() to
check most function-containing expression nodes.  For a
ScalarArrayOpExpr, however, checking only the operator function is not
enough to establish strictness.  If the array is empty, a NULL scalar
input produces FALSE for ANY or TRUE for ALL, rather than NULL.

Consequently, a STRICT SQL function containing such an expression could
be judged strict and inlined.  The inlined expression then bypassed the
function's NULL-on-NULL-input behavior and could produce a wrong answer.

Before 2f153ddfdd3, contain_nonstrict_functions() handled SAOPs
specially using is_strict_saop() with falseOK = false.  Restore that
check, while still examining the SAOP arguments when the expression is
provably strict.

Add regression tests covering empty-array ANY and ALL expressions in
STRICT SQL functions.
---
 src/backend/optimizer/util/clauses.c          |  8 +++++++
 .../regress/expected/create_function_sql.out  | 22 +++++++++++++++++++
 src/test/regress/sql/create_function_sql.sql  | 21 ++++++++++++++++++
 3 files changed, 51 insertions(+)

diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index aa8886ec210..6692b16e29b 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1122,6 +1122,14 @@ contain_nonstrict_functions_walker(Node *node, void *context)
 		return contain_nonstrict_functions_walker((Node *) ((ArrayCoerceExpr *) node)->arg,
 												  context);
 	}
+	if (IsA(node, ScalarArrayOpExpr))
+	{
+		ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node;
+
+		if (!is_strict_saop(expr, false))
+			return true;
+		/* else fall through to check args */
+	}
 	if (IsA(node, CaseExpr))
 		return true;
 	if (IsA(node, ArrayExpr))
diff --git a/src/test/regress/expected/create_function_sql.out b/src/test/regress/expected/create_function_sql.out
index 42524230d2b..bcaa67f7860 100644
--- a/src/test/regress/expected/create_function_sql.out
+++ b/src/test/regress/expected/create_function_sql.out
@@ -729,6 +729,28 @@ TABLE ddl_test;
 
 RESET check_function_bodies;
 -- Regression tests for bugs:
+-- Check that inlining a strict SQL function preserves its null-input behavior
+CREATE TEMP TABLE saop_inline_test (x int);
+INSERT INTO saop_inline_test VALUES (NULL), (1);
+CREATE FUNCTION saop_strict_any(int) RETURNS bool
+LANGUAGE SQL STRICT IMMUTABLE AS
+$$ SELECT $1 = ANY ('{}'::int[]) $$;
+CREATE FUNCTION saop_strict_all(int) RETURNS bool
+LANGUAGE SQL STRICT IMMUTABLE AS
+$$ SELECT $1 <> ALL ('{}'::int[]) $$;
+SELECT x IS NULL AS null_input,
+       saop_strict_any(x) IS NULL AS any_is_null,
+       saop_strict_all(x) IS NULL AS all_is_null
+FROM saop_inline_test
+ORDER BY x NULLS FIRST;
+ null_input | any_is_null | all_is_null 
+------------+-------------+-------------
+ t          | t           | t
+ f          | f           | f
+(2 rows)
+
+DROP FUNCTION saop_strict_any(int), saop_strict_all(int);
+DROP TABLE saop_inline_test;
 -- Check that arguments that are R/W expanded datums aren't corrupted by
 -- multiple uses.  This test knows that array_append() returns a R/W datum
 -- and will modify a R/W array input in-place.  We use SETOF to prevent
diff --git a/src/test/regress/sql/create_function_sql.sql b/src/test/regress/sql/create_function_sql.sql
index 4543273f93a..707db3afe32 100644
--- a/src/test/regress/sql/create_function_sql.sql
+++ b/src/test/regress/sql/create_function_sql.sql
@@ -432,6 +432,27 @@ RESET check_function_bodies;
 
 -- Regression tests for bugs:
 
+-- Check that inlining a strict SQL function preserves its null-input behavior
+CREATE TEMP TABLE saop_inline_test (x int);
+INSERT INTO saop_inline_test VALUES (NULL), (1);
+
+CREATE FUNCTION saop_strict_any(int) RETURNS bool
+LANGUAGE SQL STRICT IMMUTABLE AS
+$$ SELECT $1 = ANY ('{}'::int[]) $$;
+
+CREATE FUNCTION saop_strict_all(int) RETURNS bool
+LANGUAGE SQL STRICT IMMUTABLE AS
+$$ SELECT $1 <> ALL ('{}'::int[]) $$;
+
+SELECT x IS NULL AS null_input,
+       saop_strict_any(x) IS NULL AS any_is_null,
+       saop_strict_all(x) IS NULL AS all_is_null
+FROM saop_inline_test
+ORDER BY x NULLS FIRST;
+
+DROP FUNCTION saop_strict_any(int), saop_strict_all(int);
+DROP TABLE saop_inline_test;
+
 -- Check that arguments that are R/W expanded datums aren't corrupted by
 -- multiple uses.  This test knows that array_append() returns a R/W datum
 -- and will modify a R/W array input in-place.  We use SETOF to prevent
-- 
2.34.1

