From 667d50be65dd1f4307e2a46811695d4da92aa16c Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-46-230.ec2.internal>
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

