Fix Bug of COPY TO Support Partition Table
Core Problem
The "Support COPY TO for partitioned tables" feature introduced a bug in the attribute mapping logic when converting partition tuples to the root table's rowtype during COPY TO operations on partitioned tables.
Technical Background
When PostgreSQL performs COPY TO on a partitioned table, it must scan each partition individually and output tuples in the root table's column format. Partitions can have different physical tuple descriptors than their root tables — for example, a partition may have been created independently with extra columns that were later dropped, resulting in different attribute numbering in the physical tuple.
To handle this, the code uses build_attrmap_by_name_if_req() to create an attribute map, and then execute_attr_map_slot() to transform tuples from the partition's format to the root table's format.
The Bug
The attribute map was constructed with reversed arguments:
map = build_attrmap_by_name_if_req(RelationGetDescr(root_rel), // input descriptor
RelationGetDescr(rel), // output descriptor
false);
The semantics of build_attrmap_by_name_if_req() are: given an input tuple descriptor and an output tuple descriptor, build a map that transforms tuples from input format to output format. However, the code then uses this map in execute_attr_map_slot(map, slot, root_slot) where:
slotcontains a partition tuple (input)root_slotis the destination in root table format (output)
This means the map should map from partition descriptor to root descriptor, but was incorrectly built mapping from root descriptor to partition descriptor — exactly backwards.
Why It Was Latent
In the common case, partitions are created directly via CREATE TABLE ... PARTITION OF, which gives them identical physical column layouts to the root table. In this case, build_attrmap_by_name_if_req() returns NULL (no mapping needed), and the code falls through to the else branch that simply deconstructs the slot directly. The bug only manifests when a partition has a physically different tuple descriptor — such as when an independently-created table (with dropped columns causing different attribute offsets) is attached as a partition.
Reproduction Scenario
The reporter demonstrated the bug by:
- Creating a standalone table
c (x int, a int, b int) - Dropping column
x(leaving a "hole" in the physical tuple descriptor at attnum 1) - Attaching
cas a partition ofp (a int, b int) - Running
COPY p TO stdout— which incorrectly outputs all NULLs because the reversed map reads from wrong attribute positions
The Fix
The fix is a one-line argument swap:
map = build_attrmap_by_name_if_req(RelationGetDescr(rel), // partition (input)
RelationGetDescr(root_rel), // root table (output)
false);
This correctly builds a map that transforms from partition tuple format to root table tuple format, matching the subsequent execute_attr_map_slot(map, slot, root_slot) call.
Architectural Significance
This bug class is a recurring pattern in PostgreSQL's partition handling code. The attribute mapping API (build_attrmap_by_name_if_req, execute_attr_map_slot) has subtle directional semantics that are easy to confuse. Similar bugs have appeared in:
- Partition tuple routing (INSERT path)
- Partition-wise join result projection
- Logical replication column mapping
The fact that most test scenarios use partitions with identical layouts to their parents means these mapping bugs can persist undetected until edge cases involving dropped columns, column reordering, or independently-created tables expose them.
Review Assessment
The fix is minimal, correct, and low-risk. The original author (Sawada) acknowledged the bug and pushed the fix quickly, which is appropriate for a clear-cut correctness issue with an obvious one-line fix.