From 8d2ca7a6510dc646045cc3be552a61941ff28c6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kaval=C3=ADk?= <jkavalik@gmail.com>
Date: Sun, 25 May 2025 23:23:56 +0200
Subject: [PATCH] ALTER TABLE progress support

---
 doc/src/sgml/monitoring.sgml         | 13 +++----
 doc/src/sgml/ref/alter_table.sgml    | 10 ++++++
 src/backend/catalog/storage.c        |  7 ++++
 src/backend/catalog/system_views.sql |  2 ++
 src/backend/commands/tablecmds.c     | 53 ++++++++++++++++++++++++++++
 src/include/commands/progress.h      |  2 ++
 src/test/regress/expected/rules.out  |  2 ++
 7 files changed, 83 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 823afe1b30b..02d70a86731 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -400,7 +400,7 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      <row>
       <entry><structname>pg_stat_progress_cluster</structname><indexterm><primary>pg_stat_progress_cluster</primary></indexterm></entry>
       <entry>One row for each backend running
-       <command>CLUSTER</command> or <command>VACUUM FULL</command>, showing current progress.
+       <command>CLUSTER</command>, <command>VACUUM FULL</command> or <command>ALTER TABLE</command>, showing current progress.
        See <xref linkend="cluster-progress-reporting"/>.
       </entry>
      </row>
@@ -5494,7 +5494,7 @@ FROM pg_stat_get_backend_idset() AS backendid;
    <productname>PostgreSQL</productname> has the ability to report the progress of
    certain commands during command execution.  Currently, the only commands
    which support progress reporting are <command>ANALYZE</command>,
-   <command>CLUSTER</command>,
+   <command>CLUSTER</command>, <command>ALTER TABLE</command>,
    <command>CREATE INDEX</command>, <command>VACUUM</command>,
    <command>COPY</command>,
    and <xref linkend="protocol-replication-base-backup"/> (i.e., replication
@@ -5740,8 +5740,9 @@ FROM pg_stat_get_backend_idset() AS backendid;
   </indexterm>
 
   <para>
-   Whenever <command>CLUSTER</command> or <command>VACUUM FULL</command> is
-   running, the <structname>pg_stat_progress_cluster</structname> view will
+   Whenever <command>CLUSTER</command>, <command>VACUUM FULL</command>
+   or <command>ALTER TABLE</command> is running,
+   the <structname>pg_stat_progress_cluster</structname> view will
    contain a row for each backend that is currently running either command.
    The tables below describe the information that will be reported and
    provide information about how to interpret it.
@@ -5803,7 +5804,7 @@ FROM pg_stat_get_backend_idset() AS backendid;
        <structfield>command</structfield> <type>text</type>
       </para>
       <para>
-       The command that is running. Either <literal>CLUSTER</literal> or <literal>VACUUM FULL</literal>.
+       The command that is running. Either <literal>CLUSTER</literal>, <literal>VACUUM FULL</literal> or <literal>ALTER TABLE</literal>.
       </para></entry>
      </row>
 
@@ -5886,7 +5887,7 @@ FROM pg_stat_get_backend_idset() AS backendid;
   </table>
 
   <table id="cluster-phases">
-   <title>CLUSTER and VACUUM FULL Phases</title>
+   <title>CLUSTER, VACUUM FULL and ALTER TABLE Phases</title>
    <tgroup cols="2">
     <colspec colname="col1" colwidth="1*"/>
     <colspec colname="col2" colwidth="2*"/>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 1e4f26c13f6..865b14ba7d9 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1859,6 +1859,16 @@ ALTER TABLE measurement
   </para>
  </refsect1>
 
+ <refsect1>
+  <title>Progress Reporting</title>
+  <para>
+    When an <command>ALTER TABLE</command> operation rewrites the table, progress
+    can be monitored via the <literal>pg_stat_progress_cluster</literal> system view,
+    similar to <command>CLUSTER</command> and <command>VACUUM FULL</command> commands.
+    The command type will be reported as <literal>'ALTER TABLE'</literal>.
+  </para>
+ </refsect1>
+
  <refsect1>
   <title>See Also</title>
 
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 227df90f89c..79d14d86bc9 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -26,6 +26,7 @@
 #include "access/xlogutils.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
+#include "commands/progress.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "storage/bulk_write.h"
@@ -505,6 +506,9 @@ RelationCopyStorage(SMgrRelation src, SMgrRelation dst,
 
 	nblocks = smgrnblocks(src, forkNum);
 
+	/* Report expected number of block to copy */
+	pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS, nblocks);
+
 	for (blkno = 0; blkno < nblocks; blkno++)
 	{
 		BulkWriteBuffer buf;
@@ -556,6 +560,9 @@ RelationCopyStorage(SMgrRelation src, SMgrRelation dst,
 		 * page including any unused space.
 		 */
 		smgr_bulk_write(bulkstate, blkno, buf, false);
+
+		/* Update progress report */
+		pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, blkno + 1);
 	}
 	smgr_bulk_finish(bulkstate);
 }
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index b2d5332effc..9d68d02ec26 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1260,6 +1260,7 @@ CREATE VIEW pg_stat_progress_cluster AS
         S.relid AS relid,
         CASE S.param1 WHEN 1 THEN 'CLUSTER'
                       WHEN 2 THEN 'VACUUM FULL'
+                      WHEN 3 THEN 'ALTER TABLE'
                       END AS command,
         CASE S.param2 WHEN 0 THEN 'initializing'
                       WHEN 1 THEN 'seq scanning heap'
@@ -1269,6 +1270,7 @@ CREATE VIEW pg_stat_progress_cluster AS
                       WHEN 5 THEN 'swapping relation files'
                       WHEN 6 THEN 'rebuilding index'
                       WHEN 7 THEN 'performing final cleanup'
+                      WHEN 8 THEN 'checking foreign key constraints'
                       END AS phase,
         CAST(S.param3 AS oid) AS cluster_index_relid,
         S.param4 AS heap_tuples_scanned,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..43376539209 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -59,6 +59,7 @@
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
+#include "commands/progress.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
@@ -5838,6 +5839,10 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 		if (!RELKIND_HAS_STORAGE(tab->relkind))
 			continue;
 
+		/* Start progress reporting */
+		pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tab->relid);
+		pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, PROGRESS_CLUSTER_COMMAND_ALTER_TABLE);
+
 		/*
 		 * If we change column data types, the operation has to be propagated
 		 * to tables that use this table's rowtype as a column type.
@@ -5978,6 +5983,9 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 */
 			ATRewriteTable(tab, OIDNewHeap);
 
+			/* Report that we are now swapping relation files */
+			pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
+										 PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES);
 			/*
 			 * Swap the physical files of the old and new heaps, then rebuild
 			 * indexes and discard the old heap.  We can use RecentXmin for
@@ -6089,6 +6097,10 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			table_close(rel, NoLock);
 	}
 
+	/* Report that we are now doing clean up */
+	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
+								 PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP);
+
 	/* Finally, run any afterStmts that were queued up */
 	foreach(ltab, *wqueue)
 	{
@@ -6103,6 +6115,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			CommandCounterIncrement();
 		}
 	}
+
+	pgstat_progress_end_command();
 }
 
 /*
@@ -6128,6 +6142,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	BulkInsertState bistate;
 	int			ti_options;
 	ExprState  *partqualstate = NULL;
+	int64		numTuples = 0;
 
 	/*
 	 * Open the relation(s).  We have surely already locked the existing
@@ -6137,6 +6152,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	oldTupDesc = tab->oldDesc;
 	newTupDesc = RelationGetDescr(oldrel);	/* includes all mods */
 
+	/* Update progress reporting - we are actually scanning and possibly rewriting the table */
+	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP);
+
 	if (OidIsValid(OIDNewHeap))
 	{
 		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
@@ -6244,6 +6262,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		TupleTableSlot *oldslot;
 		TupleTableSlot *newslot;
 		TableScanDesc scan;
+		HeapScanDesc heapScan;
 		MemoryContext oldCxt;
 		List	   *dropped_attrs = NIL;
 		ListCell   *lc;
@@ -6343,6 +6362,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		 */
 		snapshot = RegisterSnapshot(GetLatestSnapshot());
 		scan = table_beginscan(oldrel, snapshot, 0, NULL);
+		heapScan = (HeapScanDesc) scan;
+		pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS,
+											 heapScan->rs_nblocks);
 
 		/*
 		 * Switch to per-tuple memory context and reset it for each tuple
@@ -6353,6 +6375,13 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		while (table_scan_getnextslot(scan, ForwardScanDirection, oldslot))
 		{
 			TupleTableSlot *insertslot;
+			pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
+											(heapScan->rs_cblock +
+											 heapScan->rs_nblocks -
+											 heapScan->rs_startblock
+											 ) % heapScan->rs_nblocks + 1);
+			pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED,
+											 ++numTuples);
 
 			if (tab->rewrite > 0)
 			{
@@ -6401,6 +6430,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 
 				ExecStoreVirtualTuple(newslot);
 
+				pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN,
+											 numTuples);
+
 				/*
 				 * Now, evaluate any expressions whose inputs come from the
 				 * new tuple.  We assume these columns won't reference each
@@ -6521,6 +6553,8 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 
 			CHECK_FOR_INTERRUPTS();
 		}
+		pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
+													 heapScan->rs_nblocks);
 
 		MemoryContextSwitchTo(oldCxt);
 		table_endscan(scan);
@@ -13664,10 +13698,12 @@ validateForeignKeyConstraint(char *conname,
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
+	HeapScanDesc heapScan;
 	Trigger		trig = {0};
 	Snapshot	snapshot;
 	MemoryContext oldcxt;
 	MemoryContext perTupCxt;
+	int64		numTuples = 0;
 
 	ereport(DEBUG1,
 			(errmsg_internal("validating foreign key constraint \"%s\"", conname)));
@@ -13686,6 +13722,10 @@ validateForeignKeyConstraint(char *conname,
 	trig.tginitdeferred = false;
 	/* we needn't fill in remaining fields */
 
+	/* Report that we are now checking foreign keys */
+	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
+								 PROGRESS_CLUSTER_PHASE_CHECK_FKEYS);
+
 	/*
 	 * See if we can do it with a single LEFT JOIN query.  A false result
 	 * indicates we must proceed with the fire-the-trigger method. We can't do
@@ -13703,6 +13743,7 @@ validateForeignKeyConstraint(char *conname,
 	snapshot = RegisterSnapshot(GetLatestSnapshot());
 	slot = table_slot_create(rel, NULL);
 	scan = table_beginscan(rel, snapshot, 0, NULL);
+	heapScan = (HeapScanDesc) scan;
 
 	perTupCxt = AllocSetContextCreate(CurrentMemoryContext,
 									  "validateForeignKeyConstraint",
@@ -13738,6 +13779,14 @@ validateForeignKeyConstraint(char *conname,
 		RI_FKey_check_ins(fcinfo);
 
 		MemoryContextReset(perTupCxt);
+
+		pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
+										(heapScan->rs_cblock +
+										 heapScan->rs_nblocks -
+										 heapScan->rs_startblock
+										 ) % heapScan->rs_nblocks + 1);
+		pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED,
+										 ++numTuples);
 	}
 
 	MemoryContextSwitchTo(oldcxt);
@@ -16828,6 +16877,10 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
 	 */
 	rel = relation_open(tableOid, lockmode);
 
+	/* Update progress reporting - we are copying the table */
+	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP);
+	pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS, RelationGetNumberOfBlocks(rel));
+
 	/* Check first if relation can be moved to new tablespace */
 	if (!CheckRelationTableSpaceMove(rel, newTableSpace))
 	{
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 7c736e7b03b..56585742838 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -74,10 +74,12 @@
 #define PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES	5
 #define PROGRESS_CLUSTER_PHASE_REBUILD_INDEX	6
 #define PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP	7
+#define PROGRESS_CLUSTER_PHASE_CHECK_FKEYS		8
 
 /* Commands of PROGRESS_CLUSTER */
 #define PROGRESS_CLUSTER_COMMAND_CLUSTER		1
 #define PROGRESS_CLUSTER_COMMAND_VACUUM_FULL	2
+#define PROGRESS_CLUSTER_COMMAND_ALTER_TABLE    3    /* New command type */
 
 /* Progress parameters for CREATE INDEX */
 /* 3, 4 and 5 reserved for "waitfor" metrics */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index dce8c672b40..b0d498d0339 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1986,6 +1986,7 @@ pg_stat_progress_cluster| SELECT s.pid,
         CASE s.param1
             WHEN 1 THEN 'CLUSTER'::text
             WHEN 2 THEN 'VACUUM FULL'::text
+            WHEN 3 THEN 'ALTER TABLE'::text
             ELSE NULL::text
         END AS command,
         CASE s.param2
@@ -1997,6 +1998,7 @@ pg_stat_progress_cluster| SELECT s.pid,
             WHEN 5 THEN 'swapping relation files'::text
             WHEN 6 THEN 'rebuilding index'::text
             WHEN 7 THEN 'performing final cleanup'::text
+            WHEN 8 THEN 'checking foreign key constraints'::text
             ELSE NULL::text
         END AS phase,
     (s.param3)::oid AS cluster_index_relid,
-- 
2.34.1

