Hello, hackers!

A few days ago Karina Litskevich noticed that strict function with variadic argument containing a few values, only one of which was NULL, returned NULL without evaluating the function itself. It didn't match with documented behaviour:

"If a function is declared STRICT with a VARIADIC argument, the strictness check tests that the variadic array as a whole is non-null. The function will still be called if the array has null elements."

After some digging it turned out that since VARIADIC "any" argument contents are not (can not?) transformed into single array (due to being possibly of different types), the corresponding parameters are checked in evaluate_function individually. Thus the following check

        /*
* If the function is strict and has a constant-NULL input, it will never * be called at all, so we can replace the call by a NULL constant, even
         * if there are other inputs that aren't constant, and even if the
         * function is not otherwise immutable.
         */
        if (funcform->proisstrict && has_null_input)
                return (Expr *) makeNullConst(result_type, result_typmod,
                                                                          
result_collid);
Passes successfully.

To see this bug in action it is enough to create a few functions with variadic arguments (i used a small contrib to pack it neatly):
=====================
/* contrib/test/test--1.0.sql */

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION test" to load this file. \quit

CREATE FUNCTION test_variadic_any(VARIADIC "any")
RETURNS text
AS 'MODULE_PATHNAME'
LANGUAGE C;

CREATE FUNCTION test_variadic_any_strict(VARIADIC "any")
RETURNS text
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

CREATE FUNCTION test_variadic_int_strict(VARIADIC b int[])
RETURNS text
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

CREATE FUNCTION test_variadic_int(VARIADIC b int[])
RETURNS text
AS 'MODULE_PATHNAME'
LANGUAGE C;

=================
Functions (it is enough to just have them to return something not-NULL, just wanted to see what extract_variadic_args will return:

PG_FUNCTION_INFO_V1(test_variadic_any);
Datum
test_variadic_any(PG_FUNCTION_ARGS)
{
        int nargs;
        Datum *args;
        bool *nulls;
        Oid *types;

        nargs = extract_variadic_args(fcinfo, 0, true, &args, &types, &nulls);

        if (nargs < 0)
                PG_RETURN_TEXT_P(cstring_to_text("is_null"));

        PG_RETURN_TEXT_P(cstring_to_text("OK"));
}

PG_FUNCTION_INFO_V1(test_variadic_any_strict);
Datum
test_variadic_any_strict(PG_FUNCTION_ARGS)
{
        int nargs;
        Datum *args;
        bool *nulls;
        Oid *types;

        nargs = extract_variadic_args(fcinfo, 0, true, &args, &types, &nulls);

        if (nargs < 0)
                PG_RETURN_TEXT_P(cstring_to_text("is_null"));

        PG_RETURN_TEXT_P(cstring_to_text("OK"));
}

PG_FUNCTION_INFO_V1(test_variadic_int);
Datum
test_variadic_int(PG_FUNCTION_ARGS)
{
        int nargs;
        Datum *args;
        bool *nulls;
        Oid *types;

        nargs = extract_variadic_args(fcinfo, 0, true, &args, &types, &nulls);

        if (nargs < 0)
                PG_RETURN_TEXT_P(cstring_to_text("is_null"));

        PG_RETURN_TEXT_P(cstring_to_text("OK"));
}

PG_FUNCTION_INFO_V1(test_variadic_int_strict);
Datum
test_variadic_int_strict(PG_FUNCTION_ARGS)
{
        int nargs;
        Datum *args;
        bool *nulls;
        Oid *types;

        nargs = extract_variadic_args(fcinfo, 0, true, &args, &types, &nulls);

        if (nargs < 0)
                PG_RETURN_TEXT_P(cstring_to_text("is_null"));

        PG_RETURN_TEXT_P(cstring_to_text("OK"));
}

=================
Output:

postgres=# select test_variadic_int_strict(1, 2, NULL);
 test_variadic_int_strict
--------------------------
 OK
(1 row)

postgres=# select test_variadic_any_strict(1, 2, NULL);
 test_variadic_any_strict
--------------------------

(1 row)

=================
Debugger showed that for these tests in evaluate_function for VARIADIC "any" case while funcform->pronargs = 1, actual amount of args is three, whereas for VARIADIC int[] funcform->pronargs = 1 and args list consists from one argument (array of int).

Attached is a small patch which partially fixes this place in evaluate_function by using funcform->provariadic and difference in args list length and funcform->pronargs to improve this check to work at least a little more correctly.

This version is not distinguishing between VARIADIC NULL::int[] and NULL for VARIADIC "any" case - extract_variadic_args (by the way, it seems that the comment to this function is not correct for VARIADIC "any" case, since the variadic array is not constructed... The code itself works correctly with VARIADIC "any".) thinks that the second is array from one (NULL) element, while the first is real NULL (probably right):
=================
--no patch

postgres=# select test_variadic_any(NULL);
 test_variadic_any
-------------------
 OK
(1 row)

postgres=# select test_variadic_any(VARIADIC NULL::int[]);
 test_variadic_any
-------------------
 is_null
(1 row)

postgres=# select test_variadic_int(NULL);
 test_variadic_int
-------------------
 OK
(1 row)

postgres=# select test_variadic_int(VARIADIC NULL::int[]);
 test_variadic_int
-------------------
 is_null
(1 row)

In evaluate_function these cases look pretty similar. Probably should check consttype of elements of args list, since for simple NULL it will be "unknown", and for VARIADIC NULL::int[] (or any other type) it will be this type. Not sure, how to do it correctly.

The second and the bigger issue is that even with fixing evaluate_function the result is still the same, since ExecInterpExpr does the same check as evaluate_function:


postgres=# select test_variadic_any_strict(1, 2, NULL);
 test_variadic_any_strict
--------------------------

(1 row)
============================

                /* strict function call with more than two arguments */
                EEO_CASE(EEOP_FUNCEXPR_STRICT)
                {
                        FunctionCallInfo fcinfo = op->d.func.fcinfo_data;
                        NullableDatum *args = fcinfo->args;
                        int                     nargs = op->d.func.nargs;
                        Datum           d;

                        Assert(nargs > 2);

                        /* strict function, so check for NULL args */
                        for (int argno = 0; argno < nargs; argno++)
                        {
                                if (args[argno].isnull)
                                {
                                        *op->resnull = true;
                                        goto strictfail;
                                }
                        }
                        fcinfo->isnull = false;
                        d = op->d.func.fn_addr(fcinfo);
                        *op->resvalue = d;
                        *op->resnull = fcinfo->isnull;

(And nargs here is three, get_fn_expr_variadic returns false, so no sign (or I don't see one) that we are actually working with VARIADIC "any" argument).

I am not quite sure where to go from here, since in ExecInterpExpr there is no (obvious for me) way to know if we are working with VARIADIC "any" argument. If not fixed (or not considered a bug), at least this behaviour should probably be mentioned in documentation for CREATE FUNCTION.

Thoughts?

With best regards,
Svetlana Derevyanko

Postgres Professional: http://postgrespro.com
From d046a4bea07a09b2e5bc519b16b331713523ee65 Mon Sep 17 00:00:00 2001
From: Svetlana Derevyanko <[email protected]>
Date: Fri, 5 Dec 2025 10:37:17 +0300
Subject: [PATCH v1] Teach STRICT functions with VARIADIC "any" arguments to
 distinguish between NULL element and NULL array

---
 src/backend/optimizer/util/clauses.c | 27 ++++++++++++++++++++++++++-
 1 file changed, 26 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index bda4c4eb292..4a261866d95 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4616,6 +4616,8 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
 	Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
 	bool		has_nonconst_input = false;
 	bool		has_null_input = false;
+	bool		has_variadic_null = false;
+	int			i = 0;
 	ListCell   *arg;
 	FuncExpr   *newexpr;
 
@@ -4644,11 +4646,34 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
 	foreach(arg, args)
 	{
 		if (IsA(lfirst(arg), Const))
-			has_null_input |= ((Const *) lfirst(arg))->constisnull;
+		{
+			if (i < funcform->pronargs - 1)
+				has_null_input |= ((Const *) lfirst(arg))->constisnull;
+			else
+			{
+				/*
+				 * For VARIADIC "any" last args are not constructed into an
+				 * array, but funcform->pronargs is calculated as if they
+				 * were. However, we still want has_null_input to not be set
+				 * in true if null argument is part of the VARIADIC array.
+				 */
+				if (funcform->provariadic)
+				{
+					has_variadic_null |= ((Const *) lfirst(arg))->constisnull;
+				}
+				else			/* last argument for non-variadic */
+					has_null_input |= ((Const *) lfirst(arg))->constisnull;
+			}
+		}
 		else
 			has_nonconst_input = true;
+		i++;
 	}
 
+	/* VARIADIC "any" argument is null. */
+	if (has_variadic_null && (i == funcform->pronargs))
+		has_null_input |= has_variadic_null;
+
 	/*
 	 * If the function is strict and has a constant-NULL input, it will never
 	 * be called at all, so we can replace the call by a NULL constant, even
-- 
2.34.1

Reply via email to