From 667d50be65dd1f4307e2a46811695d4da92aa16c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 24 Oct 2025 00:34:54 +0000 Subject: [PATCH 1/1] Fix jumbling of squashed lists with row expansion Commit 0f65f3eec introduced squashing of constant lists, but did not handle row expansion of composite values correctly. As a result, the same location could be recorded multiple times, leading to assertion failures in pg_stat_statements during generate_normalized_query. This fix tracks the start position of the last recorded constant and skips it if encountered again during _jumbleElements. Discussion: https://www.postgresql.org/message-id/2b91e358-0d99-43f7-be44-d2d4dbce37b3%40garret.ru --- .../pg_stat_statements/expected/squashing.out | 43 +++++++++++++++++++ contrib/pg_stat_statements/sql/squashing.sql | 18 ++++++++ src/backend/nodes/queryjumblefuncs.c | 18 ++++++-- src/include/nodes/queryjumble.h | 3 ++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/contrib/pg_stat_statements/expected/squashing.out b/contrib/pg_stat_statements/expected/squashing.out index f952f47ef7b..af54ee39ba1 100644 --- a/contrib/pg_stat_statements/expected/squashing.out +++ b/contrib/pg_stat_statements/expected/squashing.out @@ -809,6 +809,47 @@ SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; select where $1 IN ($2 /*, ... */) | 2 (2 rows) +-- composite functions with row expansion +create table test_composite(x integer); +CREATE FUNCTION composite_f(a integer[], out x integer, out y integer) returns +record as $$ begin + x = a[1]; + y = a[2]; + end; +$$ language plpgsql; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT ((composite_f(array[1, 2]))).* FROM test_composite; + x | y +---+--- +(0 rows) + +SELECT ((composite_f(array[1, 2, 3]))).* FROM test_composite; + x | y +---+--- +(0 rows) + +SELECT ((composite_f(array[1, 2, 3]))).*, 1, 2, 3, ((composite_f(array[1, 2, 3]))).*, 1, 2 +FROM test_composite +WHERE x IN (1, 2, 3); + x | y | ?column? | ?column? | ?column? | x | y | ?column? | ?column? +---+---+----------+----------+----------+---+---+----------+---------- +(0 rows) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +-------------------------------------------------------------------------------------------------------------+------- + SELECT ((composite_f(array[$1 /*, ... */]))).* FROM test_composite | 2 + SELECT ((composite_f(array[$1 /*, ... */]))).*, $2, $3, $4, ((composite_f(array[$5 /*, ... */]))).*, $6, $7+| 1 + FROM test_composite +| + WHERE x IN ($8 /*, ... */) | + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) + -- -- cleanup -- @@ -818,3 +859,5 @@ DROP TABLE test_squash_numeric; DROP TABLE test_squash_bigint; DROP TABLE test_squash_cast CASCADE; DROP TABLE test_squash_jsonb; +DROP TABLE test_composite; +DROP FUNCTION composite_f; diff --git a/contrib/pg_stat_statements/sql/squashing.sql b/contrib/pg_stat_statements/sql/squashing.sql index 53138d125a9..6fc9e0e56b2 100644 --- a/contrib/pg_stat_statements/sql/squashing.sql +++ b/contrib/pg_stat_statements/sql/squashing.sql @@ -291,6 +291,22 @@ select where '1' IN ('1'::int::text, '2'::int::text); select where '1' = ANY (array['1'::int::text, '2'::int::text]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +-- composite functions with row expansion +create table test_composite(x integer); +CREATE FUNCTION composite_f(a integer[], out x integer, out y integer) returns +record as $$ begin + x = a[1]; + y = a[2]; + end; +$$ language plpgsql; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT ((composite_f(array[1, 2]))).* FROM test_composite; +SELECT ((composite_f(array[1, 2, 3]))).* FROM test_composite; +SELECT ((composite_f(array[1, 2, 3]))).*, 1, 2, 3, ((composite_f(array[1, 2, 3]))).*, 1, 2 +FROM test_composite +WHERE x IN (1, 2, 3); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + -- -- cleanup -- @@ -300,3 +316,5 @@ DROP TABLE test_squash_numeric; DROP TABLE test_squash_bigint; DROP TABLE test_squash_cast CASCADE; DROP TABLE test_squash_jsonb; +DROP TABLE test_composite; +DROP FUNCTION composite_f; diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index 31f97151977..c8bad6f3c12 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -193,6 +193,7 @@ InitJumble(void) jstate->highest_extern_param_id = 0; jstate->pending_nulls = 0; jstate->has_squashed_lists = false; + jstate->last_start_location = 0; #ifdef USE_ASSERT_CHECKING jstate->total_jumble_len = 0; #endif @@ -656,10 +657,19 @@ _jumbleElements(JumbleState *jstate, List *elements, Node *node) if (aexpr->list_start > 0 && aexpr->list_end > 0) { - RecordConstLocation(jstate, - false, - aexpr->list_start + 1, - (aexpr->list_end - aexpr->list_start) - 1); + /* + * There are cases where the same location could be reached by + * jumbling multiple times. In that case, we don't want to + * record it multiple times. + */ + if (aexpr->list_start != jstate->last_start_location) + { + RecordConstLocation(jstate, + false, + aexpr->list_start + 1, + (aexpr->list_end - aexpr->list_start) - 1); + jstate->last_start_location = aexpr->list_start; + } normalize_list = true; jstate->has_squashed_lists = true; } diff --git a/src/include/nodes/queryjumble.h b/src/include/nodes/queryjumble.h index dcb36dcb44f..06bd17984cb 100644 --- a/src/include/nodes/queryjumble.h +++ b/src/include/nodes/queryjumble.h @@ -64,6 +64,9 @@ typedef struct JumbleState /* Whether squashable lists are present */ bool has_squashed_lists; + /* The last start location recorded */ + int last_start_location; + /* * Count of the number of NULL nodes seen since last appending a value. * These are flushed out to the jumble buffer before subsequent appends -- 2.43.0