BUG #19840: contain_mutable_functions() misses VOLATILE sub-expressions in SQL/JSON functions when the jsonpath

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: 303677365(at)qq(dot)com
Subject: BUG #19840: contain_mutable_functions() misses VOLATILE sub-expressions in SQL/JSON functions when the jsonpath
Date: 2026-11-07 03:52:57
Message-ID: 19840-7d5243be84d7ecd3@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19840
Logged by: chunling qin
Email address: 303677365(at)qq(dot)com
PostgreSQL version: 18.6
Operating system: Linux x86_64; platform-independent (reproduced on
Description:

**Description:**

When an SQL/JSON function's path expression is a NULL jsonpath constant,
`contain_mutable_functions()` wrongly reports the whole expression as
immutable, so VOLATILE sub-expressions in the same JsonExpr (RETURNING
coercion, PASSING values, DEFAULT/ON ERROR expressions) escape the check. As
a result:

- expression indexes that should be rejected with `functions in index
expression must be marked IMMUTABLE` (42P17) are accepted — and then return
wrong results once the volatile value changes (index scan vs seq scan
disagree);
- partition-key and stored-generated-column checks are bypassed the same
way;
- a NON-NULL constant jsonpath with the very same VOLATILE sub-expressions
is correctly rejected, showing the NULL branch is the defect.

**Root cause:** `src/backend/optimizer/util/clauses.c`,
`contain_mutable_functions_walker()` JsonExpr branch (around lines 450-466):

```c
if (cnst->constisnull) return false; /* returns "no mutable functions" for
the
whole subtree and does NOT recurse
*/
```

This is the only early-return in clauses.c that skips
`expression_tree_walker()` recursion. The sibling walkers
`contain_volatile_functions_walker()` and `max_parallel_hazard_walker()`
have no such special case and always recurse, so they correctly find the
VOLATILE function — only the mutable-check is asymmetric.

**Reproduction (self-contained SQL; this exact script verified on master —
partition-key face also verified on 18.6):**

```sql
CREATE DOMAIN d_jb AS int NOT NULL;
CREATE FUNCTION g() RETURNS int LANGUAGE plpgsql VOLATILE AS
$$ BEGIN RETURN current_setting('my.g', true)::int; END $$;

SET my.g = '1'; -- value used when the index is built
CREATE TABLE t(id int, j jsonb);
INSERT INTO t SELECT g, '{}'::jsonb FROM generate_series(1,5) g;

-- CONTROL: same VOLATILE expression, NON-NULL const jsonpath -> correctly
rejected
CREATE INDEX t_ctrl ON t (JSON_VALUE(j, '$.a' RETURNING d_jb DEFAULT g() ON
ERROR));
-- ERROR: functions in index expression must be marked IMMUTABLE

-- TEST: NULL const jsonpath -> accepted (should have been rejected too)
CREATE INDEX t_test ON t (JSON_VALUE(j, NULL::jsonpath RETURNING d_jb
DEFAULT g() ON ERROR));
-- CREATE INDEX (no 42P17!)
ANALYZE t;

-- change the volatile function's value AFTER the index was built
SET my.g = '2';

-- both plans answer the SAME query; they must agree:
SET enable_seqscan=off; SET enable_bitmapscan=off;
SELECT 'idxscan' tag, count(*) AS n FROM t
WHERE JSON_VALUE(j, NULL::jsonpath RETURNING d_jb DEFAULT g() ON ERROR) =
1;
-- idxscan | 5 (uses the values frozen at index-build time)

RESET enable_seqscan; SET enable_indexscan=off; SET enable_bitmapscan=off;
SELECT 'seqscan' tag, count(*) AS n FROM t
WHERE JSON_VALUE(j, NULL::jsonpath RETURNING d_jb DEFAULT g() ON ERROR) =
1;
-- seqscan | 0 (re-evaluates the volatile function now)

RESET enable_indexscan; RESET enable_bitmapscan;

-- partition-key check bypassed the same way (also verified on 18.6):
CREATE TABLE tp(id int, j jsonb)
PARTITION BY RANGE (JSON_VALUE(j, NULL::jsonpath RETURNING d_jb DEFAULT
g() ON ERROR));
-- CREATE TABLE (accepted)
```

**Actual results:** with a NULL jsonpath the index and the partitioned table
are created without the 42P17 error; after the volatile value changes, the
index scan returns 5 while the seq scan returns 0 for the same query.

**Expected behavior:** a NULL jsonpath constant must not short-circuit the
mutability check of the surrounding JsonExpr; the expression should be
treated as mutable whenever any sub-expression (RETURNING coercion, PASSING
values, DEFAULT/ON ERROR expressions) contains STABLE/VOLATILE functions —
as already happens for a non-NULL jsonpath.

**Impact:** silent wrong results from stale expression indexes; UNIQUE
constraints built on such expressions fail to enforce; wrong partition
routing; stored generated columns computed once and never re-checked. The
affected callers include the index-expression check (indexcmds.c),
partition-key check (tablecmds.c), generated-column check, and SQL-function
inlining volatility checks (clauses.c).

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-11-07 04:12:26 BUG #19841: SQL/JSON RETURNING coercion to GUC-dependent types (timestamptz etc.) is wrongly judged IMMUTABLE -
Previous Message Atisss hh 2026-11-07 01:26:16 Re: BUG #19839: Assert in ReorderBufferRestoreCleanup for aborted, spilled transaction