From fa969ac551d6d66174a6bae038e3f119f2e6476e Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Thu, 22 Oct 2026 14:18:34 +0800
Subject: [PATCH v1] Reject incompatible options specified with VACUUM
 ONLY_DATABASE_STATS

The existing check examines only option bits, so it misses PARALLEL,
INDEX_CLEANUP, TRUNCATE, and BUFFER_USAGE_LIMIT. These options are
accepted even though they have no effect with ONLY_DATABASE_STATS.

Track incompatible options while parsing and reject any explicitly
specified option other than VERBOSE, regardless of its value. This also
rejects explicitly disabled options and explicit PROCESS_MAIN and
PROCESS_TOAST options, while allowing their implicit defaults.

Update the documentation and add regression coverage.

Author: Chao Li <lic@highgo.com>
---
 doc/src/sgml/ref/vacuum.sgml         |  2 +-
 src/backend/commands/vacuum.c        | 43 +++++++++++++++++++++++-----
 src/test/regress/expected/vacuum.out | 32 +++++++++++++++++++++
 src/test/regress/sql/vacuum.sql      | 17 +++++++++++
 4 files changed, 86 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 3c85846df26..0df6082f27b 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -303,7 +303,7 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
       update the database-wide statistics about oldest unfrozen XIDs.
       When this option is specified,
       the <replaceable class="parameter">table_and_columns</replaceable>
-      list must be empty, and no other option may be enabled
+      list must be empty, and no other option may be specified
       except <literal>VERBOSE</literal>.
      </para>
     </listitem>
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index c7595d59bb2..420997e135d 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -176,6 +176,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	int			ring_size;
 	bool		skip_database_stats = false;
 	bool		only_database_stats = false;
+	bool		only_database_stats_conflict = false;
 	MemoryContext vac_context;
 	ListCell   *lc;
 
@@ -196,7 +197,10 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	 */
 	ring_size = -1;
 
-	/* Parse options list */
+	/*
+	 * Parse options list, remembering any specified option that conflicts
+	 * with ONLY_DATABASE_STATS.  Only VERBOSE is allowed alongside it.
+	 */
 	foreach(lc, vacstmt->options)
 	{
 		DefElem    *opt = (DefElem *) lfirst(lc);
@@ -205,7 +209,10 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 		if (strcmp(opt->defname, "verbose") == 0)
 			verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "skip_locked") == 0)
+		{
 			skip_locked = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "buffer_usage_limit") == 0)
 		{
 			const char *hintmsg;
@@ -231,6 +238,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			}
 
 			ring_size = result;
+			only_database_stats_conflict = true;
 		}
 		else if (!vacstmt->is_vacuumcmd)
 			ereport(ERROR,
@@ -241,13 +249,25 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 
 		/* Parse options available on VACUUM */
 		else if (strcmp(opt->defname, "analyze") == 0)
+		{
 			analyze = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "freeze") == 0)
+		{
 			freeze = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "full") == 0)
+		{
 			full = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "disable_page_skipping") == 0)
+		{
 			disable_page_skipping = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "index_cleanup") == 0)
 		{
 			/* Interpret no string as the default, which is 'auto' */
@@ -263,13 +283,23 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				else
 					params.index_cleanup = get_vacoptval_from_boolean(opt);
 			}
+			only_database_stats_conflict = true;
 		}
 		else if (strcmp(opt->defname, "process_main") == 0)
+		{
 			process_main = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "process_toast") == 0)
+		{
 			process_toast = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "truncate") == 0)
+		{
 			params.truncate = get_vacoptval_from_boolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "parallel") == 0)
 		{
 			int			nworkers = defGetInt32(opt);
@@ -290,9 +320,13 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				params.nworkers = -1;
 			else
 				params.nworkers = nworkers;
+			only_database_stats_conflict = true;
 		}
 		else if (strcmp(opt->defname, "skip_database_stats") == 0)
+		{
 			skip_database_stats = defGetBoolean(opt);
+			only_database_stats_conflict = true;
+		}
 		else if (strcmp(opt->defname, "only_database_stats") == 0)
 			only_database_stats = defGetBoolean(opt);
 		else
@@ -378,12 +412,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("ONLY_DATABASE_STATS cannot be specified with a list of tables")));
-		/* don't require people to turn off PROCESS_TOAST/MAIN explicitly */
-		if (params.options & ~(VACOPT_VACUUM |
-							   VACOPT_VERBOSE |
-							   VACOPT_PROCESS_MAIN |
-							   VACOPT_PROCESS_TOAST |
-							   VACOPT_ONLY_DATABASE_STATS))
+		if (only_database_stats_conflict)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("ONLY_DATABASE_STATS cannot be specified with other VACUUM options")));
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..e3f544d428a 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -551,6 +551,38 @@ VACUUM (SKIP_DATABASE_STATS) vactst;
 VACUUM (ONLY_DATABASE_STATS);
 VACUUM (ONLY_DATABASE_STATS) vactst;  -- error
 ERROR:  ONLY_DATABASE_STATS cannot be specified with a list of tables
+VACUUM (ONLY_DATABASE_STATS, FREEZE);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, ANALYZE);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, FULL);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, SKIP_LOCKED);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, DISABLE_PAGE_SKIPPING);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, SKIP_DATABASE_STATS);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, PARALLEL 1);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, INDEX_CLEANUP ON);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, INDEX_CLEANUP AUTO);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, INDEX_CLEANUP);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, TRUNCATE ON);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, BUFFER_USAGE_LIMIT '1MB');  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, PROCESS_MAIN ON);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (ONLY_DATABASE_STATS, PROCESS_TOAST ON);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+VACUUM (PARALLEL 1, ONLY_DATABASE_STATS);  -- error
+ERROR:  ONLY_DATABASE_STATS cannot be specified with other VACUUM options
+-- VERBOSE is allowed.
+VACUUM (ONLY_DATABASE_STATS, VERBOSE);
 DROP VIEW vac_option_tab_counts;
 DROP TABLE vac_option_tab;
 DROP TABLE vaccluster;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..8f1fc17d299 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -411,6 +411,23 @@ VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
 VACUUM (ONLY_DATABASE_STATS);
 VACUUM (ONLY_DATABASE_STATS) vactst;  -- error
+VACUUM (ONLY_DATABASE_STATS, FREEZE);  -- error
+VACUUM (ONLY_DATABASE_STATS, ANALYZE);  -- error
+VACUUM (ONLY_DATABASE_STATS, FULL);  -- error
+VACUUM (ONLY_DATABASE_STATS, SKIP_LOCKED);  -- error
+VACUUM (ONLY_DATABASE_STATS, DISABLE_PAGE_SKIPPING);  -- error
+VACUUM (ONLY_DATABASE_STATS, SKIP_DATABASE_STATS);  -- error
+VACUUM (ONLY_DATABASE_STATS, PARALLEL 1);  -- error
+VACUUM (ONLY_DATABASE_STATS, INDEX_CLEANUP ON);  -- error
+VACUUM (ONLY_DATABASE_STATS, INDEX_CLEANUP AUTO);  -- error
+VACUUM (ONLY_DATABASE_STATS, INDEX_CLEANUP);  -- error
+VACUUM (ONLY_DATABASE_STATS, TRUNCATE ON);  -- error
+VACUUM (ONLY_DATABASE_STATS, BUFFER_USAGE_LIMIT '1MB');  -- error
+VACUUM (ONLY_DATABASE_STATS, PROCESS_MAIN ON);  -- error
+VACUUM (ONLY_DATABASE_STATS, PROCESS_TOAST ON);  -- error
+VACUUM (PARALLEL 1, ONLY_DATABASE_STATS);  -- error
+-- VERBOSE is allowed.
+VACUUM (ONLY_DATABASE_STATS, VERBOSE);
 
 DROP VIEW vac_option_tab_counts;
 DROP TABLE vac_option_tab;
-- 
2.50.1 (Apple Git-155)

