From: Andrew Dunstan Date: Tue, 10 Mar 2026 06:25:29 +0000 (+0800) Subject: make immutability tests in to_json and to_jsonb complete X-Git-Tag: REL_19_BETA1~791 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ecd9288624a1582a732cf86ac5a01475a1ce7815;p=thirdparty%2Fpostgresql.git make immutability tests in to_json and to_jsonb 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. Range and multirange types are now also checked recursively: if the subtype's conversion is immutable, the range is considered immutable for JSON purposes, even though range_out is generically marked STABLE. This is a behavioral change: range types with immutable subtypes (e.g., int4range) can now appear in expression indexes via JSON_ARRAY/JSON_OBJECT, whereas previously they were conservatively rejected. Add regression tests for JSON_ARRAY and JSON_OBJECT mutability with expression indexes and generated columns, covering arrays, composites, domains, ranges, multiranges and combinations thereof. Author: Jian He Reviewed-by: Andrew Dunstan Discussion: https://postgr.es/m/CACJufxFz=OsXQdsMJ-cqoqspD9aJrwntsQP-U2A-UaV_M+-S9g@mail.gmail.com Commitfest: https://commitfest.postgresql.org/patch/5759 --- diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index ab38ddb9079..ae73c64fbb5 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" @@ -693,45 +692,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; - - 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; - } + bool has_mutable = false; - 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; - - 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; - } + bool has_mutable = false; - 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 efdf60ca5a5..97cc3d60340 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -17,6 +17,8 @@ #include #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" @@ -6063,3 +6065,106 @@ 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); + if (*has_mutable) + break; + } + 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_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_NUMERIC: + break; + + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: + *has_mutable = true; + break; + + case JSONTYPE_JSON: + case JSONTYPE_JSONB: + case JSONTYPE_ARRAY: + case JSONTYPE_COMPOSITE: + break; + + 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..4e3b4540d42 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1109,6 +1109,171 @@ 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, + f6 mydomainmultirange); +-- JSON_OBJECTAGG, JSON_ARRAYAGG are aggregate functions, cannot 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... + ^ +create index on test_mutability(json_array(f6 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f6 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... + ^ +create index on test_mutability(json_object('hello' value f6 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f6... + ^ +-- data type json doesn't have a 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(f3 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 +alter table test_mutability add column f11 json generated always as (json_array(f6 returning json)); -- error +ERROR: generation expression is not immutable +-- data type json doesn't have a 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 +alter table test_mutability add column f12 json generated always as (json_object('hello' value f6 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; +-- Range/multirange with immutable subtype should be considered immutable +create type range_int as range(subtype=int); +create table test_range_immutable(r range_int, m multirange_int); +create index on test_range_immutable(json_array(r returning jsonb)); -- ok +create index on test_range_immutable(json_array(m returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value r returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value m returning jsonb)); -- ok +drop table test_range_immutable; +drop type range_int; -- 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..0ad7fb14e7d 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -386,6 +386,95 @@ 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, + f6 mydomainmultirange); + +-- JSON_OBJECTAGG, JSON_ARRAYAGG are aggregate functions, cannot 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 +create index on test_mutability(json_array(f6 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 +create index on test_mutability(json_object('hello' value f6 returning jsonb)); -- error + +-- data type json doesn't have a 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(f3 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 +alter table test_mutability add column f11 json generated always as (json_array(f6 returning json)); -- error + +-- data type json doesn't have a 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 +alter table test_mutability add column f12 json generated always as (json_object('hello' value f6 returning json)); -- error + +drop table test_mutability; +drop domain d_comp1; +drop type comp3; +drop type mydomainrange; +drop domain mydomain; +drop type comp1; + +-- Range/multirange with immutable subtype should be considered immutable +create type range_int as range(subtype=int); +create table test_range_immutable(r range_int, m multirange_int); +create index on test_range_immutable(json_array(r returning jsonb)); -- ok +create index on test_range_immutable(json_array(m returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value r returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value m returning jsonb)); -- ok +drop table test_range_immutable; +drop type range_int; + -- IS JSON predicate SELECT NULL IS JSON; SELECT NULL IS NOT JSON;