From bc62a310eb1589751f641ea4474fd0af99b4fda5 Mon Sep 17 00:00:00 2001 From: Staroverov Ilja Date: Mon, 19 Oct 2026 07:22:41 +0300 Subject: [PATCH v3 2/2] Include JSON_TABLE column definitions in query jumbling TableFunc.colvalexprs and TableFunc.plan, which hold the column definitions and the NESTED PATH structure of a JSON_TABLE, were marked query_jumble_ignore, like several other TableFunc fields that have always been ignored for XMLTABLE. But unlike XMLTABLE, JSON_TABLE doesn't use colexprs, so the column definitions were not part of the query ID at all, only the references to the columns from the rest of the query: queries with different column types, ON EMPTY or ON ERROR behaviors, NESTED PATH structure or PLAN join types got the same query ID, and were merged into a single pg_stat_statements entry. Include both fields in query jumbling. Column paths and other literals are still treated as constants, so queries that differ only in those are still merged, and those constants are now shown as parameter symbols. For that, also give the path Consts in the plan the location of their literals, as the column paths already have, so that NESTED PATH expressions are shown as parameter symbols too. Column names and path names are left out of the query ID, as other column names are: column names only label the output columns, and path names are only used to refer to paths in the PLAN clause and when deparsing. The exception is a column without PATH, whose path is derived from its name and doesn't appear in the query text, so JsonExpr.column_name gets a custom jumble function that includes the name only in that case. This changes the query IDs of JSON_TABLE queries, and also those of the other SQL/JSON query functions, since their NULL column_name is no longer jumbled, so it should not be back-patched to released branches. Author: Ilya Staroverov Reviewed-by: Sami Imseih Discussion: https://postgr.es/m/086A90EE-9771-4387-8B46-BF84A610F04F@localhost.localdomain Discussion: https://postgr.es/m/DFE8E2CE-7A51-447C-B600-7D3979E38F1B@localhost.localdomain --- .../pg_stat_statements/expected/select.out | 90 +++++++++++++++++++ contrib/pg_stat_statements/sql/select.sql | 27 ++++++ src/backend/nodes/queryjumblefuncs.c | 21 +++++ src/backend/parser/parse_jsontable.c | 2 + src/include/nodes/primnodes.h | 13 +-- 5 files changed, 148 insertions(+), 5 deletions(-) diff --git a/contrib/pg_stat_statements/expected/select.out b/contrib/pg_stat_statements/expected/select.out index cb96d8350..985c2dbb1 100644 --- a/contrib/pg_stat_statements/expected/select.out +++ b/contrib/pg_stat_statements/expected/select.out @@ -956,3 +956,93 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t; t (1 row) +-- +-- JSON_TABLE column definitions are part of the query ID +-- +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.a')) jt; + a +--- + 1 +(1 row) + +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a text PATH '$.a')) jt; + a +--- + 1 +(1 row) + +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.b' DEFAULT 0 ON EMPTY)) jt; + a +--- + 0 +(1 row) + +-- same query ID as the previous one: only constants and column names differ +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (b int PATH '$.c' DEFAULT 1 ON EMPTY)) jt; + b +--- + 1 +(1 row) + +-- without PATH, the path is derived from the column name, so the name counts +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int)) jt; + a +--- + 1 +(1 row) + +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (b int)) jt; + b +--- + +(1 row) + +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.a', + NESTED PATH '$.b[*]' AS p1 COLUMNS (b int PATH '$'))) jt; + a | b +---+--- + 1 | +(1 row) + +-- same query ID as the previous one: only the path name differs +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.a', + NESTED PATH '$.b[*]' AS p2 COLUMNS (b int PATH '$'))) jt; + a | b +---+--- + 1 | +(1 row) + +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; + calls | query +-------+------------------------------------------------------------------------ + 2 | SELECT * FROM JSON_TABLE($1::jsonb, $2 + + | COLUMNS (a int PATH $3 DEFAULT $4 ON EMPTY)) jt + 1 | SELECT * FROM JSON_TABLE($1::jsonb, $2 + + | COLUMNS (a int PATH $3)) jt + 2 | SELECT * FROM JSON_TABLE($1::jsonb, $2 + + | COLUMNS (a int PATH $3, + + | NESTED PATH $4 AS p1 COLUMNS (b int PATH $5))) jt + 1 | SELECT * FROM JSON_TABLE($1::jsonb, $2 + + | COLUMNS (a int)) jt + 1 | SELECT * FROM JSON_TABLE($1::jsonb, $2 + + | COLUMNS (a text PATH $3)) jt + 1 | SELECT * FROM JSON_TABLE($1::jsonb, $2 + + | COLUMNS (b int)) jt + 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 320f9d363..9689b1db2 100644 --- a/contrib/pg_stat_statements/sql/select.sql +++ b/contrib/pg_stat_statements/sql/select.sql @@ -284,3 +284,30 @@ SELECT JSON_EXISTS('{}'::jsonb, 'strict $.a' FALSE ON ERROR); SELECT JSON_EXISTS(jsonb '{}', 'strict $.a' UNKNOWN ON ERROR); SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +-- +-- JSON_TABLE column definitions are part of the query ID +-- +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.a')) jt; +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a text PATH '$.a')) jt; +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.b' DEFAULT 0 ON EMPTY)) jt; +-- same query ID as the previous one: only constants and column names differ +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (b int PATH '$.c' DEFAULT 1 ON EMPTY)) jt; +-- without PATH, the path is derived from the column name, so the name counts +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int)) jt; +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (b int)) jt; +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.a', + NESTED PATH '$.b[*]' AS p1 COLUMNS (b int PATH '$'))) jt; +-- same query ID as the previous one: only the path name differs +SELECT * FROM JSON_TABLE('[{"a": 1}]'::jsonb, '$[*]' + COLUMNS (a int PATH '$.a', + NESTED PATH '$.b[*]' AS p2 COLUMNS (b int PATH '$'))) jt; +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/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index e5f645dc2..7bd21fa43 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -79,6 +79,9 @@ static void _jumbleVariableSetStmt(JumbleState *jstate, Node *node); static void _jumbleRangeTblEntry_eref(JumbleState *jstate, RangeTblEntry *rte, Alias *expr); +static void _jumbleJsonExpr_column_name(JumbleState *jstate, + JsonExpr *expr, + char *column_name); /* * Given a possibly multi-statement source string, confine our attention to the @@ -774,6 +777,24 @@ _jumbleRangeTblEntry_eref(JumbleState *jstate, JUMBLE_STRING(aliasname); } +/* + * Custom query jumble function for JsonExpr.column_name. + */ +static void +_jumbleJsonExpr_column_name(JumbleState *jstate, + JsonExpr *expr, + char *column_name) +{ + /* + * Like other column names, the name of a JSON_TABLE column is ignored. + * But a column without PATH gets a path derived from its name, which + * doesn't appear in the query text and so has no location. Include the + * name then, since the value of the path is not jumbled. + */ + if (column_name != NULL && exprLocation(expr->path_spec) < 0) + JUMBLE_STRING(column_name); +} + /* * CompLocation: comparator for qsorting LocationLen structs by location */ diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index d86c6946e..7955cea0d 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -686,6 +686,8 @@ makeJsonTablePathScan(JsonTableParseContext *cxt, JsonTablePathSpec *pathspec, DirectFunctionCall1(jsonpath_in, CStringGetDatum(pathstring)), false, false); + /* The path is a constant in the query text, so it can be normalized */ + value->location = castNode(A_Const, pathspec->string)->location; scan->plan.type = T_JsonTablePathScan; scan->path = makeJsonTablePath(value, pathspec->name); diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 726c186c7..fe90853d4 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -134,13 +134,13 @@ typedef struct TableFunc /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); /* JSON_TABLE: list of column value expressions */ - List *colvalexprs pg_node_attr(query_jumble_ignore); + List *colvalexprs; /* JSON_TABLE: list of PASSING argument expressions */ List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); /* JSON_TABLE plan */ - Node *plan pg_node_attr(query_jumble_ignore); + Node *plan; /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1846,8 +1846,11 @@ typedef struct JsonExpr JsonExprOp op; - char *column_name; /* JSON_TABLE() column name or NULL if this is - * not for a JSON_TABLE() */ + /* + * JSON_TABLE() column name or NULL if this is not for a JSON_TABLE(). + * Only jumbled for a column without PATH, see queryjumblefuncs.c. + */ + char *column_name pg_node_attr(custom_query_jumble); /* jsonb-valued expression to query */ Node *formatted_expr; @@ -1899,7 +1902,7 @@ typedef struct JsonTablePath NodeTag type; Const *value; - char *name; + char *name pg_node_attr(query_jumble_ignore); } JsonTablePath; /* -- 2.43.0