From 3770c86a88618e36c61db1908b6144b051bf2d7c Mon Sep 17 00:00:00 2001 From: Ilya Staroverov Date: Wed, 21 Oct 2026 10:44:04 +0300 Subject: [PATCH v5 1/2] Don't give the SQL/JSON behavior constants a location GetJsonBehaviorConst() builds the Const for an ON EMPTY or ON ERROR behavior like NULL, TRUE, FALSE, UNKNOWN, EMPTY, EMPTY ARRAY or EMPTY OBJECT. Such a Const doesn't correspond to a constant in the query text, but it was given the location of the behavior clause. Query jumbling records the locations of Consts, so pg_stat_statements replaced the behavior keyword with a parameter symbol when normalizing the query text: for example, "EMPTY ARRAY ON EMPTY" became "$2 ARRAY ON EMPTY", and "TRUE ON ERROR" and "FALSE ON ERROR" both became "$2 ON ERROR", although the behavior is part of the query ID, so such queries got separate entries with the same text. The same happened with the ON ERROR clause of JSON_TABLE. Leave the location of the Const unset. It was only otherwise used as the error position when coercing the Const fails, so use the location of the behavior clause there directly. Query IDs don't change, since the locations of Consts are only recorded, not hashed. Oversight in 6185c9737cf4. Author: Ilya Staroverov Reviewed-by: Sami Imseih Reviewed-by: Michael Paquier Discussion: https://postgr.es/m/086A90EE-9771-4387-8B46-BF84A610F04F@localhost.localdomain Discussion: https://postgr.es/m/DFE8E2CE-7A51-447C-B600-7D3979E38F1B@localhost.localdomain Discussion: https://postgr.es/m/457969A9-5D24-422F-B09E-F388053DE028@localhost.localdomain --- .../pg_stat_statements/expected/sqljson.out | 34 +++++++++---------- src/backend/parser/parse_expr.c | 15 +++++--- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/contrib/pg_stat_statements/expected/sqljson.out b/contrib/pg_stat_statements/expected/sqljson.out index e62504122..43f0a560c 100644 --- a/contrib/pg_stat_statements/expected/sqljson.out +++ b/contrib/pg_stat_statements/expected/sqljson.out @@ -72,11 +72,11 @@ SELECT JSON_VALUE(js, '$.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR) FROM pgss_sql SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C", calls; calls | query -------+----------------------------------------------------------------------------------------- - 2 | SELECT JSON_VALUE(js, $1 $2 ON EMPTY ERROR ON ERROR) FROM pgss_sqljson_tab 1 | SELECT JSON_VALUE(js, $1 DEFAULT $2 ON EMPTY DEFAULT $3 ON ERROR) FROM pgss_sqljson_tab 2 | SELECT JSON_VALUE(js, $1 DEFAULT $2 ON EMPTY) FROM pgss_sqljson_tab 1 | SELECT JSON_VALUE(js, $1 DEFAULT $2 ON ERROR) FROM pgss_sqljson_tab - 2 | SELECT JSON_VALUE(js, $1 ERROR ON EMPTY $2 ON ERROR) FROM pgss_sqljson_tab + 2 | SELECT JSON_VALUE(js, $1 ERROR ON EMPTY NULL ON ERROR) FROM pgss_sqljson_tab + 2 | SELECT JSON_VALUE(js, $1 NULL ON EMPTY ERROR ON ERROR) FROM pgss_sqljson_tab 3 | SELECT JSON_VALUE(js, $1) FROM pgss_sqljson_tab 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (7 rows) @@ -219,19 +219,19 @@ SELECT JSON_QUERY(js, '$.a' EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR) (0 rows) SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C", calls; - calls | query --------+--------------------------------------------------------------------- - 1 | SELECT JSON_QUERY(js, $1 $2 ARRAY ON EMPTY $3 OBJECT ON ERROR) + - | FROM pgss_sqljson_tab - 2 | SELECT JSON_QUERY(js, $1 $2 ARRAY ON EMPTY) FROM pgss_sqljson_tab - 1 | SELECT JSON_QUERY(js, $1 $2 OBJECT ON EMPTY) FROM pgss_sqljson_tab - 1 | SELECT JSON_QUERY(js, $1 $2 OBJECT ON ERROR) FROM pgss_sqljson_tab - 1 | SELECT JSON_QUERY(js, $1 $2 ON EMPTY) FROM pgss_sqljson_tab - 2 | SELECT JSON_QUERY(js, $1 $2 ON ERROR) FROM pgss_sqljson_tab + calls | query +-------+----------------------------------------------------------------------- 1 | SELECT JSON_QUERY(js, $1 DEFAULT $2 ON EMPTY) FROM pgss_sqljson_tab 1 | SELECT JSON_QUERY(js, $1 DEFAULT $2 ON ERROR) FROM pgss_sqljson_tab + 1 | SELECT JSON_QUERY(js, $1 EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR) + + | FROM pgss_sqljson_tab + 2 | SELECT JSON_QUERY(js, $1 EMPTY ARRAY ON EMPTY) FROM pgss_sqljson_tab + 1 | SELECT JSON_QUERY(js, $1 EMPTY OBJECT ON EMPTY) FROM pgss_sqljson_tab + 1 | SELECT JSON_QUERY(js, $1 EMPTY OBJECT ON ERROR) FROM pgss_sqljson_tab + 2 | SELECT JSON_QUERY(js, $1 EMPTY ON ERROR) FROM pgss_sqljson_tab 1 | SELECT JSON_QUERY(js, $1 ERROR ON EMPTY) FROM pgss_sqljson_tab 1 | SELECT JSON_QUERY(js, $1 ERROR ON ERROR) FROM pgss_sqljson_tab + 1 | SELECT JSON_QUERY(js, $1 NULL ON EMPTY) FROM pgss_sqljson_tab 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (11 rows) @@ -429,13 +429,13 @@ SELECT JSON_EXISTS(js, '$.a ? (@ > $x)' PASSING 2 AS x) FROM pgss_sqljson_tab; (0 rows) SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C", calls; - calls | query --------+------------------------------------------------------------------ - 1 | SELECT JSON_EXISTS(js, $1 $2 ON ERROR) FROM pgss_sqljson_tab - 1 | SELECT JSON_EXISTS(js, $1 $2 ON ERROR) FROM pgss_sqljson_tab - 2 | SELECT JSON_EXISTS(js, $1 $2 ON ERROR) FROM pgss_sqljson_tab + calls | query +-------+------------------------------------------------------------------- 1 | SELECT JSON_EXISTS(js, $1 ERROR ON ERROR) FROM pgss_sqljson_tab + 2 | SELECT JSON_EXISTS(js, $1 FALSE ON ERROR) FROM pgss_sqljson_tab 2 | SELECT JSON_EXISTS(js, $1 PASSING $2 AS x) FROM pgss_sqljson_tab + 1 | SELECT JSON_EXISTS(js, $1 TRUE ON ERROR) FROM pgss_sqljson_tab + 1 | SELECT JSON_EXISTS(js, $1 UNKNOWN ON ERROR) FROM pgss_sqljson_tab 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (6 rows) @@ -995,7 +995,7 @@ SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C", calls; calls | query -------+------------------------------------------------------------------- 3 | SELECT * FROM pgss_sqljson_tab, + - | JSON_TABLE(js, $1 COLUMNS (a int PATH '$.a') $2 ON ERROR) jt + | JSON_TABLE(js, $1 COLUMNS (a int PATH '$.a') EMPTY ON ERROR) jt 1 | SELECT * FROM pgss_sqljson_tab, + | JSON_TABLE(js, $1 COLUMNS (a int PATH '$.a') ERROR ON ERROR) jt 2 | SELECT * FROM pgss_sqljson_tab, + diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 05a6b72d4..6ac9243c7 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -99,7 +99,7 @@ static JsonBehavior *transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, JsonBehavior *behavior, JsonBehaviorType default_behavior, JsonReturning *returning); -static Node *GetJsonBehaviorConst(JsonBehaviorType btype, int location); +static Node *GetJsonBehaviorConst(JsonBehaviorType btype); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -4897,7 +4897,7 @@ transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, } if (expr == NULL && btype != JSON_BEHAVIOR_ERROR) - expr = GetJsonBehaviorConst(btype, location); + expr = GetJsonBehaviorConst(btype); /* * Try to coerce the expression if needed. @@ -4988,7 +4988,7 @@ transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, errmsg("cannot cast behavior expression of type %s to %s", format_type_be(exprType(expr)), format_type_be(returning->typid)), - parser_errposition(pstate, exprLocation(expr))); + parser_errposition(pstate, location)); } expr = coerced_expr; @@ -5010,7 +5010,7 @@ transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, * JsonBehaviorType. */ static Node * -GetJsonBehaviorConst(JsonBehaviorType btype, int location) +GetJsonBehaviorConst(JsonBehaviorType btype) { Datum val = (Datum) 0; Oid typid = JSONBOID; @@ -5064,8 +5064,13 @@ GetJsonBehaviorConst(JsonBehaviorType btype, int location) break; } + /* + * Leave the location unset: the constant doesn't appear in the query + * text, so it mustn't look like it does, for example to query jumbling, + * which would make pg_stat_statements replace the behavior keyword with a + * parameter symbol. + */ con = makeConst(typid, -1, InvalidOid, len, val, isnull, isbyval); - con->location = location; return (Node *) con; } base-commit: 5ffb8c7409385c58e5c1abaa22ee77ba4c1b5c33 -- 2.43.0