Monthly Summary: Fix UPDATE/DELETE FOR PORTION OF with Inheritance Tables (May 2026)
Overview
This thread addresses a correctness bug in PostgreSQL's temporal DML (FOR PORTION OF) when applied to classical (non-partitioned) inheritance hierarchies. The bug involves two defects: (1) leftover tuples from range-splitting are incorrectly inserted into the parent table instead of the originating child, and (2) attribute number mismatches between parent and child relations cause tuple corruption. The month saw the thread briefly merged into a larger consolidated thread, then separated back out with a new standalone v2 patch and discussion toward v3.
Key Developments
Bug Identification and Impact
The core issue is that FOR PORTION OF implementation conflates "has children" with "is partitioned." In partitioned tables, routing leftovers through the root is correct because tuple routing dispatches to the right leaf. In plain inheritance, no such routing exists — leftovers silently land in the parent table, constituting data corruption from the user's perspective (affecting visibility under ONLY, triggers, constraints, RLS, and logical replication).
Thread Merge and Un-merge
Early in the month, the thread was administratively merged into a larger thread addressing updatedCols issues (message-id CAHg+QDcd=t69gLf9yQexO07EJ2mx0Z70NFHo6h94X1EDA=hM0g). Once the UPDATE permission checking issue was resolved independently on that thread, Paul Jungwirth restored this patch's independence with a v2 submission.
v2 Patch and Architectural Discussion
Paul Jungwirth posted a standalone v2 patch that:
- Introduces
ExecInitForPortionOf()to consolidate executor state initialization for FOR PORTION OF (attnum resolution, tuple conversion maps) - Uses a
partitionRoutingboolean to distinguish partitioned routing (leftovers go through root) from plain inheritance (leftovers go directly back to the source child) - Resolves the range column's
attnumrelative to the actual child relation
Lazy vs. Eager Initialization Debate
Paul initially proposed moving FOR PORTION OF per-child initialization into ExecInitModifyTable() (eager), arguing the planner already prunes irrelevant partitions. He then reversed this position after recognizing that runtime filtering can eliminate child tables beyond what the planner prunes, making lazy initialization still valuable for avoiding unnecessary work.
Direction Toward v3
The preferred approach retains ExecInitForPortionOf() as a refactoring that reduces branching, consolidates attnum tracking, and makes the inheritance fix clearer. The earlier v1 patch (from jian he) initialized tuple conversion maps for all children but only used them for partitions — a code smell the refactored version avoids.
Open Questions
- Handling of dropped columns between parent and child range attnum positions
- Whether
FOR PORTION OFon plain-inheritance parents should be a long-term supported pattern - Correct
tableoidreporting in RETURNING clauses for leftover inserts - Trigger and constraint firing semantics on leftover INSERTs to child relations
- Authorship/credit attribution among three contributors