From 22c8d7de7d937c78681762dc1d164e78273f4813 Mon Sep 17 00:00:00 2001 From: Ilya Staroverov Date: Fri, 16 Oct 2026 15:22:04 +0300 Subject: [PATCH v1] 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, 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 "$3 ARRAY ON EMPTY", and "TRUE ON ERROR" and "FALSE ON ERROR" both became "$3 ON ERROR". The result is not valid SQL, so it can't be used with EXPLAIN (GENERIC_PLAN), for example. 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. Oversight in 6185c9737cf4. Author: Ilya Staroverov Backpatch-through: 17 --- .../pg_stat_statements/expected/select.out | 58 +++++++++++++++++++ contrib/pg_stat_statements/sql/select.sql | 12 ++++ src/backend/parser/parse_expr.c | 15 +++-- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/contrib/pg_stat_statements/expected/select.out b/contrib/pg_stat_statements/expected/select.out index a069119c7..875fe28ec 100644 --- a/contrib/pg_stat_statements/expected/select.out +++ b/contrib/pg_stat_statements/expected/select.out @@ -889,3 +889,61 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t; t (1 row) +-- +-- SQL/JSON ON EMPTY / ON ERROR behaviors are not constants +-- +SELECT JSON_QUERY(jsonb '{}', '$.a' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '{}', '$.a' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$.a' NULL ON EMPTY); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$.a' DEFAULT 1 ON EMPTY); + json_value +------------ + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', 'strict $.a' TRUE ON ERROR); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', 'strict $.a' FALSE ON ERROR); + json_exists +------------- + f +(1 row) + +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; + calls | query +-------+------------------------------------------------------------------------ + 1 | SELECT JSON_EXISTS(jsonb $1, $2 FALSE ON ERROR) + 1 | SELECT JSON_EXISTS(jsonb $1, $2 TRUE ON ERROR) + 1 | SELECT JSON_QUERY(jsonb $1, $2 EMPTY ARRAY ON EMPTY) + 1 | SELECT JSON_QUERY(jsonb $1, $2 EMPTY OBJECT ON EMPTY) + 1 | SELECT JSON_VALUE(jsonb $1, $2 DEFAULT $3 ON EMPTY) + 1 | SELECT JSON_VALUE(jsonb $1, $2 NULL ON EMPTY) + 0 | SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C" + 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/contrib/pg_stat_statements/sql/select.sql b/contrib/pg_stat_statements/sql/select.sql index a10d618c0..f43d8dd08 100644 --- a/contrib/pg_stat_statements/sql/select.sql +++ b/contrib/pg_stat_statements/sql/select.sql @@ -269,3 +269,15 @@ DROP SCHEMA pgss_schema_1 CASCADE; DROP SCHEMA pgss_schema_2 CASCADE; DROP TABLE tab_search_same, tab_search_diff_1, tab_search_diff_2; SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +-- +-- SQL/JSON ON EMPTY / ON ERROR behaviors are not constants +-- +SELECT JSON_QUERY(jsonb '{}', '$.a' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '{}', '$.a' EMPTY OBJECT ON EMPTY); +SELECT JSON_VALUE(jsonb '{}', '$.a' NULL ON EMPTY); +SELECT JSON_VALUE(jsonb '{}', '$.a' DEFAULT 1 ON EMPTY); +SELECT JSON_EXISTS(jsonb '{}', 'strict $.a' TRUE ON ERROR); +SELECT JSON_EXISTS(jsonb '{}', 'strict $.a' FALSE ON ERROR); +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; 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; } -- 2.43.0