From b1e1b530f713e617a755d1b0ff20bda1e375dbad Mon Sep 17 00:00:00 2001 From: Ayush Tiwari 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