From 6bdc4be74b93b97f5cc898a7e8fc896bacd294f3 Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Fri, 2 Oct 2026 11:57:42 +0500
Subject: [PATCH] Don't copy heap attstorage onto opclass STORAGE types.

ALTER TABLE ... SET STORAGE (and SET COMPRESSION) updates simple
index columns through SetIndexStorageProperties().  That copied the
heap setting even when ConstructTupleDescriptor() had replaced the
index column type with the opclass STORAGE type.

gist_trgm_ops stores gtrgm, whose typstorage is PLAIN.  After SET
STORAGE EXTENDED the index attstorage became EXTENDED.  Later inserts
packed short varlena headers.  TRGM macros use VARSIZE on a four-byte
header and crashed.

Skip the catalog update when the index column type differs from the
heap column.  A btree index on the same column still receives the new
setting.

Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Thiago Bonfante <thiago@subbase.io>
Discussion: https://www.postgresql.org/message-id/CAA8TiqELrCMyAkNMev0NerxACZTaQL5a2Y2htzfDxFirJ%2BQGkw%40mail.gmail.com
Backpatch-through: 14
---
 contrib/pg_trgm/expected/pg_trgm.out | 31 +++++++++++++++++++++++++++
 contrib/pg_trgm/sql/pg_trgm.sql      | 12 ++++++++++++
 src/backend/commands/tablecmds.c     | 29 ++++++++++++++------
 3 files changed, 64 insertions(+), 8 deletions(-)

diff --git a/contrib/pg_trgm/expected/pg_trgm.out b/contrib/pg_trgm/expected/pg_trgm.out
index 612625f1fda..092b7a9b8f5 100644
--- a/contrib/pg_trgm/expected/pg_trgm.out
+++ b/contrib/pg_trgm/expected/pg_trgm.out
@@ -5448,3 +5448,34 @@ SELECT DISTINCT city, similarity(city, 'Warsaw'), show_limit()
  Warsaw |          1 |        0.5
 (1 row)
 
+-- SET STORAGE must not copy heap attstorage onto gtrgm.
+CREATE TABLE gst (s text);
+CREATE INDEX gst_gist ON gst USING gist (s gist_trgm_ops);
+CREATE INDEX gst_btree ON gst (s);
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_gist'::regclass AND attnum = 1;
+ attstorage 
+------------
+ p
+(1 row)
+
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_btree'::regclass AND attnum = 1;
+ attstorage 
+------------
+ x
+(1 row)
+
+ALTER TABLE gst ALTER COLUMN s SET STORAGE EXTERNAL;
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_gist'::regclass AND attnum = 1;
+ attstorage 
+------------
+ p
+(1 row)
+
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_btree'::regclass AND attnum = 1;
+ attstorage 
+------------
+ e
+(1 row)
+
+INSERT INTO gst SELECT 'item ' || g FROM generate_series(1, 2000) g;
+DROP TABLE gst;
diff --git a/contrib/pg_trgm/sql/pg_trgm.sql b/contrib/pg_trgm/sql/pg_trgm.sql
index 49db86caf7d..aea58a2e252 100644
--- a/contrib/pg_trgm/sql/pg_trgm.sql
+++ b/contrib/pg_trgm/sql/pg_trgm.sql
@@ -244,3 +244,15 @@ SELECT DISTINCT city, similarity(city, 'Warsaw'), show_limit()
 SELECT set_limit(0.5);
 SELECT DISTINCT city, similarity(city, 'Warsaw'), show_limit()
   FROM restaurants WHERE city % 'Warsaw';
+
+-- SET STORAGE must not copy heap attstorage onto gtrgm.
+CREATE TABLE gst (s text);
+CREATE INDEX gst_gist ON gst USING gist (s gist_trgm_ops);
+CREATE INDEX gst_btree ON gst (s);
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_gist'::regclass AND attnum = 1;
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_btree'::regclass AND attnum = 1;
+ALTER TABLE gst ALTER COLUMN s SET STORAGE EXTERNAL;
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_gist'::regclass AND attnum = 1;
+SELECT attstorage FROM pg_attribute WHERE attrelid = 'gst_btree'::regclass AND attnum = 1;
+INSERT INTO gst SELECT 'item ' || g FROM generate_series(1, 2000) g;
+DROP TABLE gst;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..f0772c56a02 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9235,18 +9235,31 @@ SetIndexStorageProperties(Relation rel, Relation attrelation,
 		if (HeapTupleIsValid(tuple))
 		{
 			Form_pg_attribute attrtuple = (Form_pg_attribute) GETSTRUCT(tuple);
+			const FormData_pg_attribute *heapatt;
 
-			if (setstorage)
-				attrtuple->attstorage = newstorage;
+			heapatt = TupleDescAttr(RelationGetDescr(rel),
+									attnum - 1);
 
-			if (setcompression)
-				attrtuple->attcompression = newcompression;
+			/*
+			 * ConstructTupleDescriptor() copies heap attstorage only when the
+			 * index column has the same type as the heap column.  An opclass
+			 * STORAGE type keeps its own typstorage.  Do not overwrite that
+			 * with the heap setting.
+			 */
+			if (attrtuple->atttypid == heapatt->atttypid)
+			{
+				if (setstorage)
+					attrtuple->attstorage = newstorage;
 
-			CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
+				if (setcompression)
+					attrtuple->attcompression = newcompression;
 
-			InvokeObjectPostAlterHook(RelationRelationId,
-									  RelationGetRelid(rel),
-									  attrtuple->attnum);
+				CatalogTupleUpdate(attrelation, &tuple->t_self, tuple);
+
+				InvokeObjectPostAlterHook(RelationRelationId,
+										  RelationGetRelid(rel),
+										  attrtuple->attnum);
+			}
 
 			heap_freetuple(tuple);
 		}
-- 
2.53.0
