pg_stat_statements replaces SQL/JSON ON EMPTY / ON ERROR keywords with parameters

From: Staroverov Ilja <i(dot)staroverov(at)ftdata(dot)ru>
To: "pgsql-bugs(at)lists(dot)postgresql(dot)org" <pgsql-bugs(at)lists(dot)postgresql(dot)org>
Subject: pg_stat_statements replaces SQL/JSON ON EMPTY / ON ERROR keywords with parameters
Date: 2026-10-16 12:48:55
Message-ID: 086A90EE-9771-4387-8B46-BF84A610F04F@localhost.localdomain
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

Hi,

When pg_stat_statements normalizes a query that uses an ON EMPTY or ON
ERROR clause of the SQL/JSON query functions, the behavior keyword is
replaced with a parameter symbol. On 18.6:

CREATE TABLE jt (js jsonb);
SELECT JSON_QUERY(js, '$.b' EMPTY ARRAY ON EMPTY) FROM jt;
SELECT JSON_EXISTS(js, 'strict $.b' TRUE ON ERROR) FROM jt;
SELECT JSON_EXISTS(js, 'strict $.b' FALSE ON ERROR) FROM jt;
SELECT query FROM pg_stat_statements WHERE query LIKE 'SELECT JSON%';
query
-----------------------------------------------------
SELECT JSON_EXISTS(js, $1 $2 ON ERROR) FROM jt
SELECT JSON_EXISTS(js, $1 $2 ON ERROR) FROM jt
SELECT JSON_QUERY(js, $1 $2 ARRAY ON EMPTY) FROM jt

The same happens with EMPTY OBJECT and NULL. DEFAULT <expr> is
normalized correctly. The normalized text is not valid SQL, so it can't
be used with EXPLAIN (GENERIC_PLAN):

EXPLAIN (GENERIC_PLAN) SELECT JSON_QUERY(js, $1 $2 ARRAY ON EMPTY) FROM jt;
ERROR: syntax error at or near "$2"

and the entries for TRUE ON ERROR and FALSE ON ERROR, which do opposite
things, show the same text.

The documentation says that a constant is replaced by a parameter
symbol "when a constant's value has been ignored for purposes of
matching the query to other queries". That's not the case here: these
keywords are not constants in the query text, and the behavior is part
of the query ID. TRUE ON ERROR and FALSE ON ERROR, EMPTY ARRAY and
EMPTY OBJECT, NULL ON EMPTY and ERROR ON EMPTY all give different query
IDs, unlike, say, SELECT true and SELECT false, which are merged and
shown as SELECT $1.

The cause is in GetJsonBehaviorConst(), which builds the Const for such
a behavior and sets its location to that of the behavior clause,
although the Const doesn't correspond to a constant in the query text.
Query jumbling records the locations of Consts, so pg_stat_statements
takes the keyword at that location for a constant. This comes from
6185c9737cf4, so 17 and later are affected. Master, as of 57926d757,
behaves the same.

The attached patch leaves the location of the Const unset. The only
other use of it was the error position when coercing the Const fails,
in transformJsonBehavior(), which now uses the location of the behavior
clause directly, so error messages don't change. With the patch, the
queries above are normalized as

SELECT JSON_QUERY(js, $1 EMPTY ARRAY ON EMPTY) FROM jt
SELECT JSON_EXISTS(js, $1 TRUE ON ERROR) FROM jt

and EXPLAIN (GENERIC_PLAN) works on them. Query IDs don't change,
since the locations of Consts are only recorded, not hashed, so
existing pg_stat_statements entries keep their IDs after a minor
upgrade. The patch adds a pg_stat_statements test, which fails without
the fix. It applies to REL_18_STABLE and REL_19_STABLE. For
REL_17_STABLE, the code applies, but the test has to go elsewhere in
select.sql, since the file ends differently there.

--
Ilya Staroverov

Attachment Content-Type Size
v1-0001-Don-t-give-the-SQL-JSON-behavior-constants-a-loca.patch application/octet-stream 6.3 KB

Browse pgsql-bugs by date

  From Date Subject
Next Message Jan Nidzwetzki 2026-10-16 13:03:59 Re: START_REPLICATION silently truncates an overlong LSN component
Previous Message Staroverov Ilja 2026-10-16 11:54:31 Re: [***DKIM violation***] Re: BUG #19418: SQL/JSON JSON_VALUE() does not conform to ISO/IEC 9075-2:2023(E) 6.34 <JSON value constructor>