From 31911d8297546e1bacd487d086414df6ca9a399a Mon Sep 17 00:00:00 2001 From: Mehmet YILMAZ Date: Fri, 5 Dec 2025 16:50:42 +0300 Subject: [PATCH] =?UTF-8?q?PG18=20=E2=80=93=20Respect=20VACUUM/ANALYZE=20O?= =?UTF-8?q?NLY=20semantics=20for=20Citus=20tables=20(#8365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #8364 PostgreSQL 18 changes VACUUM/ANALYZE to recurse into inheritance children by default, and introduces `ONLY` to limit processing to the parent. Upstream change: [https://github.com/postgres/postgres/commit/62ddf7ee9](https://github.com/postgres/postgres/commit/62ddf7ee9) For Citus tables, we should treat shard placements as “children” and avoid propagating `VACUUM/ANALYZE` to shards when the user explicitly asks for `ONLY`. This PR adjusts the Citus VACUUM handling to align with PG18 semantics, and adds regression coverage on both regular distributed tables and partitioned distributed tables. --- ### Behavior changes * Introduce a per-relation helper struct: ```c typedef struct CitusVacuumRelation { VacuumRelation *vacuumRelation; Oid relationId; } CitusVacuumRelation; ``` This lets us keep both: * the resolved relation OID (for `IsCitusTable`, task building), and * the original `VacuumRelation` node (for column list and ONLY/inh flag). * Replace the old `VacuumRelationIdList` / `ExtractVacuumTargetRels` flow with: ```c static List *VacuumRelationList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams); ``` `VacuumRelationList` now: * Iterates over `vacuumStmt->rels`. * Resolves `relid` via `RangeVarGetRelidExtended` when `relation` is present. * Falls back to locking `VacuumRelation->oid` when only an OID is available. * Respects `VACOPT_FULL` for lock mode and `VACOPT_SKIP_LOCKED` for locking behavior. * Builds a `List *` of `CitusVacuumRelation` entries. * Update: ```c IsDistributedVacuumStmt(List *vacuumRelationList); ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *vacuumRelationList, CitusVacuumParams vacuumParams); ``` to operate on `CitusVacuumRelation` instead of bare OIDs. * Implement `ONLY` semantics in `ExecuteVacuumOnDistributedTables`: ```c RangeVar *relation = vacuumRelation->relation; if (relation != NULL && !relation->inh) { /* ONLY specified, so don't recurse to shard placements */ continue; } ``` Effect: * `VACUUM / ANALYZE` (no `ONLY`) on a Citus table: behavior unchanged, Citus creates tasks and propagates to shard placements. * `VACUUM ONLY ` / `ANALYZE ONLY `: * Core still processes the coordinator relation as usual. * Citus **skips** building tasks for shard placements, so we do not recurse into distributed children. * The code compiles and behaves as before on pre-PG18; the new behavior becomes observable only when the core planner starts setting `inh = false` for `ONLY` (PG18). * Unqualified `VACUUM` / `ANALYZE` (no rels) is unchanged and still handled via `ExecuteUnqualifiedVacuumTasks`. * Remove now-redundant helpers: * `VacuumColumnList` * `ExtractVacuumTargetRels` Column lists are now taken directly from `vacuumRelation->va_cols` via `CitusVacuumRelation`. --- ### Testing Extend `src/test/regress/sql/pg18.sql` and `expected/pg18.out` with two PG18-only blocks that verify we do not recurse into shard placements when `ONLY` is used: 1. **Simple distributed table (`pg18_vacuum_part`)** * Create and distribute a regular table: ```sql CREATE SCHEMA pg18_vacuum_part; SET search_path TO pg18_vacuum_part; CREATE TABLE vac_analyze_only (a int); SELECT create_distributed_table('vac_analyze_only', 'a'); INSERT INTO vac_analyze_only VALUES (1), (2), (3); ``` * On the coordinator: * Run `ANALYZE vac_analyze_only;` and later `ANALYZE ONLY vac_analyze_only;`. * Run `VACUUM vac_analyze_only;` and later `VACUUM ONLY vac_analyze_only;`. * On `worker_1`: * Capture `coalesce(max(last_analyze), 'epoch')` from `pg_stat_user_tables` for `vac_analyze_only_%` into `:analyze_before_only`, then assert: ```sql SELECT max(last_analyze) = :'analyze_before_only'::timestamptz AS analyze_only_skipped; ``` * Capture `coalesce(max(last_vacuum), 'epoch')` into `:vacuum_before_only`, then assert: ```sql SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz AS vacuum_only_skipped; ``` Both checks return `t`, confirming `ONLY` does not change `last_analyze` / `last_vacuum` on shard tables. 2. **Partitioned distributed table (`pg18_vacuum_part_dist`)** * Create a partitioned table whose parent is distributed: ```sql CREATE SCHEMA pg18_vacuum_part_dist; SET search_path TO pg18_vacuum_part_dist; SET citus.shard_count = 2; SET citus.shard_replication_factor = 1; CREATE TABLE part_dist (id int, v int) PARTITION BY RANGE (id); CREATE TABLE part_dist_1 PARTITION OF part_dist FOR VALUES FROM (1) TO (100); CREATE TABLE part_dist_2 PARTITION OF part_dist FOR VALUES FROM (100) TO (200); SELECT create_distributed_table('part_dist', 'id'); INSERT INTO part_dist SELECT g, g FROM generate_series(1, 199) g; ``` * On the coordinator: * Run `ANALYZE part_dist;` then `ANALYZE ONLY part_dist;`. * Run `VACUUM part_dist;` then `VACUUM ONLY part_dist;` (PG18 emits the expected warning: `VACUUM ONLY of partitioned table "part_dist" has no effect`). * On `worker_1`: * Capture `coalesce(max(last_analyze), 'epoch')` for `part_dist_%` into `:analyze_before_only`, then assert: ```sql SELECT max(last_analyze) = :'analyze_before_only'::timestamptz AS analyze_only_partitioned_skipped; ``` * Capture `coalesce(max(last_vacuum), 'epoch')` into `:vacuum_before_only`, then assert: ```sql SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz AS vacuum_only_partitioned_skipped; ``` Both checks return `t`, confirming that even for a partitioned distributed parent, `VACUUM/ANALYZE ONLY` does not recurse into shard placements, and Citus behavior matches PG18’s “ONLY = parent only” semantics. --- src/backend/distributed/commands/vacuum.c | 128 ++++++++--------- src/test/regress/expected/pg18.out | 150 ++++++++++++++++++++ src/test/regress/sql/pg18.sql | 160 ++++++++++++++++++++++ 3 files changed, 374 insertions(+), 64 deletions(-) diff --git a/src/backend/distributed/commands/vacuum.c b/src/backend/distributed/commands/vacuum.c index 3bdabe467..08064b4b0 100644 --- a/src/backend/distributed/commands/vacuum.c +++ b/src/backend/distributed/commands/vacuum.c @@ -48,21 +48,27 @@ typedef struct CitusVacuumParams #endif } CitusVacuumParams; +/* + * Information we track per VACUUM/ANALYZE target relation. + */ +typedef struct CitusVacuumRelation +{ + VacuumRelation *vacuumRelation; + Oid relationId; +} CitusVacuumRelation; + /* Local functions forward declarations for processing distributed table commands */ -static bool IsDistributedVacuumStmt(List *vacuumRelationIdList); +static bool IsDistributedVacuumStmt(List *vacuumRelationList); static List * VacuumTaskList(Oid relationId, CitusVacuumParams vacuumParams, List *vacuumColumnList); static char * DeparseVacuumStmtPrefix(CitusVacuumParams vacuumParams); static char * DeparseVacuumColumnNames(List *columnNameList); -static List * VacuumColumnList(VacuumStmt *vacuumStmt, int relationIndex); -static List * ExtractVacuumTargetRels(VacuumStmt *vacuumStmt); -static void ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationIdList, +static void ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationList, CitusVacuumParams vacuumParams); static void ExecuteUnqualifiedVacuumTasks(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams); static CitusVacuumParams VacuumStmtParams(VacuumStmt *vacstmt); -static List * VacuumRelationIdList(VacuumStmt *vacuumStmt, CitusVacuumParams - vacuumParams); +static List * VacuumRelationList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams); /* * PostprocessVacuumStmt processes vacuum statements that may need propagation to @@ -97,7 +103,7 @@ PostprocessVacuumStmt(Node *node, const char *vacuumCommand) * when no table is specified propagate the command as it is; * otherwise, only propagate when there is at least 1 citus table */ - List *relationIdList = VacuumRelationIdList(vacuumStmt, vacuumParams); + List *vacuumRelationList = VacuumRelationList(vacuumStmt, vacuumParams); if (list_length(vacuumStmt->rels) == 0) { @@ -105,11 +111,11 @@ PostprocessVacuumStmt(Node *node, const char *vacuumCommand) ExecuteUnqualifiedVacuumTasks(vacuumStmt, vacuumParams); } - else if (IsDistributedVacuumStmt(relationIdList)) + else if (IsDistributedVacuumStmt(vacuumRelationList)) { /* there is at least 1 citus table specified */ - ExecuteVacuumOnDistributedTables(vacuumStmt, relationIdList, + ExecuteVacuumOnDistributedTables(vacuumStmt, vacuumRelationList, vacuumParams); } @@ -120,39 +126,58 @@ PostprocessVacuumStmt(Node *node, const char *vacuumCommand) /* - * VacuumRelationIdList returns the oid of the relations in the given vacuum statement. + * VacuumRelationList returns the list of relations in the given vacuum statement, + * along with their resolved Oids (if they can be locked). */ static List * -VacuumRelationIdList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams) +VacuumRelationList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams) { LOCKMODE lockMode = (vacuumParams.options & VACOPT_FULL) ? AccessExclusiveLock : ShareUpdateExclusiveLock; bool skipLocked = (vacuumParams.options & VACOPT_SKIP_LOCKED); - List *vacuumRelationList = ExtractVacuumTargetRels(vacuumStmt); + List *relationList = NIL; - List *relationIdList = NIL; - - RangeVar *vacuumRelation = NULL; - foreach_declared_ptr(vacuumRelation, vacuumRelationList) + VacuumRelation *vacuumRelation = NULL; + foreach_declared_ptr(vacuumRelation, vacuumStmt->rels) { + Oid relationId = InvalidOid; + /* * If skip_locked option is enabled, we are skipping that relation - * if the lock for it is currently not available; else, we get the lock. + * if the lock for it is currently not available; otherwise, we get the lock. */ - Oid relationId = RangeVarGetRelidExtended(vacuumRelation, + if (vacuumRelation->relation) + { + relationId = RangeVarGetRelidExtended(vacuumRelation->relation, lockMode, skipLocked ? RVR_SKIP_LOCKED : 0, NULL, NULL); + } + else if (OidIsValid(vacuumRelation->oid)) + { + /* fall back to the Oid directly when provided */ + if (!skipLocked || ConditionalLockRelationOid(vacuumRelation->oid, lockMode)) + { + if (!skipLocked) + { + LockRelationOid(vacuumRelation->oid, lockMode); + } + relationId = vacuumRelation->oid; + } + } if (OidIsValid(relationId)) { - relationIdList = lappend_oid(relationIdList, relationId); + CitusVacuumRelation *relation = palloc(sizeof(CitusVacuumRelation)); + relation->vacuumRelation = vacuumRelation; + relation->relationId = relationId; + relationList = lappend(relationList, relation); } } - return relationIdList; + return relationList; } @@ -161,12 +186,13 @@ VacuumRelationIdList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams) * otherwise, it returns false. */ static bool -IsDistributedVacuumStmt(List *vacuumRelationIdList) +IsDistributedVacuumStmt(List *vacuumRelationList) { - Oid relationId = InvalidOid; - foreach_declared_oid(relationId, vacuumRelationIdList) + CitusVacuumRelation *vacuumRelation = NULL; + foreach_declared_ptr(vacuumRelation, vacuumRelationList) { - if (OidIsValid(relationId) && IsCitusTable(relationId)) + if (OidIsValid(vacuumRelation->relationId) && + IsCitusTable(vacuumRelation->relationId)) { return true; } @@ -181,24 +207,31 @@ IsDistributedVacuumStmt(List *vacuumRelationIdList) * if they are citus tables. */ static void -ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationIdList, +ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationList, CitusVacuumParams vacuumParams) { - int relationIndex = 0; - - Oid relationId = InvalidOid; - foreach_declared_oid(relationId, relationIdList) + CitusVacuumRelation *vacuumRelationEntry = NULL; + foreach_declared_ptr(vacuumRelationEntry, relationList) { + Oid relationId = vacuumRelationEntry->relationId; + VacuumRelation *vacuumRelation = vacuumRelationEntry->vacuumRelation; + + RangeVar *relation = vacuumRelation->relation; + if (relation != NULL && !relation->inh) + { + /* ONLY specified, so don't recurse to shard placements */ + continue; + } + if (IsCitusTable(relationId)) { - List *vacuumColumnList = VacuumColumnList(vacuumStmt, relationIndex); + List *vacuumColumnList = vacuumRelation->va_cols; List *taskList = VacuumTaskList(relationId, vacuumParams, vacuumColumnList); /* local execution is not implemented for VACUUM commands */ bool localExecutionSupported = false; ExecuteUtilityTaskList(taskList, localExecutionSupported); } - relationIndex++; } } @@ -484,39 +517,6 @@ DeparseVacuumColumnNames(List *columnNameList) } -/* - * VacuumColumnList returns list of columns from relation - * in the vacuum statement at specified relationIndex. - */ -static List * -VacuumColumnList(VacuumStmt *vacuumStmt, int relationIndex) -{ - VacuumRelation *vacuumRelation = (VacuumRelation *) list_nth(vacuumStmt->rels, - relationIndex); - - return vacuumRelation->va_cols; -} - - -/* - * ExtractVacuumTargetRels returns list of target - * relations from vacuum statement. - */ -static List * -ExtractVacuumTargetRels(VacuumStmt *vacuumStmt) -{ - List *vacuumList = NIL; - - VacuumRelation *vacuumRelation = NULL; - foreach_declared_ptr(vacuumRelation, vacuumStmt->rels) - { - vacuumList = lappend(vacuumList, vacuumRelation->relation); - } - - return vacuumList; -} - - /* * VacuumStmtParams returns a CitusVacuumParams based on the supplied VacuumStmt. */ diff --git a/src/test/regress/expected/pg18.out b/src/test/regress/expected/pg18.out index 191a3f1e8..0e715b43c 100644 --- a/src/test/regress/expected/pg18.out +++ b/src/test/regress/expected/pg18.out @@ -1802,6 +1802,156 @@ NOTICE: renaming the new table to pg18_nn.generated_stored_ref (4 rows) ROLLBACK; +-- PG18 Feature: VACUUM/ANALYZE support ONLY to limit processing to the parent. +-- For Citus, ensure ONLY does not trigger shard propagation. +-- PG18 commit: https://github.com/postgres/postgres/commit/62ddf7ee9 +CREATE SCHEMA pg18_vacuum_part; +SET search_path TO pg18_vacuum_part; +CREATE TABLE vac_analyze_only (a int); +SELECT create_distributed_table('vac_analyze_only', 'a'); + create_distributed_table +--------------------------------------------------------------------- + +(1 row) + +INSERT INTO vac_analyze_only VALUES (1), (2), (3); +-- ANALYZE (no ONLY) should recurse into shard placements +ANALYZE vac_analyze_only; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; +SELECT coalesce(max(last_analyze), 'epoch'::timestamptz) AS analyze_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%' +\gset +\c - - - :master_port +SET search_path TO pg18_vacuum_part; +-- ANALYZE ONLY should not recurse into shard placements +ANALYZE ONLY vac_analyze_only; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; +SELECT max(last_analyze) = :'analyze_before_only'::timestamptz + AS analyze_only_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%'; + analyze_only_skipped +--------------------------------------------------------------------- + t +(1 row) + +\c - - - :master_port +SET search_path TO pg18_vacuum_part; +-- VACUUM (no ONLY) should recurse into shard placements +VACUUM vac_analyze_only; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; +SELECT coalesce(max(last_vacuum), 'epoch'::timestamptz) AS vacuum_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%' +\gset +\c - - - :master_port +SET search_path TO pg18_vacuum_part; +-- VACUUM ONLY should not recurse into shard placements +VACUUM ONLY vac_analyze_only; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; +SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz + AS vacuum_only_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%'; + vacuum_only_skipped +--------------------------------------------------------------------- + t +(1 row) + +\c - - - :master_port +SET search_path TO pg18_vacuum_part; +DROP SCHEMA pg18_vacuum_part CASCADE; +NOTICE: drop cascades to table vac_analyze_only +SET search_path TO pg18_nn; +-- END PG18 Feature: VACUUM/ANALYZE support ONLY to limit processing to the parent +-- PG18 Feature: VACUUM/ANALYZE ONLY on a partitioned distributed table +-- Ensure Citus does not recurse into shard placements when ONLY is used +-- on the partitioned parent. +-- PG18 commit: https://github.com/postgres/postgres/commit/62ddf7ee9 +CREATE SCHEMA pg18_vacuum_part_dist; +SET search_path TO pg18_vacuum_part_dist; +SET citus.shard_count = 2; +SET citus.shard_replication_factor = 1; +CREATE TABLE part_dist (id int, v int) PARTITION BY RANGE (id); +CREATE TABLE part_dist_1 PARTITION OF part_dist FOR VALUES FROM (1) TO (100); +CREATE TABLE part_dist_2 PARTITION OF part_dist FOR VALUES FROM (100) TO (200); +SELECT create_distributed_table('part_dist', 'id'); + create_distributed_table +--------------------------------------------------------------------- + +(1 row) + +INSERT INTO part_dist +SELECT g, g FROM generate_series(1, 199) g; +-- ANALYZE (no ONLY) should recurse into partitions and shard placements +ANALYZE part_dist; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; +SELECT coalesce(max(last_analyze), 'epoch'::timestamptz) AS analyze_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%' +\gset +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; +-- ANALYZE ONLY should not recurse into shard placements +ANALYZE ONLY part_dist; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; +SELECT max(last_analyze) = :'analyze_before_only'::timestamptz + AS analyze_only_partitioned_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%'; + analyze_only_partitioned_skipped +--------------------------------------------------------------------- + t +(1 row) + +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; +-- VACUUM (no ONLY) should recurse into partitions and shard placements +VACUUM part_dist; +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; +SELECT coalesce(max(last_vacuum), 'epoch'::timestamptz) AS vacuum_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%' +\gset +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; +-- VACUUM ONLY parent: core warns and does no work; Citus must not +-- propagate to shard placements. +VACUUM ONLY part_dist; +WARNING: VACUUM ONLY of partitioned table "part_dist" has no effect +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; +SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz + AS vacuum_only_partitioned_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%'; + vacuum_only_partitioned_skipped +--------------------------------------------------------------------- + t +(1 row) + +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; +DROP SCHEMA pg18_vacuum_part_dist CASCADE; +NOTICE: drop cascades to table part_dist +SET search_path TO pg18_nn; +-- END PG18 Feature: VACUUM/ANALYZE ONLY on partitioned distributed table -- cleanup with minimum verbosity SET client_min_messages TO ERROR; RESET search_path; diff --git a/src/test/regress/sql/pg18.sql b/src/test/regress/sql/pg18.sql index 4c7112f9a..02889b912 100644 --- a/src/test/regress/sql/pg18.sql +++ b/src/test/regress/sql/pg18.sql @@ -1120,6 +1120,166 @@ BEGIN; SELECT * FROM generated_stored_ref; ROLLBACK; +-- PG18 Feature: VACUUM/ANALYZE support ONLY to limit processing to the parent. +-- For Citus, ensure ONLY does not trigger shard propagation. +-- PG18 commit: https://github.com/postgres/postgres/commit/62ddf7ee9 +CREATE SCHEMA pg18_vacuum_part; +SET search_path TO pg18_vacuum_part; + +CREATE TABLE vac_analyze_only (a int); +SELECT create_distributed_table('vac_analyze_only', 'a'); +INSERT INTO vac_analyze_only VALUES (1), (2), (3); + +-- ANALYZE (no ONLY) should recurse into shard placements +ANALYZE vac_analyze_only; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; + +SELECT coalesce(max(last_analyze), 'epoch'::timestamptz) AS analyze_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%' +\gset + +\c - - - :master_port +SET search_path TO pg18_vacuum_part; + +-- ANALYZE ONLY should not recurse into shard placements +ANALYZE ONLY vac_analyze_only; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; + +SELECT max(last_analyze) = :'analyze_before_only'::timestamptz + AS analyze_only_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%'; + +\c - - - :master_port +SET search_path TO pg18_vacuum_part; + +-- VACUUM (no ONLY) should recurse into shard placements +VACUUM vac_analyze_only; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; + +SELECT coalesce(max(last_vacuum), 'epoch'::timestamptz) AS vacuum_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%' +\gset + +\c - - - :master_port +SET search_path TO pg18_vacuum_part; + +-- VACUUM ONLY should not recurse into shard placements +VACUUM ONLY vac_analyze_only; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part; + +SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz + AS vacuum_only_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part' + AND relname LIKE 'vac_analyze_only_%'; + +\c - - - :master_port +SET search_path TO pg18_vacuum_part; + +DROP SCHEMA pg18_vacuum_part CASCADE; +SET search_path TO pg18_nn; + +-- END PG18 Feature: VACUUM/ANALYZE support ONLY to limit processing to the parent + +-- PG18 Feature: VACUUM/ANALYZE ONLY on a partitioned distributed table +-- Ensure Citus does not recurse into shard placements when ONLY is used +-- on the partitioned parent. +-- PG18 commit: https://github.com/postgres/postgres/commit/62ddf7ee9 +CREATE SCHEMA pg18_vacuum_part_dist; +SET search_path TO pg18_vacuum_part_dist; + +SET citus.shard_count = 2; +SET citus.shard_replication_factor = 1; + +CREATE TABLE part_dist (id int, v int) PARTITION BY RANGE (id); +CREATE TABLE part_dist_1 PARTITION OF part_dist FOR VALUES FROM (1) TO (100); +CREATE TABLE part_dist_2 PARTITION OF part_dist FOR VALUES FROM (100) TO (200); + +SELECT create_distributed_table('part_dist', 'id'); + +INSERT INTO part_dist +SELECT g, g FROM generate_series(1, 199) g; + +-- ANALYZE (no ONLY) should recurse into partitions and shard placements +ANALYZE part_dist; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; + +SELECT coalesce(max(last_analyze), 'epoch'::timestamptz) AS analyze_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%' +\gset + +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; + +-- ANALYZE ONLY should not recurse into shard placements +ANALYZE ONLY part_dist; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; + +SELECT max(last_analyze) = :'analyze_before_only'::timestamptz + AS analyze_only_partitioned_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%'; + +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; + +-- VACUUM (no ONLY) should recurse into partitions and shard placements +VACUUM part_dist; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; + +SELECT coalesce(max(last_vacuum), 'epoch'::timestamptz) AS vacuum_before_only +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%' +\gset + +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; + +-- VACUUM ONLY parent: core warns and does no work; Citus must not +-- propagate to shard placements. +VACUUM ONLY part_dist; + +\c - - - :worker_1_port +SET search_path TO pg18_vacuum_part_dist; + +SELECT max(last_vacuum) = :'vacuum_before_only'::timestamptz + AS vacuum_only_partitioned_skipped +FROM pg_stat_user_tables +WHERE schemaname = 'pg18_vacuum_part_dist' + AND relname LIKE 'part_dist_%'; + +\c - - - :master_port +SET search_path TO pg18_vacuum_part_dist; + +DROP SCHEMA pg18_vacuum_part_dist CASCADE; +SET search_path TO pg18_nn; + +-- END PG18 Feature: VACUUM/ANALYZE ONLY on partitioned distributed table + -- cleanup with minimum verbosity SET client_min_messages TO ERROR; RESET search_path;