On 2026-01-08 Th 2:05 AM, jian he wrote:
hi.

rebase due to conflict in
https://git.postgresql.org/cgit/postgresql.git/commit/?id=ba75f717526cbaa9000b546aac456e43d03aaf53



Here's a rework of this patch. It preserves the original signature of to_json{b}_is_immutable, and fixes some code duplication. It also uses the typecache to get composite info instead of calling relation_open, supports MultiRange types, and exits early if we made a recursive call (no need for json_categorize_type etc. in these cases).


cheers


andrew


--
Andrew Dunstan
EDB: https://www.enterprisedb.com
From 71b3e509877493bce4d6f94d7d898e85637d92df Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Thu, 5 Mar 2026 16:48:51 -0500
Subject: [PATCH] make to_json, to_jsonb immutability test complete

Complete the TODOs in to_json_is_immutable() and to_jsonb_is_immutable()
by recursing into container types (arrays, composites, ranges, multiranges,
domains) to check element/sub-type mutability, rather than conservatively
returning "mutable" for all arrays and composites.

The shared logic is factored into a single json_check_mutability() function
in jsonfuncs.c, with the existing exported functions as thin wrappers.
Composite type inspection uses lookup_rowtype_tupdesc() (typcache) instead
of relation_open() to avoid unnecessary lock acquisition in the optimizer.

Add comprehensive regression tests for JSON_ARRAY and JSON_OBJECT
mutability with expression indexes and generated columns, covering arrays,
composites, domains, ranges, and combinations thereof.
---
 src/backend/utils/adt/json.c          |  38 +------
 src/backend/utils/adt/jsonb.c         |  38 +------
 src/backend/utils/adt/jsonfuncs.c     | 103 +++++++++++++++++++
 src/include/utils/jsonfuncs.h         |   2 +
 src/test/regress/expected/sqljson.out | 141 ++++++++++++++++++++++++++
 src/test/regress/sql/sqljson.sql      |  73 +++++++++++++
 6 files changed, 325 insertions(+), 70 deletions(-)

diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c
index 0b161398465..52f2ef2875b 100644
--- a/src/backend/utils/adt/json.c
+++ b/src/backend/utils/adt/json.c
@@ -14,7 +14,6 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
-#include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "common/hashfn.h"
 #include "funcapi.h"
@@ -692,45 +691,14 @@ row_to_json_pretty(PG_FUNCTION_ARGS)
 
 /*
  * Is the given type immutable when coming out of a JSON context?
- *
- * At present, datetimes are all considered mutable, because they
- * depend on timezone.  XXX we should also drill down into objects
- * and arrays, but do not.
  */
 bool
 to_json_is_immutable(Oid typoid)
 {
-	JsonTypeCategory tcategory;
-	Oid			outfuncoid;
+	bool		has_mutable = false;
 
-	json_categorize_type(typoid, false, &tcategory, &outfuncoid);
-
-	switch (tcategory)
-	{
-		case JSONTYPE_BOOL:
-		case JSONTYPE_JSON:
-		case JSONTYPE_JSONB:
-		case JSONTYPE_NULL:
-			return true;
-
-		case JSONTYPE_DATE:
-		case JSONTYPE_TIMESTAMP:
-		case JSONTYPE_TIMESTAMPTZ:
-			return false;
-
-		case JSONTYPE_ARRAY:
-			return false;		/* TODO recurse into elements */
-
-		case JSONTYPE_COMPOSITE:
-			return false;		/* TODO recurse into fields */
-
-		case JSONTYPE_NUMERIC:
-		case JSONTYPE_CAST:
-		case JSONTYPE_OTHER:
-			return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE;
-	}
-
-	return false;				/* not reached */
+	json_check_mutability(typoid, false, &has_mutable);
+	return !has_mutable;
 }
 
 /*
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 0a3a77ee786..1b1a8f301f2 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -13,7 +13,6 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
-#include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "libpq/pqformat.h"
@@ -1077,45 +1076,14 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result,
 
 /*
  * Is the given type immutable when coming out of a JSONB context?
- *
- * At present, datetimes are all considered mutable, because they
- * depend on timezone.  XXX we should also drill down into objects and
- * arrays, but do not.
  */
 bool
 to_jsonb_is_immutable(Oid typoid)
 {
-	JsonTypeCategory tcategory;
-	Oid			outfuncoid;
+	bool		has_mutable = false;
 
-	json_categorize_type(typoid, true, &tcategory, &outfuncoid);
-
-	switch (tcategory)
-	{
-		case JSONTYPE_NULL:
-		case JSONTYPE_BOOL:
-		case JSONTYPE_JSON:
-		case JSONTYPE_JSONB:
-			return true;
-
-		case JSONTYPE_DATE:
-		case JSONTYPE_TIMESTAMP:
-		case JSONTYPE_TIMESTAMPTZ:
-			return false;
-
-		case JSONTYPE_ARRAY:
-			return false;		/* TODO recurse into elements */
-
-		case JSONTYPE_COMPOSITE:
-			return false;		/* TODO recurse into fields */
-
-		case JSONTYPE_NUMERIC:
-		case JSONTYPE_CAST:
-		case JSONTYPE_OTHER:
-			return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE;
-	}
-
-	return false;				/* not reached */
+	json_check_mutability(typoid, true, &has_mutable);
+	return !has_mutable;
 }
 
 /*
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index d5b64d7fca5..d22b011ecb1 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -17,6 +17,8 @@
 #include <limits.h>
 
 #include "access/htup_details.h"
+#include "access/tupdesc.h"
+#include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "common/int.h"
 #include "common/jsonapi.h"
@@ -6062,3 +6064,104 @@ json_categorize_type(Oid typoid, bool is_jsonb,
 			break;
 	}
 }
+
+/*
+ * Check whether a type conversion to JSON or JSONB involves any mutable
+ * functions.  This recurses into container types (arrays, composites,
+ * ranges, multiranges, domains) to check their element/sub types.
+ *
+ * The caller must initialize *has_mutable to false before calling.
+ * If any mutable function is found, *has_mutable is set to true.
+ */
+void
+json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable)
+{
+	char		att_typtype = get_typtype(typoid);
+	JsonTypeCategory tcategory;
+	Oid			outfuncoid;
+
+	/* since this function recurses, it could be driven to stack overflow */
+	check_stack_depth();
+
+	Assert(has_mutable != NULL);
+
+	if (*has_mutable)
+		return;
+
+	if (att_typtype == TYPTYPE_DOMAIN)
+	{
+		json_check_mutability(getBaseType(typoid), is_jsonb, has_mutable);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_COMPOSITE)
+	{
+		/*
+		 * For a composite type, recurse into its attributes.  Use the
+		 * typcache to avoid opening the relation directly.
+		 */
+		TupleDesc	tupdesc = lookup_rowtype_tupdesc(typoid, -1);
+
+		for (int i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+			if (attr->attisdropped)
+				continue;
+
+			json_check_mutability(attr->atttypid, is_jsonb, has_mutable);
+		}
+		ReleaseTupleDesc(tupdesc);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_RANGE)
+	{
+		json_check_mutability(get_range_subtype(typoid), is_jsonb,
+							  has_mutable);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_MULTIRANGE)
+	{
+		json_check_mutability(get_multirange_range(typoid), is_jsonb,
+							  has_mutable);
+		return;
+	}
+	else
+	{
+		Oid			att_typelem = get_element_type(typoid);
+
+		if (OidIsValid(att_typelem))
+		{
+			/* recurse into array element type */
+			json_check_mutability(att_typelem, is_jsonb, has_mutable);
+			return;
+		}
+	}
+
+	json_categorize_type(typoid, is_jsonb, &tcategory, &outfuncoid);
+
+	switch (tcategory)
+	{
+		case JSONTYPE_BOOL:
+		case JSONTYPE_JSON:
+		case JSONTYPE_JSONB:
+		case JSONTYPE_NULL:
+			break;
+
+		case JSONTYPE_DATE:
+		case JSONTYPE_TIMESTAMP:
+		case JSONTYPE_TIMESTAMPTZ:
+			*has_mutable = true;
+			break;
+
+		case JSONTYPE_ARRAY:
+		case JSONTYPE_COMPOSITE:
+			break;
+
+		case JSONTYPE_NUMERIC:
+		case JSONTYPE_CAST:
+		case JSONTYPE_OTHER:
+			if (func_volatile(outfuncoid) != PROVOLATILE_IMMUTABLE)
+				*has_mutable = true;
+			break;
+	}
+}
diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h
index 636f0f55840..27713be3aeb 100644
--- a/src/include/utils/jsonfuncs.h
+++ b/src/include/utils/jsonfuncs.h
@@ -83,6 +83,8 @@ typedef enum
 
 extern void json_categorize_type(Oid typoid, bool is_jsonb,
 								 JsonTypeCategory *tcategory, Oid *outfuncoid);
+extern void json_check_mutability(Oid typoid, bool is_jsonb,
+								  bool *has_mutable);
 extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
 						   Oid outfuncoid);
 extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index c7b9e575445..5a5b78e3a16 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -1109,6 +1109,147 @@ CREATE OR REPLACE VIEW public.json_array_subquery_view AS
            FROM ( SELECT foo.i
                    FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i)) q(a)) AS "json_array"
 DROP VIEW json_array_subquery_view;
+-- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT
+create type comp1 as (a int, b date);
+create domain d_comp1 as comp1;
+create domain mydomain as timestamptz;
+create type mydomainrange as range(subtype=mydomain);
+create type comp3 as (a int, b mydomainrange);
+create table test_mutability(
+	a text[], b timestamp, c timestamptz,
+	d date, f1 comp1[], f2 timestamp[],
+	f3 d_comp1[],
+	f4 mydomainrange[],
+	f5 comp3);
+--JSON_OBJECTAGG, JSON_ARRAYAGG is aggregate function, can not be used in index
+create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning jsonb));
+ERROR:  aggregate functions are not allowed in index expressions
+LINE 1: create index xx on test_mutability(json_objectagg(a: b absen...
+                                           ^
+create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning json));
+ERROR:  aggregate functions are not allowed in index expressions
+LINE 1: create index xx on test_mutability(json_objectagg(a: b absen...
+                                           ^
+create index xx on test_mutability(json_arrayagg(a returning jsonb));
+ERROR:  aggregate functions are not allowed in index expressions
+LINE 1: create index xx on test_mutability(json_arrayagg(a returning...
+                                           ^
+create index xx on test_mutability(json_arrayagg(a returning json));
+ERROR:  aggregate functions are not allowed in index expressions
+LINE 1: create index xx on test_mutability(json_arrayagg(a returning...
+                                           ^
+-- jsonb: create expression index via json_array
+create index on test_mutability(json_array(a returning jsonb)); --ok
+create index on test_mutability(json_array(b returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(b returning jsonb...
+                                        ^
+create index on test_mutability(json_array(c returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(c returning jsonb...
+                                        ^
+create index on test_mutability(json_array(d returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(d returning jsonb...
+                                        ^
+create index on test_mutability(json_array(f1 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(f1 returning json...
+                                        ^
+create index on test_mutability(json_array(f2 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(f2 returning json...
+                                        ^
+create index on test_mutability(json_array(f3 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(f3 returning json...
+                                        ^
+create index on test_mutability(json_array(f4 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(f4 returning json...
+                                        ^
+create index on test_mutability(json_array(f5 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_array(f5 returning json...
+                                        ^
+--jsonb: create expression index via json_object
+create index on test_mutability(json_object('hello' value a returning jsonb)); --ok
+create index on test_mutability(json_object('hello' value b returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value b ...
+                                        ^
+create index on test_mutability(json_object('hello' value c returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value c ...
+                                        ^
+create index on test_mutability(json_object('hello' value d returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value d ...
+                                        ^
+create index on test_mutability(json_object('hello' value f1 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value f1...
+                                        ^
+create index on test_mutability(json_object('hello' value f2 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value f2...
+                                        ^
+create index on test_mutability(json_object('hello' value f3 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value f3...
+                                        ^
+create index on test_mutability(json_object('hello' value f4 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value f4...
+                                        ^
+create index on test_mutability(json_object('hello' value f5 returning jsonb)); --error
+ERROR:  functions in index expression must be marked IMMUTABLE
+LINE 1: create index on test_mutability(json_object('hello' value f5...
+                                        ^
+-- data type json don't have default operator class for access method "btree" so
+-- we use a generated column to test whether the JSON_ARRAY expression is
+-- immutable
+alter table test_mutability add column f10 json generated always as (json_array(a returning json)); --ok
+alter table test_mutability add column f11 json generated always as (json_array(b returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f11 json generated always as (json_array(c returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f11 json generated always as (json_array(d returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f11 json generated always as (json_array(f1 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f11 json generated always as (json_array(f2 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f11 json generated always as (json_array(f4 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f11 json generated always as (json_array(f5 returning json)); --error
+ERROR:  generation expression is not immutable
+-- data type json don't have default operator class for access method "btree" so
+-- we use a generated column to test whether the JSON_OBJECT expression is
+-- immutable
+alter table test_mutability add column f11 json generated always as (json_object('hello' value a returning json)); --ok
+alter table test_mutability add column f12 json generated always as (json_object('hello' value b returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value c returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value d returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f1 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f2 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f3 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f4 returning json)); --error
+ERROR:  generation expression is not immutable
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f5 returning json)); --error
+ERROR:  generation expression is not immutable
+drop table test_mutability;
+drop domain d_comp1;
+drop type comp3;
+drop type mydomainrange;
+drop domain mydomain;
+drop type comp1;
 -- IS JSON predicate
 SELECT NULL IS JSON;
  ?column? 
diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql
index 343d344d270..fea5ce66bee 100644
--- a/src/test/regress/sql/sqljson.sql
+++ b/src/test/regress/sql/sqljson.sql
@@ -386,6 +386,79 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING
 
 DROP VIEW json_array_subquery_view;
 
+-- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT
+create type comp1 as (a int, b date);
+create domain d_comp1 as comp1;
+create domain mydomain as timestamptz;
+create type mydomainrange as range(subtype=mydomain);
+create type comp3 as (a int, b mydomainrange);
+create table test_mutability(
+	a text[], b timestamp, c timestamptz,
+	d date, f1 comp1[], f2 timestamp[],
+	f3 d_comp1[],
+	f4 mydomainrange[],
+	f5 comp3);
+
+--JSON_OBJECTAGG, JSON_ARRAYAGG is aggregate function, can not be used in index
+create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning jsonb));
+create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning json));
+create index xx on test_mutability(json_arrayagg(a returning jsonb));
+create index xx on test_mutability(json_arrayagg(a returning json));
+
+-- jsonb: create expression index via json_array
+create index on test_mutability(json_array(a returning jsonb)); --ok
+create index on test_mutability(json_array(b returning jsonb)); --error
+create index on test_mutability(json_array(c returning jsonb)); --error
+create index on test_mutability(json_array(d returning jsonb)); --error
+create index on test_mutability(json_array(f1 returning jsonb)); --error
+create index on test_mutability(json_array(f2 returning jsonb)); --error
+create index on test_mutability(json_array(f3 returning jsonb)); --error
+create index on test_mutability(json_array(f4 returning jsonb)); --error
+create index on test_mutability(json_array(f5 returning jsonb)); --error
+
+--jsonb: create expression index via json_object
+create index on test_mutability(json_object('hello' value a returning jsonb)); --ok
+create index on test_mutability(json_object('hello' value b returning jsonb)); --error
+create index on test_mutability(json_object('hello' value c returning jsonb)); --error
+create index on test_mutability(json_object('hello' value d returning jsonb)); --error
+create index on test_mutability(json_object('hello' value f1 returning jsonb)); --error
+create index on test_mutability(json_object('hello' value f2 returning jsonb)); --error
+create index on test_mutability(json_object('hello' value f3 returning jsonb)); --error
+create index on test_mutability(json_object('hello' value f4 returning jsonb)); --error
+create index on test_mutability(json_object('hello' value f5 returning jsonb)); --error
+
+-- data type json don't have default operator class for access method "btree" so
+-- we use a generated column to test whether the JSON_ARRAY expression is
+-- immutable
+alter table test_mutability add column f10 json generated always as (json_array(a returning json)); --ok
+alter table test_mutability add column f11 json generated always as (json_array(b returning json)); --error
+alter table test_mutability add column f11 json generated always as (json_array(c returning json)); --error
+alter table test_mutability add column f11 json generated always as (json_array(d returning json)); --error
+alter table test_mutability add column f11 json generated always as (json_array(f1 returning json)); --error
+alter table test_mutability add column f11 json generated always as (json_array(f2 returning json)); --error
+alter table test_mutability add column f11 json generated always as (json_array(f4 returning json)); --error
+alter table test_mutability add column f11 json generated always as (json_array(f5 returning json)); --error
+
+-- data type json don't have default operator class for access method "btree" so
+-- we use a generated column to test whether the JSON_OBJECT expression is
+-- immutable
+alter table test_mutability add column f11 json generated always as (json_object('hello' value a returning json)); --ok
+alter table test_mutability add column f12 json generated always as (json_object('hello' value b returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value c returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value d returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f1 returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f2 returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f3 returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f4 returning json)); --error
+alter table test_mutability add column f12 json generated always as (json_object('hello' value f5 returning json)); --error
+
+drop table test_mutability;
+drop domain d_comp1;
+drop type comp3;
+drop type mydomainrange;
+drop domain mydomain;
+drop type comp1;
+
 -- IS JSON predicate
 SELECT NULL IS JSON;
 SELECT NULL IS NOT JSON;
-- 
2.43.0

Reply via email to