From d3d45ec160d56e97e586f56cdc788ca22f7ea5cc Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Thu, 3 Sep 2026 10:23:56 +0900
Subject: [PATCH v2 2/2] Don't let JSON constructor coercions block SQL
 function inlining

contain_context_dependent_node_walker() reports a CaseTestExpr as
context-dependent unless it sits under a simple CaseExpr or the
elemexpr of an ArrayCoerceExpr.  A JsonConstructorExpr whose RETURNING
type requires a coercion also carries a CaseTestExpr placeholder in
that coercion, so any SQL function called with such a constructor as
an argument was refused inlining.

Teach the walker that a CaseTestExpr is expected within the coercion
of a JsonConstructorExpr, the same way it already handles the elemexpr
of an ArrayCoerceExpr.  This is safe now that eval_const_expressions
no longer lets an enclosing simple CASE clobber that placeholder.
---
 src/backend/optimizer/util/clauses.c  | 23 ++++++++++++++++++++++-
 src/test/regress/expected/sqljson.out | 18 ++++++++++++++++++
 src/test/regress/sql/sqljson.sql      |  8 ++++++++
 3 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 9a065564e9b..5d7e868eaa3 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1255,7 +1255,8 @@ contain_exec_param_walker(Node *node, List *param_ids)
  * not nested within another one, or they'll see the wrong test value.  If one
  * appears "bare" in the arguments of a SQL function, then we can't inline the
  * SQL function for fear of creating such a situation.  The same applies for
- * CaseTestExpr used within the elemexpr of an ArrayCoerceExpr.
+ * CaseTestExpr used within the elemexpr of an ArrayCoerceExpr or the coercion
+ * of a JsonConstructorExpr.
  *
  * CoerceToDomainValue would have the same issue if domain CHECK expressions
  * could get inlined into larger expressions, but presently that's impossible.
@@ -1328,6 +1329,26 @@ contain_context_dependent_node_walker(Node *node, int *flags)
 		*flags = save_flags;
 		return res;
 	}
+	else if (IsA(node, JsonConstructorExpr))
+	{
+		JsonConstructorExpr *jce = (JsonConstructorExpr *) node;
+		int			save_flags;
+		bool		res;
+
+		/* Check the args and func expressions */
+		if (contain_context_dependent_node_walker((Node *) jce->args, flags))
+			return true;
+		if (contain_context_dependent_node_walker((Node *) jce->func, flags))
+			return true;
+
+		/* Check the coercion, which is allowed to contain CaseTestExpr */
+		save_flags = *flags;
+		*flags |= CCDN_CASETESTEXPR_OK;
+		res = contain_context_dependent_node_walker((Node *) jce->coercion,
+													flags);
+		*flags = save_flags;
+		return res;
+	}
 	return expression_tree_walker(node, contain_context_dependent_node_walker,
 								  flags);
 }
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index d3cab7ae16c..30f9e10ef0a 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -580,6 +580,24 @@ SELECT CASE 'x' WHEN JSON_OBJECT('a': 'b' RETURNING text) THEN 1 ELSE 0 END;
     0
 (1 row)
 
+-- the RETURNING coercion must not prevent inlining of a SQL function
+CREATE FUNCTION json_object_inline_test(text) RETURNS text
+LANGUAGE sql IMMUTABLE AS $$ SELECT $1 || '!' $$;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT json_object_inline_test(JSON_OBJECT('a': 'b' RETURNING text));
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Result
+   Output: (JSON_OBJECT('a' : 'b'::text RETURNING text) || '!'::text)
+(2 rows)
+
+SELECT json_object_inline_test(JSON_OBJECT('a': 'b' RETURNING text));
+ json_object_inline_test 
+-------------------------
+ {"a" : "b"}!
+(1 row)
+
+DROP FUNCTION json_object_inline_test(text);
 -- BUG: https://postgr.es/m/CADXhmgTJtJZK9A3Na_ry%2BXrq-ghjcejBRhcRMzWZvbd__QdgJA%40mail.gmail.com
 -- datum_to_jsonb_internal() didn't catch keys that are casts instead of a simple scalar
 CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');
diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql
index 7b50cbf9d42..9fa203ccb53 100644
--- a/src/test/regress/sql/sqljson.sql
+++ b/src/test/regress/sql/sqljson.sql
@@ -155,6 +155,14 @@ SELECT JSON_OBJECT(1: 1, '2': NULL, '3': 1, 4: NULL, '5': 'a' ABSENT ON NULL WIT
 -- the RETURNING coercion must not pick up the test value of an enclosing CASE
 SELECT CASE 'x' WHEN JSON_OBJECT('a': 'b' RETURNING text) THEN 1 ELSE 0 END;
 
+-- the RETURNING coercion must not prevent inlining of a SQL function
+CREATE FUNCTION json_object_inline_test(text) RETURNS text
+LANGUAGE sql IMMUTABLE AS $$ SELECT $1 || '!' $$;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT json_object_inline_test(JSON_OBJECT('a': 'b' RETURNING text));
+SELECT json_object_inline_test(JSON_OBJECT('a': 'b' RETURNING text));
+DROP FUNCTION json_object_inline_test(text);
+
 -- BUG: https://postgr.es/m/CADXhmgTJtJZK9A3Na_ry%2BXrq-ghjcejBRhcRMzWZvbd__QdgJA%40mail.gmail.com
 -- datum_to_jsonb_internal() didn't catch keys that are casts instead of a simple scalar
 CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');
-- 
2.37.1 (Apple Git-137.1)

