From 33c32daa99511c0e177f39e4cd8f95015192b0e5 Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Tue, 11 Aug 2026 21:59:44 +0000
Subject: [PATCH 1/2] Preserve statistics target of extended statistics rebuilt
 by ALTER TABLE

When ALTER TABLE ALTER COLUMN TYPE (or SET EXPRESSION) recreates the
extended statistics objects that depend on the altered column, it does
so by capturing each object's definition string and replaying it
through CREATE STATISTICS.  A statistics target set with ALTER
STATISTICS ... SET STATISTICS is not part of that definition string,
and CreateStatistics() always stored a null stxstattarget, so the
target silently reverted to the default after the rebuild.

This is the same class of problem as the statistics owner reset that
commit 6713a6e04cb fixed, and the fix follows the same pattern: have
RememberStatisticsForRebuilding() capture stxstattarget along with the
owner, thread it through ATPostAlterTypeParse() into the recreated
CreateStatsStmt, and make CreateStatistics() store it.  CreateStatsStmt
gains a stxstattarget field, with -1 meaning the default (null) target.
---
 src/backend/commands/statscmds.c        |  6 +++-
 src/backend/commands/tablecmds.c        | 37 +++++++++++++++++--------
 src/backend/parser/gram.y               |  2 ++
 src/backend/parser/parse_utilcmd.c      |  1 +
 src/include/nodes/parsenodes.h          |  1 +
 src/test/regress/expected/stats_ext.out | 20 +++++++------
 src/test/regress/sql/stats_ext.sql      |  8 ++++--
 7 files changed, 51 insertions(+), 24 deletions(-)

diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index c4b5b478644..ee358c7dd92 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -563,7 +563,11 @@ CreateStatistics(List *relids, CreateStatsStmt *stmt, bool check_rights)
 	values[Anum_pg_statistic_ext_stxnamespace - 1] = ObjectIdGetDatum(namespaceId);
 	values[Anum_pg_statistic_ext_stxowner - 1] = ObjectIdGetDatum(stxowner);
 	values[Anum_pg_statistic_ext_stxkeys - 1] = PointerGetDatum(stxkeys);
-	nulls[Anum_pg_statistic_ext_stxstattarget - 1] = true;
+	if (stmt->stxstattarget >= 0)
+		values[Anum_pg_statistic_ext_stxstattarget - 1] =
+			Int16GetDatum(stmt->stxstattarget);
+	else
+		nulls[Anum_pg_statistic_ext_stxstattarget - 1] = true;
 	values[Anum_pg_statistic_ext_stxkind - 1] = PointerGetDatum(stxkind);
 
 	values[Anum_pg_statistic_ext_stxexprs - 1] = exprsDatum;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6857315c071..16e89e848d9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -212,6 +212,7 @@ typedef struct AlteredTableInfo
 	List	   *changedStatisticsOids;	/* OIDs of statistics to rebuild */
 	List	   *changedStatisticsDefs;	/* string definitions of same */
 	List	   *changedStatisticsOwners;	/* owners of same */
+	List	   *changedStatisticsTargets;	/* stxstattarget of same, or -1 */
 } AlteredTableInfo;
 
 /* Struct describing one new constraint to check in Phase 3 scan */
@@ -701,8 +702,8 @@ static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
 static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab,
 								   LOCKMODE lockmode);
 static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId,
-								 char *cmd, List **wqueue, LOCKMODE lockmode,
-								 bool rewrite);
+								 int stxstattarget, char *cmd, List **wqueue,
+								 LOCKMODE lockmode, bool rewrite);
 static void RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass,
 									 Oid objid, Relation rel, List *domname,
 									 const char *conname);
@@ -16134,6 +16135,8 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab)
 		char	   *defstring = pg_get_statisticsobjdef_string(stxoid);
 		HeapTuple	tup;
 		Form_pg_statistic_ext statext;
+		Datum		target;
+		bool		targetisnull;
 
 		tup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(stxoid));
 
@@ -16142,6 +16145,10 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab)
 
 		statext = (Form_pg_statistic_ext) GETSTRUCT(tup);
 
+		target = SysCacheGetAttr(STATEXTOID, tup,
+								 Anum_pg_statistic_ext_stxstattarget,
+								 &targetisnull);
+
 		tab->changedStatisticsOids = lappend_oid(tab->changedStatisticsOids,
 												 stxoid);
 		tab->changedStatisticsDefs = lappend(tab->changedStatisticsDefs,
@@ -16150,6 +16157,10 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab)
 		tab->changedStatisticsOwners = lappend_oid(tab->changedStatisticsOwners,
 												   statext->stxowner);
 
+		tab->changedStatisticsTargets =
+			lappend_int(tab->changedStatisticsTargets,
+						targetisnull ? -1 : (int) DatumGetInt16(target));
+
 		ReleaseSysCache(tup);
 	}
 }
@@ -16169,6 +16180,7 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 	ListCell   *def_item;
 	ListCell   *oid_item;
 	ListCell   *owner_item;
+	ListCell   *target_item;
 
 	/*
 	 * Collect all the constraints and indexes to drop so we can process them
@@ -16242,7 +16254,7 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 		if (relid != tab->relid)
 			LockRelationOid(relid, AccessExclusiveLock);
 
-		ATPostAlterTypeParse(oldId, relid, confrelid, InvalidOid,
+		ATPostAlterTypeParse(oldId, relid, confrelid, InvalidOid, -1,
 							 (char *) lfirst(def_item),
 							 wqueue, lockmode, tab->rewrite);
 	}
@@ -16261,7 +16273,7 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 		if (relid != tab->relid)
 			LockRelationOid(relid, AccessExclusiveLock);
 
-		ATPostAlterTypeParse(oldId, relid, InvalidOid, InvalidOid,
+		ATPostAlterTypeParse(oldId, relid, InvalidOid, InvalidOid, -1,
 							 (char *) lfirst(def_item),
 							 wqueue, lockmode, tab->rewrite);
 
@@ -16270,9 +16282,10 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 	}
 
 	/* add dependencies for new statistics */
-	forthree(oid_item, tab->changedStatisticsOids,
-			 def_item, tab->changedStatisticsDefs,
-			 owner_item, tab->changedStatisticsOwners)
+	forfour(oid_item, tab->changedStatisticsOids,
+			def_item, tab->changedStatisticsDefs,
+			owner_item, tab->changedStatisticsOwners,
+			target_item, tab->changedStatisticsTargets)
 	{
 		Oid			oldId = lfirst_oid(oid_item);
 		Oid			relid;
@@ -16293,8 +16306,9 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 			LockRelationOid(relid, ShareUpdateExclusiveLock);
 
 		ATPostAlterTypeParse(oldId, relid, InvalidOid, lfirst_oid(owner_item),
-							 (char *) lfirst(def_item),
-							 wqueue, lockmode, tab->rewrite);
+							 lfirst_int(target_item),
+							 (char *) lfirst(def_item), wqueue, lockmode,
+							 tab->rewrite);
 
 		ObjectAddressSet(obj, StatisticExtRelationId, oldId);
 		add_exact_object_address(&obj, objects);
@@ -16357,8 +16371,8 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
  */
 static void
 ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId,
-					 char *cmd, List **wqueue, LOCKMODE lockmode,
-					 bool rewrite)
+					 int stxstattarget, char *cmd, List **wqueue,
+					 LOCKMODE lockmode, bool rewrite)
 {
 	List	   *raw_parsetree_list;
 	List	   *querytree_list;
@@ -16403,6 +16417,7 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId,
 
 			csstmt = transformStatsStmt(oldRelId, (CreateStatsStmt *) stmt, cmd);
 			csstmt->owner = ownerId;
+			csstmt->stxstattarget = stxstattarget;
 
 			querytree_list = lappend(querytree_list, csstmt);
 		}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 17035fb4d15..d3370055ec3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4900,6 +4900,7 @@ CreateStatsStmt:
 					n->relations = $8;
 					n->stxcomment = NULL;
 					n->if_not_exists = false;
+					n->stxstattarget = -1;
 					$$ = (Node *) n;
 				}
 			| CREATE STATISTICS IF_P NOT EXISTS any_name
@@ -4913,6 +4914,7 @@ CreateStatsStmt:
 					n->relations = $11;
 					n->stxcomment = NULL;
 					n->if_not_exists = true;
+					n->stxstattarget = -1;
 					$$ = (Node *) n;
 				}
 			;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 2fe7b86761d..565ff2b848d 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2172,6 +2172,7 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
 	stats->stxcomment = NULL;
 	stats->transformed = true;	/* don't need transformStatsStmt again */
 	stats->if_not_exists = false;
+	stats->stxstattarget = -1;
 
 	/* Clean up */
 	ReleaseSysCache(ht_stats);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8a9df884276..b99ac94cb3e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3675,6 +3675,7 @@ typedef struct CreateStatsStmt
 	bool		transformed;	/* true when transformStatsStmt is finished */
 	bool		if_not_exists;	/* do nothing if stats name already exists */
 	Oid			owner;			/* OID of owner, or InvalidOid for default */
+	int			stxstattarget;	/* statistics target, or -1 for default */
 } CreateStatsStmt;
 
 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 4512670d525..bb878a33b93 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3589,18 +3589,20 @@ CREATE TABLE stats_ext_tbl (a int, b int);
 ALTER TABLE stats_ext_tbl OWNER TO regress_relowner;
 CREATE STATISTICS tst ON a, b FROM stats_ext_tbl;
 ALTER STATISTICS tst OWNER TO regress_stxowner;
-SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst';
-     stxowner     
-------------------
- regress_stxowner
+ALTER STATISTICS tst SET STATISTICS 1000;
+SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst';
+     stxowner     | stxstattarget 
+------------------+---------------
+ regress_stxowner |          1000
 (1 row)
 
--- re-creating statistics via ALTER TABLE preserve the statistics owner.
+-- re-creating statistics via ALTER TABLE preserve the statistics owner
+-- and target.
 ALTER TABLE stats_ext_tbl ALTER COLUMN a TYPE bigint;
-SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst';
-     stxowner     
-------------------
- regress_stxowner
+SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst';
+     stxowner     | stxstattarget 
+------------------+---------------
+ regress_stxowner |          1000
 (1 row)
 
 -- Tidy up
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index a9176e74f3c..a9bdb3a62df 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1839,11 +1839,13 @@ CREATE TABLE stats_ext_tbl (a int, b int);
 ALTER TABLE stats_ext_tbl OWNER TO regress_relowner;
 CREATE STATISTICS tst ON a, b FROM stats_ext_tbl;
 ALTER STATISTICS tst OWNER TO regress_stxowner;
-SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst';
+ALTER STATISTICS tst SET STATISTICS 1000;
+SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst';
 
--- re-creating statistics via ALTER TABLE preserve the statistics owner.
+-- re-creating statistics via ALTER TABLE preserve the statistics owner
+-- and target.
 ALTER TABLE stats_ext_tbl ALTER COLUMN a TYPE bigint;
-SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst';
+SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst';
 
 -- Tidy up
 DROP TABLE stats_ext_tbl;
-- 
2.54.0

