PG18 – Respect VACUUM/ANALYZE ONLY semantics for Citus tables (#8365)

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 <citus_table>` / `ANALYZE ONLY <citus_table>`:

    * 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.
main
Mehmet YILMAZ 2025-12-05 16:50:42 +03:00 committed by GitHub
parent 3399d660f3
commit 31911d8297
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 374 additions and 64 deletions

View File

@ -48,21 +48,27 @@ typedef struct CitusVacuumParams
#endif #endif
} CitusVacuumParams; } 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 */ /* 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, static List * VacuumTaskList(Oid relationId, CitusVacuumParams vacuumParams,
List *vacuumColumnList); List *vacuumColumnList);
static char * DeparseVacuumStmtPrefix(CitusVacuumParams vacuumParams); static char * DeparseVacuumStmtPrefix(CitusVacuumParams vacuumParams);
static char * DeparseVacuumColumnNames(List *columnNameList); static char * DeparseVacuumColumnNames(List *columnNameList);
static List * VacuumColumnList(VacuumStmt *vacuumStmt, int relationIndex); static void ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationList,
static List * ExtractVacuumTargetRels(VacuumStmt *vacuumStmt);
static void ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationIdList,
CitusVacuumParams vacuumParams); CitusVacuumParams vacuumParams);
static void ExecuteUnqualifiedVacuumTasks(VacuumStmt *vacuumStmt, static void ExecuteUnqualifiedVacuumTasks(VacuumStmt *vacuumStmt,
CitusVacuumParams vacuumParams); CitusVacuumParams vacuumParams);
static CitusVacuumParams VacuumStmtParams(VacuumStmt *vacstmt); static CitusVacuumParams VacuumStmtParams(VacuumStmt *vacstmt);
static List * VacuumRelationIdList(VacuumStmt *vacuumStmt, CitusVacuumParams static List * VacuumRelationList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams);
vacuumParams);
/* /*
* PostprocessVacuumStmt processes vacuum statements that may need propagation to * 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; * when no table is specified propagate the command as it is;
* otherwise, only propagate when there is at least 1 citus table * 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) if (list_length(vacuumStmt->rels) == 0)
{ {
@ -105,11 +111,11 @@ PostprocessVacuumStmt(Node *node, const char *vacuumCommand)
ExecuteUnqualifiedVacuumTasks(vacuumStmt, vacuumParams); ExecuteUnqualifiedVacuumTasks(vacuumStmt, vacuumParams);
} }
else if (IsDistributedVacuumStmt(relationIdList)) else if (IsDistributedVacuumStmt(vacuumRelationList))
{ {
/* there is at least 1 citus table specified */ /* there is at least 1 citus table specified */
ExecuteVacuumOnDistributedTables(vacuumStmt, relationIdList, ExecuteVacuumOnDistributedTables(vacuumStmt, vacuumRelationList,
vacuumParams); 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 * static List *
VacuumRelationIdList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams) VacuumRelationList(VacuumStmt *vacuumStmt, CitusVacuumParams vacuumParams)
{ {
LOCKMODE lockMode = (vacuumParams.options & VACOPT_FULL) ? AccessExclusiveLock : LOCKMODE lockMode = (vacuumParams.options & VACOPT_FULL) ? AccessExclusiveLock :
ShareUpdateExclusiveLock; ShareUpdateExclusiveLock;
bool skipLocked = (vacuumParams.options & VACOPT_SKIP_LOCKED); bool skipLocked = (vacuumParams.options & VACOPT_SKIP_LOCKED);
List *vacuumRelationList = ExtractVacuumTargetRels(vacuumStmt); List *relationList = NIL;
List *relationIdList = NIL; VacuumRelation *vacuumRelation = NULL;
foreach_declared_ptr(vacuumRelation, vacuumStmt->rels)
RangeVar *vacuumRelation = NULL;
foreach_declared_ptr(vacuumRelation, vacuumRelationList)
{ {
Oid relationId = InvalidOid;
/* /*
* If skip_locked option is enabled, we are skipping that relation * 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, lockMode,
skipLocked ? RVR_SKIP_LOCKED : 0, NULL, skipLocked ? RVR_SKIP_LOCKED : 0, NULL,
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)) 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. * otherwise, it returns false.
*/ */
static bool static bool
IsDistributedVacuumStmt(List *vacuumRelationIdList) IsDistributedVacuumStmt(List *vacuumRelationList)
{ {
Oid relationId = InvalidOid; CitusVacuumRelation *vacuumRelation = NULL;
foreach_declared_oid(relationId, vacuumRelationIdList) foreach_declared_ptr(vacuumRelation, vacuumRelationList)
{ {
if (OidIsValid(relationId) && IsCitusTable(relationId)) if (OidIsValid(vacuumRelation->relationId) &&
IsCitusTable(vacuumRelation->relationId))
{ {
return true; return true;
} }
@ -181,24 +207,31 @@ IsDistributedVacuumStmt(List *vacuumRelationIdList)
* if they are citus tables. * if they are citus tables.
*/ */
static void static void
ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationIdList, ExecuteVacuumOnDistributedTables(VacuumStmt *vacuumStmt, List *relationList,
CitusVacuumParams vacuumParams) CitusVacuumParams vacuumParams)
{ {
int relationIndex = 0; CitusVacuumRelation *vacuumRelationEntry = NULL;
foreach_declared_ptr(vacuumRelationEntry, relationList)
Oid relationId = InvalidOid;
foreach_declared_oid(relationId, relationIdList)
{ {
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)) if (IsCitusTable(relationId))
{ {
List *vacuumColumnList = VacuumColumnList(vacuumStmt, relationIndex); List *vacuumColumnList = vacuumRelation->va_cols;
List *taskList = VacuumTaskList(relationId, vacuumParams, vacuumColumnList); List *taskList = VacuumTaskList(relationId, vacuumParams, vacuumColumnList);
/* local execution is not implemented for VACUUM commands */ /* local execution is not implemented for VACUUM commands */
bool localExecutionSupported = false; bool localExecutionSupported = false;
ExecuteUtilityTaskList(taskList, localExecutionSupported); 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. * VacuumStmtParams returns a CitusVacuumParams based on the supplied VacuumStmt.
*/ */

View File

@ -1802,6 +1802,156 @@ NOTICE: renaming the new table to pg18_nn.generated_stored_ref
(4 rows) (4 rows)
ROLLBACK; 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 -- cleanup with minimum verbosity
SET client_min_messages TO ERROR; SET client_min_messages TO ERROR;
RESET search_path; RESET search_path;

View File

@ -1120,6 +1120,166 @@ BEGIN;
SELECT * FROM generated_stored_ref; SELECT * FROM generated_stored_ref;
ROLLBACK; 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 -- cleanup with minimum verbosity
SET client_min_messages TO ERROR; SET client_min_messages TO ERROR;
RESET search_path; RESET search_path;