From 5c9925e40524d1773452169ec581ad6571c20ae4 Mon Sep 17 00:00:00 2001
From: Christophe Pettus <cpettus@pgexperts.com>
Date: Wed, 29 Jul 2026 09:19:00 -0700
Subject: [PATCH] vacuumdb, clusterdb, reindexdb: Add --continue

With --all, these commands connect to each database in turn, and a failed
connection was fatal, so a single database that refuses connections
prevented every remaining database from being processed.  Managed
PostgreSQL services commonly reserve a database for the provider that
ordinary users cannot connect to, which defeats --all on those systems.

Add --continue, which reports such a database as a warning and moves on to
the next one.  Without the option the previous behavior is unchanged, and
the option is rejected unless --all is also given, so a database named on
the command line is never silently skipped.

cluster_one_database() and reindex_one_database() now return a bool
indicating whether the database was processed, since both are called more
than once for a single database and only the first call should report it.

So that the warning can carry the reason the connection failed,
connectDatabase() now hands a failed connection back to the caller when
fail_ok is set, rather than closing it and returning NULL; its only
existing fail_ok caller, connectMaintenanceDatabase(), is adjusted to match.
---
 doc/src/sgml/ref/clusterdb.sgml    |  20 +++++
 doc/src/sgml/ref/reindexdb.sgml    |  20 +++++
 doc/src/sgml/ref/vacuumdb.sgml     |  20 +++++
 src/bin/scripts/clusterdb.c        |  59 +++++++++++----
 src/bin/scripts/reindexdb.c        | 114 ++++++++++++++++++++---------
 src/bin/scripts/t/010_clusterdb.pl |  27 +++++++
 src/bin/scripts/t/090_reindexdb.pl |  27 +++++++
 src/bin/scripts/t/100_vacuumdb.pl  |  28 +++++++
 src/bin/scripts/vacuumdb.c         |  11 +++
 src/bin/scripts/vacuuming.c        |  19 ++++-
 src/bin/scripts/vacuuming.h        |   2 +
 src/fe_utils/connect_utils.c       |  13 ++--
 12 files changed, 306 insertions(+), 54 deletions(-)

diff --git a/doc/src/sgml/ref/clusterdb.sgml b/doc/src/sgml/ref/clusterdb.sgml
index 0d2051bf6f1..0c97067526c 100644
--- a/doc/src/sgml/ref/clusterdb.sgml
+++ b/doc/src/sgml/ref/clusterdb.sgml
@@ -84,6 +84,26 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--continue</option></term>
+      <listitem>
+       <para>
+        When used with <option>--all</option>, report a database that cannot
+        be connected to as a warning and continue with the remaining
+        databases, instead of terminating with an error.  This option can
+        only be used together with <option>--all</option>.
+       </para>
+       <para>
+        This is chiefly of use with managed
+        <productname>PostgreSQL</productname> services, which commonly
+        include a database reserved for the provider that other users are not
+        permitted to connect to; without this option such a database prevents
+        <option>--all</option> from processing any of the databases that
+        follow it.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option><optional>-d</optional> <replaceable class="parameter">dbname</replaceable></option></term>
       <term><option><optional>--dbname=</optional><replaceable class="parameter">dbname</replaceable></option></term>
diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml
index a90e48ea86b..5d5fda88500 100644
--- a/doc/src/sgml/ref/reindexdb.sgml
+++ b/doc/src/sgml/ref/reindexdb.sgml
@@ -121,6 +121,26 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--continue</option></term>
+      <listitem>
+       <para>
+        When used with <option>--all</option>, report a database that cannot
+        be connected to as a warning and continue with the remaining
+        databases, instead of terminating with an error.  This option can
+        only be used together with <option>--all</option>.
+       </para>
+       <para>
+        This is chiefly of use with managed
+        <productname>PostgreSQL</productname> services, which commonly
+        include a database reserved for the provider that other users are not
+        permitted to connect to; without this option such a database prevents
+        <option>--all</option> from processing any of the databases that
+        follow it.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option><optional>-d</optional> <replaceable class="parameter">dbname</replaceable></option></term>
       <term><option><optional>--dbname=</optional><replaceable class="parameter">dbname</replaceable></option></term>
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 508c8dfa146..efe9a531d1a 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -144,6 +144,26 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--continue</option></term>
+      <listitem>
+       <para>
+        When used with <option>--all</option>, report a database that cannot
+        be connected to as a warning and continue with the remaining
+        databases, instead of terminating with an error.  This option can
+        only be used together with <option>--all</option>.
+       </para>
+       <para>
+        This is chiefly of use with managed
+        <productname>PostgreSQL</productname> services, which commonly
+        include a database reserved for the provider that other users are not
+        permitted to connect to; without this option such a database prevents
+        <option>--all</option> from processing any of the databases that
+        follow it.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option><optional>-d</optional> <replaceable class="parameter">dbname</replaceable></option></term>
       <term><option><optional>--dbname=</optional><replaceable class="parameter">dbname</replaceable></option></term>
diff --git a/src/bin/scripts/clusterdb.c b/src/bin/scripts/clusterdb.c
index 53bbb42c883..24a99baf9bf 100644
--- a/src/bin/scripts/clusterdb.c
+++ b/src/bin/scripts/clusterdb.c
@@ -18,11 +18,12 @@
 #include "fe_utils/simple_list.h"
 
 
-static void cluster_one_database(const ConnParams *cparams, const char *table,
-								 const char *progname, bool verbose, bool echo);
+static bool cluster_one_database(const ConnParams *cparams, const char *table,
+								 const char *progname, bool verbose, bool echo,
+								 bool conn_fail_ok);
 static void cluster_all_databases(ConnParams *cparams, SimpleStringList *tables,
 								  const char *progname, bool verbose, bool echo,
-								  bool quiet);
+								  bool quiet, bool conn_fail_ok);
 static void help(const char *progname);
 
 
@@ -42,6 +43,7 @@ main(int argc, char *argv[])
 		{"table", required_argument, NULL, 't'},
 		{"verbose", no_argument, NULL, 'v'},
 		{"maintenance-db", required_argument, NULL, 2},
+		{"continue", no_argument, NULL, 3},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -60,6 +62,7 @@ main(int argc, char *argv[])
 	bool		quiet = false;
 	bool		alldb = false;
 	bool		verbose = false;
+	bool		conn_fail_ok = false;
 	SimpleStringList tables = {NULL, NULL};
 
 	pg_logging_init(argv[0]);
@@ -108,6 +111,9 @@ main(int argc, char *argv[])
 			case 2:
 				maintenance_db = pg_strdup(optarg);
 				break;
+			case 3:
+				conn_fail_ok = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -142,6 +148,11 @@ main(int argc, char *argv[])
 
 	setup_cancel_handler(NULL);
 
+	/* --continue only governs the all-databases loop. */
+	if (conn_fail_ok && !alldb)
+		pg_fatal("cannot use the \"%s\" option without \"%s\"",
+				 "continue", "all");
+
 	if (alldb)
 	{
 		if (dbname)
@@ -150,7 +161,7 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		cluster_all_databases(&cparams, &tables,
-							  progname, verbose, echo, quiet);
+							  progname, verbose, echo, quiet, conn_fail_ok);
 	}
 	else
 	{
@@ -173,27 +184,40 @@ main(int argc, char *argv[])
 			for (cell = tables.head; cell; cell = cell->next)
 			{
 				cluster_one_database(&cparams, cell->val,
-									 progname, verbose, echo);
+									 progname, verbose, echo, false);
 			}
 		}
 		else
 			cluster_one_database(&cparams, NULL,
-								 progname, verbose, echo);
+								 progname, verbose, echo, false);
 	}
 
 	exit(0);
 }
 
 
-static void
+/*
+ * Cluster one database (or one table in it).  Returns false if the database
+ * had to be skipped because it could not be connected to, which is only
+ * possible when conn_fail_ok is set; see cluster_all_databases().
+ */
+static bool
 cluster_one_database(const ConnParams *cparams, const char *table,
-					 const char *progname, bool verbose, bool echo)
+					 const char *progname, bool verbose, bool echo,
+					 bool conn_fail_ok)
 {
 	PQExpBufferData sql;
 
 	PGconn	   *conn;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
+	conn = connectDatabase(cparams, progname, echo, conn_fail_ok, true);
+	if (PQstatus(conn) == CONNECTION_BAD)
+	{
+		pg_log_warning("skipping database \"%s\": %s",
+					   PQdb(conn), PQerrorMessage(conn));
+		PQfinish(conn);
+		return false;
+	}
 
 	initPQExpBuffer(&sql);
 
@@ -220,13 +244,15 @@ cluster_one_database(const ConnParams *cparams, const char *table,
 	}
 	PQfinish(conn);
 	termPQExpBuffer(&sql);
+
+	return true;
 }
 
 
 static void
 cluster_all_databases(ConnParams *cparams, SimpleStringList *tables,
 					  const char *progname, bool verbose, bool echo,
-					  bool quiet)
+					  bool quiet, bool conn_fail_ok)
 {
 	PGconn	   *conn;
 	PGresult   *result;
@@ -255,12 +281,17 @@ cluster_all_databases(ConnParams *cparams, SimpleStringList *tables,
 			SimpleStringListCell *cell;
 
 			for (cell = tables->head; cell; cell = cell->next)
-				cluster_one_database(cparams, cell->val,
-									 progname, verbose, echo);
+			{
+				/* one warning per skipped database, not per table */
+				if (!cluster_one_database(cparams, cell->val,
+										  progname, verbose, echo,
+										  conn_fail_ok))
+					break;
+			}
 		}
 		else
 			cluster_one_database(cparams, NULL,
-								 progname, verbose, echo);
+								 progname, verbose, echo, conn_fail_ok);
 	}
 
 	PQclear(result);
@@ -275,6 +306,8 @@ help(const char *progname)
 	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                 cluster all databases\n"));
+	printf(_("      --continue            with --all, skip databases that cannot be\n"
+			 "                            connected to instead of exiting\n"));
 	printf(_("  -d, --dbname=DBNAME       database to cluster\n"));
 	printf(_("  -e, --echo                show the commands being sent to the server\n"));
 	printf(_("  -q, --quiet               don't write any messages\n"));
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index d7fb16d3c85..e0e0f47657d 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -42,18 +42,20 @@ static void get_parallel_tabidx_list(PGconn *conn,
 									 SimpleStringList *index_list,
 									 SimpleOidList **table_list,
 									 bool echo);
-static void reindex_one_database(ConnParams *cparams, ReindexType type,
+static bool reindex_one_database(ConnParams *cparams, ReindexType type,
 								 SimpleStringList *user_list,
 								 const char *progname,
 								 bool echo, bool verbose, bool concurrently,
-								 int concurrentCons, const char *tablespace);
+								 int concurrentCons, const char *tablespace,
+								 bool conn_fail_ok);
 static void reindex_all_databases(ConnParams *cparams,
 								  const char *progname, bool echo,
 								  bool quiet, bool verbose, bool concurrently,
 								  int concurrentCons, const char *tablespace,
 								  bool syscatalog, SimpleStringList *schemas,
 								  SimpleStringList *tables,
-								  SimpleStringList *indexes);
+								  SimpleStringList *indexes,
+								  bool conn_fail_ok);
 static void gen_reindex_command(PGconn *conn, ReindexType type,
 								const char *name, bool echo, bool verbose,
 								bool concurrently, const char *tablespace,
@@ -86,6 +88,7 @@ main(int argc, char *argv[])
 		{"concurrently", no_argument, NULL, 1},
 		{"maintenance-db", required_argument, NULL, 2},
 		{"tablespace", required_argument, NULL, 3},
+		{"continue", no_argument, NULL, 4},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -107,6 +110,7 @@ main(int argc, char *argv[])
 	bool		quiet = false;
 	bool		verbose = false;
 	bool		concurrently = false;
+	bool		conn_fail_ok = false;
 	SimpleStringList indexes = {NULL, NULL};
 	SimpleStringList tables = {NULL, NULL};
 	SimpleStringList schemas = {NULL, NULL};
@@ -179,6 +183,9 @@ main(int argc, char *argv[])
 			case 3:
 				tablespace = pg_strdup(optarg);
 				break;
+			case 4:
+				conn_fail_ok = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -216,6 +223,11 @@ main(int argc, char *argv[])
 	if (concurrentCons > 1 && syscatalog)
 		pg_fatal("cannot use multiple jobs to reindex system catalogs");
 
+	/* --continue only governs the all-databases loop. */
+	if (conn_fail_ok && !alldb)
+		pg_fatal("cannot use the \"%s\" option without \"%s\"",
+				 "continue", "all");
+
 	if (alldb)
 	{
 		if (dbname)
@@ -225,7 +237,8 @@ main(int argc, char *argv[])
 
 		reindex_all_databases(&cparams, progname, echo, quiet, verbose,
 							  concurrently, concurrentCons, tablespace,
-							  syscatalog, &schemas, &tables, &indexes);
+							  syscatalog, &schemas, &tables, &indexes,
+							  conn_fail_ok);
 	}
 	else
 	{
@@ -244,22 +257,25 @@ main(int argc, char *argv[])
 		if (syscatalog)
 			reindex_one_database(&cparams, REINDEX_SYSTEM, NULL,
 								 progname, echo, verbose,
-								 concurrently, 1, tablespace);
+								 concurrently, 1, tablespace, false);
 
 		if (schemas.head != NULL)
 			reindex_one_database(&cparams, REINDEX_SCHEMA, &schemas,
 								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
+								 concurrently, concurrentCons, tablespace,
+								 false);
 
 		if (indexes.head != NULL)
 			reindex_one_database(&cparams, REINDEX_INDEX, &indexes,
 								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
+								 concurrently, concurrentCons, tablespace,
+								 false);
 
 		if (tables.head != NULL)
 			reindex_one_database(&cparams, REINDEX_TABLE, &tables,
 								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
+								 concurrently, concurrentCons, tablespace,
+								 false);
 
 		/*
 		 * reindex database only if neither index nor table nor schema nor
@@ -269,18 +285,24 @@ main(int argc, char *argv[])
 			tables.head == NULL && schemas.head == NULL)
 			reindex_one_database(&cparams, REINDEX_DATABASE, NULL,
 								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
+								 concurrently, concurrentCons, tablespace,
+								 false);
 	}
 
 	exit(0);
 }
 
-static void
+/*
+ * Reindex one database.  Returns false if the database had to be skipped
+ * because it could not be connected to, which is only possible when
+ * conn_fail_ok is set; see reindex_all_databases().
+ */
+static bool
 reindex_one_database(ConnParams *cparams, ReindexType type,
 					 SimpleStringList *user_list,
 					 const char *progname, bool echo,
 					 bool verbose, bool concurrently, int concurrentCons,
-					 const char *tablespace)
+					 const char *tablespace, bool conn_fail_ok)
 {
 	PGconn	   *conn;
 	SimpleStringListCell *cell;
@@ -294,7 +316,14 @@ reindex_one_database(ConnParams *cparams, ReindexType type,
 	int			items_count = 0;
 	ParallelSlot *free_slot = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
+	conn = connectDatabase(cparams, progname, echo, conn_fail_ok, true);
+	if (PQstatus(conn) == CONNECTION_BAD)
+	{
+		pg_log_warning("skipping database \"%s\": %s",
+					   PQdb(conn), PQerrorMessage(conn));
+		PQfinish(conn);
+		return false;
+	}
 
 	if (concurrently && PQserverVersion(conn) < 120000)
 	{
@@ -353,7 +382,7 @@ reindex_one_database(ConnParams *cparams, ReindexType type,
 				if (process_list == NULL)
 				{
 					PQfinish(conn);
-					return;
+					return true;
 				}
 				break;
 
@@ -371,7 +400,7 @@ reindex_one_database(ConnParams *cparams, ReindexType type,
 				if (tableoid_list == NULL)
 				{
 					PQfinish(conn);
-					return;
+					return true;
 				}
 
 				indices_tables_cell = tableoid_list->head;
@@ -489,6 +518,8 @@ finish:
 
 	if (failed)
 		exit(1);
+
+	return true;
 }
 
 /*
@@ -833,7 +864,7 @@ reindex_all_databases(ConnParams *cparams,
 					  bool concurrently, int concurrentCons,
 					  const char *tablespace, bool syscatalog,
 					  SimpleStringList *schemas, SimpleStringList *tables,
-					  SimpleStringList *indexes)
+					  SimpleStringList *indexes, bool conn_fail_ok)
 {
 	PGconn	   *conn;
 	PGresult   *result;
@@ -857,25 +888,35 @@ reindex_all_databases(ConnParams *cparams,
 
 		cparams->override_dbname = dbname;
 
-		if (syscatalog)
-			reindex_one_database(cparams, REINDEX_SYSTEM, NULL,
-								 progname, echo, verbose,
-								 concurrently, 1, tablespace);
-
-		if (schemas->head != NULL)
-			reindex_one_database(cparams, REINDEX_SCHEMA, schemas,
-								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
-
-		if (indexes->head != NULL)
-			reindex_one_database(cparams, REINDEX_INDEX, indexes,
-								 progname, echo, verbose,
-								 concurrently, 1, tablespace);
-
-		if (tables->head != NULL)
-			reindex_one_database(cparams, REINDEX_TABLE, tables,
-								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
+		/*
+		 * A skipped database is reported once, by whichever call below runs
+		 * first for it; the remaining work for that database is dropped.
+		 */
+		if (syscatalog &&
+			!reindex_one_database(cparams, REINDEX_SYSTEM, NULL,
+								  progname, echo, verbose,
+								  concurrently, 1, tablespace, conn_fail_ok))
+			continue;
+
+		if (schemas->head != NULL &&
+			!reindex_one_database(cparams, REINDEX_SCHEMA, schemas,
+								  progname, echo, verbose,
+								  concurrently, concurrentCons, tablespace,
+								  conn_fail_ok))
+			continue;
+
+		if (indexes->head != NULL &&
+			!reindex_one_database(cparams, REINDEX_INDEX, indexes,
+								  progname, echo, verbose,
+								  concurrently, 1, tablespace, conn_fail_ok))
+			continue;
+
+		if (tables->head != NULL &&
+			!reindex_one_database(cparams, REINDEX_TABLE, tables,
+								  progname, echo, verbose,
+								  concurrently, concurrentCons, tablespace,
+								  conn_fail_ok))
+			continue;
 
 		/*
 		 * reindex database only if neither index nor table nor schema nor
@@ -885,7 +926,8 @@ reindex_all_databases(ConnParams *cparams,
 			tables->head == NULL && schemas->head == NULL)
 			reindex_one_database(cparams, REINDEX_DATABASE, NULL,
 								 progname, echo, verbose,
-								 concurrently, concurrentCons, tablespace);
+								 concurrently, concurrentCons, tablespace,
+								 conn_fail_ok);
 	}
 
 	PQclear(result);
@@ -900,6 +942,8 @@ help(const char *progname)
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                    reindex all databases\n"));
 	printf(_("      --concurrently           reindex concurrently\n"));
+	printf(_("      --continue               with --all, skip databases that cannot be\n"
+			 "                               connected to instead of exiting\n"));
 	printf(_("  -d, --dbname=DBNAME          database to reindex\n"));
 	printf(_("  -e, --echo                   show the commands being sent to the server\n"));
 	printf(_("  -i, --index=INDEX            recreate specific index(es) only\n"));
diff --git a/src/bin/scripts/t/010_clusterdb.pl b/src/bin/scripts/t/010_clusterdb.pl
index 56e6881244b..cf5e90ef5ba 100644
--- a/src/bin/scripts/t/010_clusterdb.pl
+++ b/src/bin/scripts/t/010_clusterdb.pl
@@ -37,4 +37,31 @@ $node->issues_sql_like(
 $node->command_ok([qw(clusterdb --echo --verbose dbname=template1)],
 	'clusterdb with connection string');
 
+# A database that cannot be connected to terminates --all, unless --continue
+# is given.  This needs its own cluster, since connections can only be refused
+# to a role that is not a superuser.
+my $cnode = PostgreSQL::Test::Cluster->new('continue');
+$cnode->init(auth_extra => [ '--create-role' => 'regress_continue' ]);
+$cnode->start;
+$cnode->safe_psql('postgres',
+	'CREATE ROLE regress_continue LOGIN IN ROLE pg_maintain');
+$cnode->safe_psql('postgres', 'CREATE DATABASE regress_noconn');
+$cnode->safe_psql('postgres',
+	'REVOKE CONNECT ON DATABASE regress_noconn FROM PUBLIC');
+
+$cnode->command_fails_like(
+	[ 'clusterdb', '--all', '--username' => 'regress_continue' ],
+	qr/permission denied for database "regress_noconn"/,
+	'--all fails on a database that cannot be connected to');
+$cnode->command_fails_like(
+	[ 'clusterdb', '--continue', 'postgres' ],
+	qr/cannot use the "continue" option without "all"/,
+	'--continue requires --all');
+$cnode->command_checks_all(
+	[ 'clusterdb', '--all', '--continue', '--username' => 'regress_continue' ],
+	0,
+	[qr/clustering database "template1"/],
+	[qr/warning: skipping database "regress_noconn": /],
+	'--all --continue skips databases that cannot be connected to');
+
 done_testing();
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index ae7d3724464..81a5a307e27 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -323,4 +323,31 @@ $node->issues_sql_like(
 	qr/statement:\ REINDEX SCHEMA pg_catalog;/,
 	'specify both --system and --schema');
 
+# A database that cannot be connected to terminates --all, unless --continue
+# is given.  This needs its own cluster, since connections can only be refused
+# to a role that is not a superuser.
+my $cnode = PostgreSQL::Test::Cluster->new('continue');
+$cnode->init(auth_extra => [ '--create-role' => 'regress_continue' ]);
+$cnode->start;
+$cnode->safe_psql('postgres',
+	'CREATE ROLE regress_continue LOGIN IN ROLE pg_maintain');
+$cnode->safe_psql('postgres', 'CREATE DATABASE regress_noconn');
+$cnode->safe_psql('postgres',
+	'REVOKE CONNECT ON DATABASE regress_noconn FROM PUBLIC');
+
+$cnode->command_fails_like(
+	[ 'reindexdb', '--all', '--username' => 'regress_continue' ],
+	qr/permission denied for database "regress_noconn"/,
+	'--all fails on a database that cannot be connected to');
+$cnode->command_fails_like(
+	[ 'reindexdb', '--continue', 'postgres' ],
+	qr/cannot use the "continue" option without "all"/,
+	'--continue requires --all');
+$cnode->command_checks_all(
+	[ 'reindexdb', '--all', '--continue', '--username' => 'regress_continue' ],
+	0,
+	[qr/reindexing database "template1"/],
+	[qr/warning: skipping database "regress_noconn": /],
+	'--all --continue skips databases that cannot be connected to');
+
 done_testing();
diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 58e38971b3d..feeb4cb51c2 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -373,4 +373,32 @@ $node->issues_sql_unlike(
 	qr/statement:\ VACUUM/sx,
 	'--analyze-only does not run vacuum');
 
+# A database that cannot be connected to terminates --all, unless --continue
+# is given.  This needs its own cluster: connections can only be refused to a
+# non-superuser, and the tests above leave behind objects that such a role
+# cannot process (pg_maintain covers the relations, but not schema USAGE).
+my $cnode = PostgreSQL::Test::Cluster->new('continue');
+$cnode->init(auth_extra => [ '--create-role' => 'regress_continue' ]);
+$cnode->start;
+$cnode->safe_psql('postgres',
+	'CREATE ROLE regress_continue LOGIN IN ROLE pg_maintain');
+$cnode->safe_psql('postgres', 'CREATE DATABASE regress_noconn');
+$cnode->safe_psql('postgres',
+	'REVOKE CONNECT ON DATABASE regress_noconn FROM PUBLIC');
+
+$cnode->command_fails_like(
+	[ 'vacuumdb', '--all', '--username' => 'regress_continue' ],
+	qr/permission denied for database "regress_noconn"/,
+	'--all fails on a database that cannot be connected to');
+$cnode->command_fails_like(
+	[ 'vacuumdb', '--continue', 'postgres' ],
+	qr/cannot use the "continue" option without "all"/,
+	'--continue requires --all');
+$cnode->command_checks_all(
+	[ 'vacuumdb', '--all', '--continue', '--username' => 'regress_continue' ],
+	0,
+	[qr/vacuuming database "template1"/],
+	[qr/warning: skipping database "regress_noconn": /],
+	'--all --continue skips databases that cannot be connected to');
+
 done_testing();
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index f8158ca6a78..30fc584fdf2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -60,6 +60,7 @@ main(int argc, char *argv[])
 		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
 		{"dry-run", no_argument, NULL, 15},
+		{"continue", no_argument, NULL, 16},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -211,6 +212,9 @@ main(int argc, char *argv[])
 			case 15:
 				vacopts.dry_run = true;
 				break;
+			case 16:
+				vacopts.conn_fail_ok = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -307,6 +311,11 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option without \"%s\" or \"%s\"",
 				 "missing-stats-only", "analyze-only", "analyze-in-stages");
 
+	/* Prohibit --continue without --all: it only governs the database loop. */
+	if (vacopts.conn_fail_ok && !(vacopts.objfilter & OBJFILTER_ALL_DBS))
+		pg_fatal("cannot use the \"%s\" option without \"%s\"",
+				 "continue", "all");
+
 	if (vacopts.dry_run && !vacopts.quiet)
 	{
 		pg_log_info("executing in dry-run mode");
@@ -353,6 +362,8 @@ help(const char *progname)
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                       vacuum all databases\n"));
 	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
+	printf(_("      --continue                  with --all, skip databases that cannot be\n"
+			 "                                  connected to instead of exiting\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
 	printf(_("      --dry-run                   show the commands that would be sent to the server\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a5754c98..9ae2267e114 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -192,7 +192,24 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, vacopts->echo, false, true);
+	/*
+	 * With --continue, a database we cannot connect to is not a fatal
+	 * condition: managed environments routinely include a database reserved
+	 * for the hosting provider that ordinary users are refused entry to, and
+	 * treating that as fatal would abandon the run before reaching the
+	 * databases the user actually cares about.  Warn and let the caller move
+	 * on to the next one.  The option is rejected unless --all was given, so
+	 * this cannot quietly skip a database named on the command line.
+	 */
+	conn = connectDatabase(cparams, progname, vacopts->echo,
+						   vacopts->conn_fail_ok, true);
+	if (PQstatus(conn) == CONNECTION_BAD)
+	{
+		pg_log_warning("skipping database \"%s\": %s",
+					   PQdb(conn), PQerrorMessage(conn));
+		PQfinish(conn);
+		return EXIT_SUCCESS;
+	}
 
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 5a491db2526..1ac7645823c 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -54,6 +54,8 @@ typedef struct vacuumingOptions
 	bool		echo;
 	bool		quiet;
 	bool		dry_run;
+	bool		conn_fail_ok;	/* --continue: skip databases that cannot be
+								 * connected to (only valid with --all) */
 } vacuumingOptions;
 
 /* Valid values for vacuumingOptions->objfilter */
diff --git a/src/fe_utils/connect_utils.c b/src/fe_utils/connect_utils.c
index b117718513c..6fa81ce404a 100644
--- a/src/fe_utils/connect_utils.c
+++ b/src/fe_utils/connect_utils.c
@@ -27,6 +27,11 @@
  * given during previous calls to this routine.  (Callers should not pass
  * allow_password_reuse=true unless reconnecting to the same host+port+user
  * as before, else we might create password exposure hazards.)
+ *
+ * If fail_ok is true, a connection that could not be established is handed
+ * back to the caller with status CONNECTION_BAD, just as PQconnectdb would
+ * do; the caller must then report PQerrorMessage() as it sees fit and call
+ * PQfinish() on it.  Otherwise, a failure is reported and we exit.
  */
 PGconn *
 connectDatabase(const ConnParams *cparams, const char *progname,
@@ -109,10 +114,7 @@ connectDatabase(const ConnParams *cparams, const char *progname,
 	if (PQstatus(conn) == CONNECTION_BAD)
 	{
 		if (fail_ok)
-		{
-			PQfinish(conn);
-			return NULL;
-		}
+			return conn;		/* caller reports the error and closes it */
 		pg_fatal("%s", PQerrorMessage(conn));
 	}
 
@@ -143,8 +145,9 @@ connectMaintenanceDatabase(ConnParams *cparams,
 	/* Otherwise, try postgres first and then template1. */
 	cparams->dbname = "postgres";
 	conn = connectDatabase(cparams, progname, echo, true, false);
-	if (!conn)
+	if (PQstatus(conn) == CONNECTION_BAD)
 	{
+		PQfinish(conn);
 		cparams->dbname = "template1";
 		conn = connectDatabase(cparams, progname, echo, false, false);
 	}
-- 
2.55.0

