pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
From: Chao Li <li.evan.chao@gmail.com>
To: PostgreSQL-development <pgsql-hackers@postgresql.org>
Cc: Masahiko Sawada <sawada.mshk@gmail.com>
Cc: jian he <jian.universality@gmail.com>
Subject: Fix bug of COPY TO support partition table
Date: Thu, 14 May 2026 17:59:46 +0800
Message-ID: <85EA70F3-C3DB-477B-B856-EA569FDAAE7C@gmail.com> (raw)

Hi,

I just tested the "Support COPY TO for partitioned tables" patch. While tracing a normal case, I noticed this suspicious code:
```
	/*
	 * If we are exporting partition data here, we check if converting tuples
	 * to the root table's rowtype, because a partition might have column
	 * order different than its root table.
	 */
	if (root_rel != NULL)
	{
		root_slot = table_slot_create(root_rel, NULL);
		map = build_attrmap_by_name_if_req(RelationGetDescr(root_rel),   <<== input is root table
										   RelationGetDescr(rel), <== output is partition
										   false);
	}

	while (table_scan_getnextslot(scandesc, ForwardScanDirection, slot))
	{
		TupleTableSlot *copyslot;

		CHECK_FOR_INTERRUPTS();

		if (map != NULL)
			copyslot = execute_attr_map_slot(map, slot, root_slot); <== input is partition, output is root table
		else
		{
			/* Deconstruct the tuple */
			slot_getallattrs(slot);
			copyslot = slot;
		}
```

The code comment says that partition tuples should be converted to the root table's rowtype before output. Also, `execute_attr_map_slot()` takes the partition slot as input and the root slot as output. However, the attribute map is built in the opposite direction, with the root relation as input and the partition relation as output.

In normal cases, partitions have the same attribute list as the root table, so this bug is not triggered. I built the following repro by dropping a column from the partition, which makes the partition’s physical tuple descriptor different from the root table's descriptor:
```
evantest=# create table p (a int, b int) partition by range (a);
CREATE TABLE
evantest=# create table c (x int, a int, b int);
CREATE TABLE
evantest=# alter table c drop column x;
ALTER TABLE
evantest=# alter table p attach partition c for values from (1) to (10);
ALTER TABLE
evantest=# insert into p values (1, 11), (2, 22);
INSERT 0 2
evantest=# copy p to stdout;
\N	\N
\N	\N
```

As shown above, COPY incorrectly outputs all NULLs.

The fix is straightforward, just reverse the order of `rel` and `root_rel` when calling `build_attrmap_by_name_if_req()`. After the fix, COPY works correctly:
```
evantest=# copy p to stdout;
1	11
2	22
```

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

Attachments:

  [application/octet-stream] v1-0001-Fix-COPY-TO-attribute-mapping-for-partitioned-tab.patch (3.1K, ../85EA70F3-C3DB-477B-B856-EA569FDAAE7C@gmail.com/2-v1-0001-Fix-COPY-TO-attribute-mapping-for-partitioned-tab.patch)
  download | inline diff:
From 29b28080867ddbf371135e6a9c3dc81a2ea89e13 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Thu, 14 May 2026 17:51:35 +0800
Subject: [PATCH v1] Fix COPY TO attribute mapping for partitioned tables

COPY TO on a partitioned table reads tuples from partitions and maps
them to the root table's tuple descriptor before output.  However, the
attribute map was built in the wrong direction, from the root table to
the partition.

This could produce incorrect results when a partition has a different
physical tuple descriptor from the root table, for example because it has
dropped columns.

Build the attribute map from the partition descriptor to the root table
descriptor instead.

Add a regression test covering COPY TO from a partition whose physical
descriptor includes a dropped column.

Author: Chao Li <lic@highgo.com>
---
 src/backend/commands/copyto.c      |  4 ++--
 src/test/regress/expected/copy.out | 11 +++++++++++
 src/test/regress/sql/copy.sql      |  9 +++++++++
 3 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 1085d0d5b8d..ffed63a2986 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -1348,8 +1348,8 @@ CopyRelationTo(CopyToState cstate, Relation rel, Relation root_rel, uint64 *proc
 	if (root_rel != NULL)
 	{
 		root_slot = table_slot_create(root_rel, NULL);
-		map = build_attrmap_by_name_if_req(RelationGetDescr(root_rel),
-										   RelationGetDescr(rel),
+		map = build_attrmap_by_name_if_req(RelationGetDescr(rel),
+										   RelationGetDescr(root_rel),
 										   false);
 	}
 
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 1714faab39c..37498cdd6e7 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -594,3 +594,14 @@ id	val
 5	15
 6	16
 DROP TABLE PP;
+-- Check if COPY TO handles dropped columns in partitions.
+CREATE TABLE pp_dropcol (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pp_dropcol_1 (dropme int, id int, val int);
+ALTER TABLE pp_dropcol_1 DROP COLUMN dropme;
+ALTER TABLE pp_dropcol ATTACH PARTITION pp_dropcol_1 FOR VALUES FROM (1) TO (10);
+INSERT INTO pp_dropcol VALUES (1, 11), (2, 12);
+COPY pp_dropcol TO stdout(header);
+id	val
+1	11
+2	12
+DROP TABLE pp_dropcol;
diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql
index eaad290b257..094fd76c12b 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -535,3 +535,12 @@ CREATE TABLE pp_510 PARTITION OF pp_2 FOR VALUES FROM (5) TO (10);
 INSERT INTO pp SELECT g, 10 + g FROM generate_series(1,6) g;
 COPY pp TO stdout(header);
 DROP TABLE PP;
+
+-- Check if COPY TO handles dropped columns in partitions.
+CREATE TABLE pp_dropcol (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pp_dropcol_1 (dropme int, id int, val int);
+ALTER TABLE pp_dropcol_1 DROP COLUMN dropme;
+ALTER TABLE pp_dropcol ATTACH PARTITION pp_dropcol_1 FOR VALUES FROM (1) TO (10);
+INSERT INTO pp_dropcol VALUES (1, 11), (2, 12);
+COPY pp_dropcol TO stdout(header);
+DROP TABLE pp_dropcol;
-- 
2.50.1 (Apple Git-155)

=

view thread (5+ messages)  latest in thread

Message-ID: <85EA70F3-C3DB-477B-B856-EA569FDAAE7C@gmail.com>
Permalink:  ../85EA70F3-C3DB-477B-B856-EA569FDAAE7C@gmail.com/
Also on:    postgresql.org/message-id/85EA70F3-C3DB-477B-B856-EA569FDAAE7C@gmail.com

 ·  · 

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: pgsql-hackers@postgresql.org
  Cc: li.evan.chao@gmail.com, sawada.mshk@gmail.com, jian.universality@gmail.com
  Subject: Re: Fix bug of COPY TO support partition table
  In-Reply-To: <85EA70F3-C3DB-477B-B856-EA569FDAAE7C@gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox