From aaaa35d648b77decc02a0027a387406c7cca97a5 Mon Sep 17 00:00:00 2001
From: ChangAo Chen <cca5507@qq.com>
Date: Tue, 23 Jun 2026 11:12:23 +0800
Subject: [PATCH v3 1/2] Handle concurrent drop when doing whole database
 vacuum.

When doing a whole database vacuum, we scan pg_class to construct
a list of vacuumable tables. For each vacuumable table, we call
vacuum_is_permitted_for_relation() to check permissions. If a
concurrent drop happens, the pg_class_aclcheck() might report an
error because of failing to search the syscache.

To fix it, we use pg_class_aclcheck_ext() to detect the concurrent
drop and report a warning instead.
---
 src/backend/commands/vacuum.c | 36 ++++++++++++++++++++++++++++-------
 1 file changed, 29 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..2217188d86e 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -715,12 +715,18 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
  * If not, issue a WARNING log message and return false to let the caller
  * decide what to do with this relation.  This routine is used to decide if a
  * relation can be processed for VACUUM or ANALYZE.
+ *
+ * Note that the relation might not be locked, so it can be dropped concurrently.
+ * This can happen when doing a whole database vacuum or analyze in
+ * get_all_vacuum_rels().  We issue a WARNING log message and return false in
+ * this case.
  */
 bool
 vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
 								 uint32 options)
 {
 	char	   *relname;
+	bool		is_missing = false;
 
 	Assert((options & (VACOPT_VACUUM | VACOPT_ANALYZE)) != 0);
 
@@ -729,20 +735,28 @@ vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
 	 * following are true:
 	 *   - the role owns the current database and the relation is not shared
 	 *   - the role has the MAINTAIN privilege on the relation
+	 *
+	 * Note: use pg_class_aclcheck_ext() to detect a concurrent drop.
 	 *----------
 	 */
 	if ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()) &&
 		 !reltuple->relisshared) ||
-		pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK)
+		pg_class_aclcheck_ext(relid, GetUserId(), ACL_MAINTAIN, &is_missing) == ACLCHECK_OK)
 		return true;
 
 	relname = NameStr(reltuple->relname);
 
 	if ((options & VACOPT_VACUUM) != 0)
 	{
-		ereport(WARNING,
-				(errmsg("permission denied to vacuum \"%s\", skipping it",
-						relname)));
+		if (is_missing)
+			ereport(WARNING,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+							relname)));
+		else
+			ereport(WARNING,
+					(errmsg("permission denied to vacuum \"%s\", skipping it",
+							relname)));
 
 		/*
 		 * For VACUUM ANALYZE, both logs could show up, but just generate
@@ -753,9 +767,17 @@ vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
 	}
 
 	if ((options & VACOPT_ANALYZE) != 0)
-		ereport(WARNING,
-				(errmsg("permission denied to analyze \"%s\", skipping it",
-						relname)));
+	{
+		if (is_missing)
+			ereport(WARNING,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+							relname)));
+		else
+			ereport(WARNING,
+					(errmsg("permission denied to analyze \"%s\", skipping it",
+							relname)));
+	}
 
 	return false;
 }
-- 
2.34.1

