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:
ALTER COLUMN SET/RESET (attribute_option = value)(e.g.,n_distinct)ALTER COLUMN SET COMPRESSIONALTER COLUMN SET STATISTICSENABLE/DISABLE [REPLICA|ALWAYS] RULEENABLE/DISABLE ROW LEVEL SECURITYFORCE/NO FORCE ROW LEVEL SECURITYREPLICA IDENTITYOWNER TOSET TABLESPACESET SCHEMASET ACCESS METHOD(surfaced late by Zsolt Parragi)
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:
- It requires users to infer meaning from silence, which is fragile.
- It inverts the burden — users would need to mentally track which commands emit notices to know what the silence means.
- For the current state (nothing recurses, ever, for these commands), there is no "affirmative recursion" path to emit the positive notice from.
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:
- v1:
"REPLICA IDENTITY is only applied to the partitioned table itself"— David objected this is factually wrong; partitions can be altered, just individually. - v3 (David's wording):
"present partitions not affected"+HINT: partitions may be modified individually using separate commands. - v4: Evan re-added the action name because multi-subcommand
ALTER TABLEstatements would otherwise produce ambiguous notices. Settled on"ALTER action %s on relation \"%s\" does not affect present partitions", matching theATSimplePermissions()style, usingalter_table_type_to_string()for the action label. - v5+: Jim Jones enforced the project's Error Message Style Guide — hints must be full sentences, capitalized, period-terminated. Final hint:
"Partitions may be modified individually, or specify ONLY to suppress this message." - Greg later suggested
s/present/existing/for naturalness.
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 1d92e0c2cc — collect messages during execution and emit at the end. This design:
- Calls
CollectPartitionNoRecurseNotice()after each sub-command executes successfully. - Deduplicates (e.g., two identical
SET COMPRESSIONsub-commands emit one notice). - Calls
EmitPartitionNoRecurseNotice()once at the end ofATController(), emitting all collected notices followed by a single trailingHINT.
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:
- Not a hot path.
- Duplicate sub-commands are rare in practice.
- Deferring message construction forces keeping
Relationopen (or snapshotting its name) across the wholeATController()lifetime —relis closed early in the current flow. A temp variable is extra plumbing with no user-visible benefit.
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
RENAME: relation names are inherently independent per-relation; there is no meaningful "recursion" concept.OF/NOT OF(typed table binding): semantically apply only to the top table.
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:
- When the message is decided (at successful sub-command execution, with full recursion context).
- When the message is shown (at end of statement, deduplicated, with a single trailing HINT).
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
- Chao Li (Evan, author): Drove the patch across ten+ revisions, showing good judgment on when to accept suggestions and when to push back with reasoning (e.g., the Greg performance discussion).
- David G. Johnston: Set the tone early with the severity and wording corrections. Not a committer, but an experienced documentation/UX voice whose wording suggestions landed verbatim in the final patch.
- Jim Jones: Provided the most substantive review — both style-guide enforcement and the critical "notice before error" correctness bug. Volunteered to mark Ready for Committer and rebased v10 when it bit-rotted. Functionally acting as the primary reviewer.
- Greg Sabino Mullane: Code-level review on struct design, naming, dedup logic, and wording. Pushed on efficiency ideas but accepted the author's tradeoff reasoning.
- Zsolt Parragi (Percona): Domain catch — identified the missing
SET ACCESS METHODcase, which fits the pattern exactly (parent catalog entry changes, existing children untouched, affects future children only).
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
existingvspresent— Greg suggested the wording change; it's unclear from the visible messages whether this was ultimately adopted.SET SCHEMAspecial-casing — flagged as ugly but deferred.- 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. - 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.