v2-0001-Fix-jumbling-of-squashed-lists-with-row-expansion.patch
application/octet-stream
Filename: v2-0001-Fix-jumbling-of-squashed-lists-with-row-expansion.patch
Type: application/octet-stream
Part: 0
Message:
Re: Bug in pg_stat_statements
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v2-0001
Subject: Fix jumbling of squashed lists with row expansion
| File | + | − |
|---|---|---|
| contrib/pg_stat_statements/expected/squashing.out | 80 | 0 |
| contrib/pg_stat_statements/sql/squashing.sql | 26 | 0 |
| src/backend/nodes/queryjumblefuncs.c | 42 | 0 |
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