From 121cc204f732d5aeffb71cfa513b0ecb4bba1985 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 10 Nov 2025 14:33:41 -0500
Subject: [PATCH v4 2/2] Add --dry-run to vacuumdb.

This option answers the question "what tables would be affected if I ran
a command using these options" without actually initiating those
actions.
---
 doc/src/sgml/ref/vacuumdb.sgml    | 10 +++++++
 src/bin/scripts/t/100_vacuumdb.pl | 12 ++++++++
 src/bin/scripts/vacuumdb.c        | 11 ++++++-
 src/bin/scripts/vacuuming.c       | 50 +++++++++++++++++--------------
 src/bin/scripts/vacuuming.h       |  3 +-
 5 files changed, 62 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 84c76d7350c..89aa7fac4f8 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -171,6 +171,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--dry-run</option></term>
+      <listitem>
+       <para>
+        Print, but do not execute, the vacuum and analyze commands that would
+        have been sent to the server (performs a dry run).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-e</option></term>
       <term><option>--echo</option></term>
diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index a16fad593f7..fb2fecdd3c6 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -169,6 +169,10 @@ $node->issues_sql_like(
 	[ 'vacuumdb', '--schema' => '"Foo"', 'postgres' ],
 	qr/VACUUM \(SKIP_DATABASE_STATS\) "Foo".bar/,
 	'vacuumdb --schema');
+$node->issues_sql_unlike(
+	[ 'vacuumdb', '--schema' => '"Foo"', 'postgres', '--dry-run' ],
+	qr/VACUUM \(SKIP_DATABASE_STATS\) "Foo".bar/,
+	'vacuumdb --dry-run');
 $node->issues_sql_like(
 	[ 'vacuumdb', '--schema' => '"Foo"', '--schema' => '"Bar"', 'postgres' ],
 	qr/VACUUM\ \(SKIP_DATABASE_STATS\)\ "Foo".bar
@@ -241,6 +245,14 @@ $node->safe_psql('postgres', q|
   CREATE TABLE regression_vacuumdb_test AS select generate_series(1, 10) a, generate_series(2, 11) b;
   ALTER TABLE regression_vacuumdb_test ADD COLUMN c INT GENERATED ALWAYS AS (a + b);
 |);
+$node->issues_sql_unlike(
+	[
+		'vacuumdb', '--analyze-only', '--dry-run',
+		'--missing-stats-only', '-t',
+		'regression_vacuumdb_test', 'postgres'
+	],
+	qr/statement:\ ANALYZE/sx,
+	'--missing-stats-only --dry-run');
 $node->issues_sql_like(
 	[
 		'vacuumdb', '--analyze-only',
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e117dac2242..68323fd2d10 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -59,6 +59,7 @@ main(int argc, char *argv[])
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
+		{"dry-run", no_argument, NULL, 15},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -70,6 +71,7 @@ main(int argc, char *argv[])
 	ConnParams	cparams;
 	bool		echo = false;
 	bool		quiet = false;
+	bool		dry_run = false;
 	vacuumingOptions vacopts;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
@@ -213,6 +215,9 @@ main(int argc, char *argv[])
 			case 14:
 				vacopts.missing_stats_only = true;
 				break;
+			case 15:
+				dry_run = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -309,10 +314,13 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option without \"%s\" or \"%s\"",
 				 "missing-stats-only", "analyze-only", "analyze-in-stages");
 
+	if (dry_run)
+		pg_log_info("Executing in dry-run mode.");
+
 	ret = vacuuming_main(&cparams, dbname, maintenance_db, &vacopts,
 						 &objects, tbl_count,
 						 concurrentCons,
-						 progname, echo, quiet);
+						 progname, echo, quiet, dry_run);
 	exit(ret);
 }
 
@@ -351,6 +359,7 @@ help(const char *progname)
 	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
+	printf(_("      --dry-run                   dry run, show commands that would be sent to the server\n"));
 	printf(_("  -e, --echo                      show the commands being sent to the server\n"));
 	printf(_("  -f, --full                      do full vacuuming\n"));
 	printf(_("  -F, --freeze                    freeze row transaction information\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index f836f21fb03..b7e0ce65c2e 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -30,12 +30,14 @@ static int	vacuum_one_database(ConnParams *cparams,
 								SimpleStringList *objects,
 								SimpleStringList **found_objs,
 								int concurrentCons,
-								const char *progname, bool echo, bool quiet);
+								const char *progname, bool echo, bool quiet,
+								bool dry_run);
 static int	vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
 								 SimpleStringList *objects,
 								 int concurrentCons,
-								 const char *progname, bool echo, bool quiet);
+								 const char *progname, bool echo, bool quiet,
+								 bool dry_run);
 static SimpleStringList *retrieve_objects(PGconn *conn,
 										  vacuumingOptions *vacopts,
 										  SimpleStringList *objects,
@@ -43,8 +45,8 @@ static SimpleStringList *retrieve_objects(PGconn *conn,
 static void free_retrieved_objects(SimpleStringList *list);
 static void prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 								   vacuumingOptions *vacopts, const char *table);
-static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
-							   const char *table);
+static void run_vacuum_command(ParallelSlot *free_slot, const char *sql,
+							   bool echo, bool dry_run, const char *table);
 
 /*
  * Executes vacuum/analyze as indicated.  Returns 0 if the plan is carried
@@ -56,7 +58,7 @@ vacuuming_main(ConnParams *cparams, const char *dbname,
 			   const char *maintenance_db, vacuumingOptions *vacopts,
 			   SimpleStringList *objects,
 			   unsigned int tbl_count, int concurrentCons,
-			   const char *progname, bool echo, bool quiet)
+			   const char *progname, bool echo, bool quiet, bool dry_run)
 {
 	setup_cancel_handler(NULL);
 
@@ -71,7 +73,7 @@ vacuuming_main(ConnParams *cparams, const char *dbname,
 		return vacuum_all_databases(cparams, vacopts,
 									objects,
 									concurrentCons,
-									progname, echo, quiet);
+									progname, echo, quiet, dry_run);
 	}
 	else
 	{
@@ -100,7 +102,7 @@ vacuuming_main(ConnParams *cparams, const char *dbname,
 										  objects,
 										  vacopts->missing_stats_only ? &found_objs : NULL,
 										  concurrentCons,
-										  progname, echo, quiet);
+										  progname, echo, quiet, dry_run);
 				if (ret != 0)
 				{
 					free_retrieved_objects(found_objs);
@@ -116,7 +118,7 @@ vacuuming_main(ConnParams *cparams, const char *dbname,
 									   ANALYZE_NO_STAGE,
 									   objects, NULL,
 									   concurrentCons,
-									   progname, echo, quiet);
+									   progname, echo, quiet, dry_run);
 	}
 }
 
@@ -167,7 +169,7 @@ vacuum_one_database(ConnParams *cparams,
 					SimpleStringList *objects,
 					SimpleStringList **found_objs,
 					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+					const char *progname, bool echo, bool quiet, bool dry_run)
 {
 	PQExpBufferData sql;
 	PGconn	   *conn;
@@ -338,7 +340,7 @@ vacuum_one_database(ConnParams *cparams,
 	 * caller requested that mode.  We have to prepare the initial connection
 	 * ourselves before setting up the slots.
 	 */
-	if (vacopts->mode == MODE_ANALYZE_IN_STAGES)
+	if (vacopts->mode == MODE_ANALYZE_IN_STAGES && !dry_run)
 	{
 		initcmd = stage_commands[stage];
 		executeCommand(conn, initcmd, echo);
@@ -383,8 +385,8 @@ vacuum_one_database(ConnParams *cparams,
 		 * through ParallelSlotsGetIdle.
 		 */
 		ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
-		run_vacuum_command(free_slot->connection, sql.data,
-						   echo, tabname);
+		run_vacuum_command(free_slot, sql.data,
+						   echo, dry_run, tabname);
 
 		cell = cell->next;
 	} while (cell != NULL);
@@ -408,7 +410,7 @@ vacuum_one_database(ConnParams *cparams,
 		}
 
 		ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
-		run_vacuum_command(free_slot->connection, cmd, echo, NULL);
+		run_vacuum_command(free_slot, cmd, echo, dry_run, NULL);
 
 		if (!ParallelSlotsWaitCompletion(sa))
 			ret = EXIT_FAILURE; /* error already reported by handler */
@@ -436,7 +438,7 @@ vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
 					 SimpleStringList *objects,
 					 int concurrentCons,
-					 const char *progname, bool echo, bool quiet)
+					 const char *progname, bool echo, bool quiet, bool dry_run)
 {
 	int			ret = EXIT_SUCCESS;
 	PGconn	   *conn;
@@ -474,7 +476,7 @@ vacuum_all_databases(ConnParams *cparams,
 										  objects,
 										  vacopts->missing_stats_only ? &found_objs[i] : NULL,
 										  concurrentCons,
-										  progname, echo, quiet);
+										  progname, echo, quiet, dry_run);
 				if (ret != EXIT_SUCCESS)
 					break;
 			}
@@ -499,7 +501,7 @@ vacuum_all_databases(ConnParams *cparams,
 									  objects,
 									  NULL,
 									  concurrentCons,
-									  progname, echo, quiet);
+									  progname, echo, quiet, dry_run);
 			if (ret != EXIT_SUCCESS)
 				break;
 		}
@@ -1001,15 +1003,19 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
  * Any errors during command execution are reported to stderr.
  */
 static void
-run_vacuum_command(PGconn *conn, const char *sql, bool echo,
-				   const char *table)
+run_vacuum_command(ParallelSlot *free_slot, const char *sql,
+				   bool echo, bool dry_run, const char *table)
 {
-	bool		status;
+	bool		status = true;
+	PGconn	   *conn = free_slot->connection;
 
-	if (echo)
-		printf("%s\n", sql);
+	if (echo || dry_run)
+		printf("%s%s\n", sql, dry_run ? "  -- not executed" : "");
 
-	status = PQsendQuery(conn, sql) == 1;
+	if (dry_run)
+		ParallelSlotSetIdle(free_slot);
+	else
+		status = PQsendQuery(conn, sql) == 1;
 
 	if (!status)
 	{
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 49f968b32e5..46e53fc69c3 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -65,7 +65,8 @@ extern int	vacuuming_main(ConnParams *cparams, const char *dbname,
 						   SimpleStringList *objects,
 						   unsigned int tbl_count,
 						   int concurrentCons,
-						   const char *progname, bool echo, bool quiet);
+						   const char *progname, bool echo, bool quiet,
+						   bool dry_run);
 
 extern char *escape_quotes(const char *src);
 
-- 
2.39.5 (Apple Git-154)

