ALTER TABLE: warn when actions do not recurse to partitions

First seen: 2026-01-12 09:02:19+00:00 · Messages: 28 · Participants: 5

Latest Update

2026-05-06 · opus 4.7

ALTER TABLE: Warning When Actions Do Not Recurse to Partitions

The Core Problem: A Semantic Inconsistency in ALTER TABLE

PostgreSQL's ALTER TABLE command carries a documented contract regarding the ONLY keyword:

If ONLY is specified before the table name, only that table is altered. If ONLY is not specified, the table and all its descendant tables (if any) are altered.

This contract is violated silently by a substantial set of sub-commands when applied to partitioned tables. For these sub-commands, omitting ONLY produces exactly the same behavior as specifying it — the change applies only to the partitioned table (the catalog parent), never to its partitions. The user's reasonable expectation, grounded in the documentation, is broken without any feedback.

The affected sub-commands, as enumerated over the course of this thread, are:

Why does this matter architecturally? Partitioned tables in PostgreSQL are implemented as a catalog parent with independent physical child relations (pg_class entries per partition). Some properties are logical attributes of the whole logical table (e.g., constraints, column definitions) and propagate naturally through DDL recursion. Others are physical/storage properties of each relation (tablespace, access method, compression, replica identity, row-level security enablement), which are stored per-pg_class row. The latter category was intentionally left non-recursive historically, but without any user-visible signal. The silent divergence between syntactic expectation and actual semantics is a documented UX wart that prior threads (see 59FB38EF-FA62-41B7-A082-DDA251B04F9E@gmail.com) acknowledged but did not resolve.

Evan Li (Chao Li) framed this patch as a scoped, non-semantic change: rather than fix the recursion behavior (which requires broad design work and possibly breaks existing applications), emit a user-visible notice so that the current behavior stops being a silent surprise. The long-term goal stated in the opening message is inversion of the default assumption: "users can reasonably assume that an ALTER TABLE partitioned_table ... action will recurse to child partitions unless a warning explicitly tells them otherwise."

Design Decisions and Their Evolution

1. Severity: WARNING → NOTICE

The initial v1 used WARNING. David G. Johnston immediately pushed back: NOTICE is the appropriate severity class for informational messages about expected, documented behavior. WARNING implies something potentially wrong. This is consistent with existing PostgreSQL practice (e.g., ATSimplePermissions() uses NOTICE for similar informational output). Adopted in v2.

2. Signaling Approach: Affirmative Notice vs. Silent-Means-No-Recurse

David proposed an elegant alternative: always emit a notice with a partition count on recursion, and let absence of that notice indicate non-recursion. Greg Sabino Mullane and Evan both rejected this:

The consensus landed on: emit the notice only when the user's expectation is likely violated — i.e., when ONLY is omitted, the table is partitioned, and the sub-command is one that does not recurse.

3. Partition Count in the Message

v2 included (N affected). David argued this was either always 1 (misleading) or beside the point (since nothing is affected). The real signal is "partitions exist but are not touched." Evan dropped the count in v3 and instead suppressed the notice entirely when the partitioned table has no partitions — a sensible refinement, since in that case there is no expectation gap to flag.

4. Message Wording

Went through several iterations:

5. Notice Ordering vs. Error Handling (Critical Correctness Fix)

Jim's most substantive finding was at the semantic level, not cosmetic. In v4, EmitPartitionNoRecurseNotice() was called during ATPrepCmd() — before the sub-command actually executed. This produced a nonsensical trace:

ALTER TABLE m ALTER COLUMN b SET COMPRESSION pglz;
NOTICE:  ALTER action ALTER COLUMN ... SET COMPRESSION ... does not affect present partitions
HINT:  ...
ERROR:  column data type integer does not support compression

A notice about partition semantics for an action that never happened is worse than useless — it actively misleads. Jim proposed moving the emission to ATExecCmd() after each sub-command succeeds, threading cmd->recurse through if needed.

6. Deferred/Deduplicated Emission (v5 Refactor)

Evan declined the direct "emit after ATExecCmd" approach because ATExecCmd lacks clean access to recursion state, and setting cmd->recurse = true purely for messaging would be a layering violation. Instead, he borrowed the pattern from commit 1d92e0c2cccollect messages during execution and emit at the end. This design:

  1. Calls CollectPartitionNoRecurseNotice() after each sub-command executes successfully.
  2. Deduplicates (e.g., two identical SET COMPRESSION sub-commands emit one notice).
  3. Calls EmitPartitionNoRecurseNotice() once at the end of ATController(), emitting all collected notices followed by a single trailing HINT.

This also solves a secondary bug Evan identified: in v4, the HINT was duplicated per sub-command, which is noisy in multi-action statements.

7. Storage Structure Bikeshed

Greg questioned whether Evan needed a dedicated struct (partitionNoRecurseNotice) to hold a single List * field. Evan agreed and dropped the struct in v7. Greg further suggested tracking (action, relation) tuples and rendering the message string only at emission time, avoiding string construction for duplicates. Evan pushed back on cost-benefit grounds:

Greg accepted the reasoning. Final polish: replace the bool found + break pattern with an early return; on strcmp match.

8. SET SCHEMA Special-Casing

SET SCHEMA does not flow through the standard AlterTable() → ATController() → ATPrepCmd() path — it has its own entry point. This forces a hard-coded AT_SetSchema check to produce the correct action string. Greg flagged this as "mildly uneasy / hacky" but agreed it wasn't worth refactoring the whole SET SCHEMA codepath to fix.

9. Intentionally Excluded Sub-commands

Technical Insights

The "existing partitions" caveat in wording

David's phrasing "present partitions not affected" is subtly careful. It specifies present (currently existing) partitions. This matters for SET ACCESS METHOD in particular (noted by Zsolt): the change does take effect for future partitions created afterward, but existing ones retain their old AM. The notice correctly scopes its claim to existing partitions without overstating it.

Catalog-level vs. storage-level properties

The list of non-recursing sub-commands maps almost perfectly to properties stored in the parent's own pg_class / pg_attribute row that are either purely administrative (owner, schema, tablespace default) or per-physical-relation properties not currently propagated (replica identity, RLS, compression, statistics targets, access method, attribute options). This strongly suggests a future unified recursion policy is feasible but out of scope here.

The collect-then-emit pattern

Borrowing from 1d92e0c2cc is a legitimate reuse of an existing PostgreSQL pattern for handling multi-sub-command DDL messaging cleanly. It neatly separates:

Regression test churn

The patch required touching numerous existing regression tests to add ONLY — because those tests were incidentally exercising the non-recursion path on partitioned tables and would now spuriously emit notices. Jim confirmed this was acceptable churn and arguably makes those tests more precise about intent.

Participant Dynamics

No committer participated directly in this thread during the window shown; the patch was iterated to Ready for Committer state by peer review. This is a typical trajectory for a low-risk UX improvement.

Open Questions at End of Thread

  1. existing vs present — Greg suggested the wording change; it's unclear from the visible messages whether this was ultimately adopted.
  2. SET SCHEMA special-casing — flagged as ugly but deferred.
  3. The REPACK (CONCURRENTLY) interaction — Jim's 0002 in the final rebase addresses new failing tests introduced by REPACK on partitioned tables, suggesting the interaction between this notice infrastructure and newer partitioned-table DDL surfaces needs ongoing vigilance.
  4. The larger vision — this patch is explicitly a stepping stone. Whether the community eventually agrees to make these sub-commands actually recurse (with a compatibility GUC or major-version flag day) remains an open design question referenced in the opening message.