From 7c0ef8405df8154a939bf05290aeaf16a8c300c9 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 v2 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.

The fix is to de-duplicate the locations at the end of jumbling,
only if we have squashable lists.

Discussion: https://www.postgresql.org/message-id/2b91e358-0d99-43f7-be44-d2d4dbce37b3%40garret.ru
---
 .../pg_stat_statements/expected/squashing.out | 80 +++++++++++++++++++
 contrib/pg_stat_statements/sql/squashing.sql  | 26 ++++++
 src/backend/nodes/queryjumblefuncs.c          | 42 ++++++++++
 3 files changed, 148 insertions(+)

diff --git a/contrib/pg_stat_statements/expected/squashing.out b/contrib/pg_stat_statements/expected/squashing.out
index f952f47ef7b..d5bb67c7222 100644
--- a/contrib/pg_stat_statements/expected/squashing.out
+++ b/contrib/pg_stat_statements/expected/squashing.out
@@ -809,6 +809,84 @@ SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C";
  select where $1 IN ($2 /*, ... */)                 |     2
 (2 rows)
 
+-- composite function 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 ((composite_f(array[1, $1, 3]))).*, 1 FROM test_composite \bind 1
+;
+ x | y | ?column? 
+---+---+----------
+(0 rows)
+
+-- ROW() expression with row expansion
+SELECT (ROW(ARRAY[1,2])).*;
+  f1   
+-------
+ {1,2}
+(1 row)
+
+SELECT (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*;
+  f1   |   f2    
+-------+---------
+ {1,2} | {1,2,3}
+(1 row)
+
+SELECT 1, 2, (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*, 3, 4;
+ ?column? | ?column? |  f1   |   f2    | ?column? | ?column? 
+----------+----------+-------+---------+----------+----------
+        1 |        2 | {1,2} | {1,2,3} |        3 |        4
+(1 row)
+
+SELECT (ROW(ARRAY[1, 2], ARRAY[1, $1, 3])).*, 1 \bind 1
+;
+  f1   |   f2    | ?column? 
+-------+---------+----------
+ {1,2} | {1,1,3} |        1
+(1 row)
+
+SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C";
+                                                    query                                                    | calls 
+-------------------------------------------------------------------------------------------------------------+-------
+ SELECT $1, $2, (ROW(ARRAY[$3 /*, ... */], ARRAY[$4 /*, ... */])).*, $5, $6                                  |     1
+ SELECT ((composite_f(array[$1 /*, ... */]))).* FROM test_composite                                          |     2
+ SELECT ((composite_f(array[$1 /*, ... */]))).*, $2 FROM test_composite                                      |     1
+ SELECT ((composite_f(array[$1 /*, ... */]))).*, $2, $3, $4, ((composite_f(array[$5 /*, ... */]))).*, $6, $7+|     1
+ FROM test_composite                                                                                        +| 
+ WHERE x IN ($8 /*, ... */)                                                                                  | 
+ SELECT (ROW(ARRAY[$1 /*, ... */])).*                                                                        |     1
+ SELECT (ROW(ARRAY[$1 /*, ... */], ARRAY[$2 /*, ... */])).*                                                  |     1
+ SELECT (ROW(ARRAY[$1 /*, ... */], ARRAY[$2 /*, ... */])).*, $3                                              |     1
+ SELECT pg_stat_statements_reset() IS NOT NULL AS t                                                          |     1
+(8 rows)
+
 --
 -- cleanup
 --
@@ -818,3 +896,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..03b0515f872 100644
--- a/contrib/pg_stat_statements/sql/squashing.sql
+++ b/contrib/pg_stat_statements/sql/squashing.sql
@@ -291,6 +291,30 @@ 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 function 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 ((composite_f(array[1, $1, 3]))).*, 1 FROM test_composite \bind 1
+;
+-- ROW() expression with row expansion
+SELECT (ROW(ARRAY[1,2])).*;
+SELECT (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*;
+SELECT 1, 2, (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*, 3, 4;
+SELECT (ROW(ARRAY[1, 2], ARRAY[1, $1, 3])).*, 1 \bind 1
+;
+SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C";
+
 --
 -- cleanup
 --
@@ -300,3 +324,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..8aba59105da 100644
--- a/src/backend/nodes/queryjumblefuncs.c
+++ b/src/backend/nodes/queryjumblefuncs.c
@@ -68,6 +68,7 @@ static void FlushPendingNulls(JumbleState *jstate);
 static void RecordConstLocation(JumbleState *jstate,
 								bool extern_param,
 								int location, int len);
+static void CleanupConstLocations(JumbleState *jstate);
 static void _jumbleNode(JumbleState *jstate, Node *node);
 static void _jumbleList(JumbleState *jstate, Node *node);
 static void _jumbleElements(JumbleState *jstate, List *elements, Node *node);
@@ -217,7 +218,10 @@ DoJumble(JumbleState *jstate, Node *node)
 
 	/* Squashed list found, reset highest_extern_param_id */
 	if (jstate->has_squashed_lists)
+	{
 		jstate->highest_extern_param_id = 0;
+		CleanupConstLocations(jstate);
+	}
 
 	/* Process the jumble buffer and produce the hash value */
 	return DatumGetInt64(hash_any_extended(jstate->jumble,
@@ -423,6 +427,44 @@ RecordConstLocation(JumbleState *jstate, bool extern_param, int location, int le
 	}
 }
 
+/*
+ * Squashed lists may record a location more than once, as is the
+ * case with row expansion of an expression that contains a squashable
+ * list. In that case, we remove duplicate locations at the end of
+ * jumbling.
+ */
+static void
+CleanupConstLocations(JumbleState *jstate)
+{
+	int			i,
+				j,
+				k = 0;
+
+	Assert(jstate->has_squashed_lists);
+
+	if (jstate->clocations_count <= 1)
+		return;					/* nothing to do */
+
+	for (i = 0; i < jstate->clocations_count; i++)
+	{
+		for (j = i + 1; j < jstate->clocations_count;)
+		{
+			if (jstate->clocations[i].location == jstate->clocations[j].location)
+			{
+				/* remove duplicate */
+				for (k = j; k < jstate->clocations_count - 1; k++)
+					jstate->clocations[k] = jstate->clocations[k + 1];
+
+				jstate->clocations_count--; /* resize the array */
+			}
+			else
+				j++;
+		}
+	}
+
+	/* XXX: we could resize the array here, but not strictly needed */
+}
+
 /*
  * Subroutine for _jumbleElements: Verify a few simple cases where we can
  * deduce that the expression is a constant:
-- 
2.43.0

