From 60d3d006c3fec734c33994f6506b51e35822953e Mon Sep 17 00:00:00 2001 From: Ilya Staroverov Date: Fri, 16 Oct 2026 16:03:06 +0300 Subject: [PATCH v2 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 apart from the number of columns, nothing about them was part of the query ID: 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 Const for a NESTED PATH expression the location of its literal, which column paths already have. Column names, which JsonExpr.column_name holds, become part of the query ID too, much like table aliases already are. This changes the query IDs of JSON_TABLE queries, so it should not be back-patched to released branches. Author: Ilya Staroverov --- .../pg_stat_statements/expected/select.out | 60 +++++++++++++++++++ contrib/pg_stat_statements/sql/select.sql | 17 ++++++ src/backend/parser/parse_jsontable.c | 2 + src/include/nodes/primnodes.h | 4 +- 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/contrib/pg_stat_statements/expected/select.out b/contrib/pg_stat_statements/expected/select.out index 875fe28ec..e9c6d2c2b 100644 --- a/contrib/pg_stat_statements/expected/select.out +++ b/contrib/pg_stat_statements/expected/select.out @@ -947,3 +947,63 @@ 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(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.a')) jt; + a +--- + 1 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a text PATH '$.a')) jt; + a +--- + 1 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.b' DEFAULT 0 ON EMPTY)) jt; + a +--- + 0 +(1 row) + +-- same query ID as the previous one, only the constants differ +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.c' DEFAULT 1 ON EMPTY)) jt; + a +--- + 1 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.a', NESTED PATH '$.b[*]' 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(jsonb $1, $2 + + | COLUMNS (a int PATH $3 DEFAULT $4 ON EMPTY)) jt + 1 | SELECT * FROM JSON_TABLE(jsonb $1, $2 + + | COLUMNS (a int PATH $3)) jt + 1 | SELECT * FROM JSON_TABLE(jsonb $1, $2 + + | COLUMNS (a int PATH $3, NESTED PATH $4 COLUMNS (b int PATH $5))) jt + 1 | SELECT * FROM JSON_TABLE(jsonb $1, $2 + + | COLUMNS (a text PATH $3)) 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 +(6 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 f43d8dd08..9fb9a68ff 100644 --- a/contrib/pg_stat_statements/sql/select.sql +++ b/contrib/pg_stat_statements/sql/select.sql @@ -281,3 +281,20 @@ 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; + +-- +-- JSON_TABLE column definitions are part of the query ID +-- +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.a')) jt; +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a text PATH '$.a')) jt; +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.b' DEFAULT 0 ON EMPTY)) jt; +-- same query ID as the previous one, only the constants differ +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.c' DEFAULT 1 ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '[{"a": 1}]', '$[*]' + COLUMNS (a int PATH '$.a', NESTED PATH '$.b[*]' 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/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..156d940b7 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 */ -- 2.43.0