From b577c008566a2d295f84c847ae55fd65035a0487 Mon Sep 17 00:00:00 2001
From: Baji Shaik <baji.pgdev@gmail.com>
Date: Mon, 15 Jun 2026 11:54:27 -0500
Subject: [PATCH] COPY TO FORMAT JSON: respect column list order

When the user specifies a column list that includes every column but
in a different order, COPY TO with FORMAT json ignores the reordering
and outputs JSON keys in the table's physical column order.  Text and
CSV formats correctly respect the user-specified order.

The bug is in BeginCopyTo() where the JSON path builds a projected
TupleDesc only when list_length(attnumlist) < natts.  When all columns
are listed (in a different order), the condition is false and the
relation's original TupleDesc is used, losing the reorder.

Fix by extending the condition to also fire when an explicit column
list was supplied (attnamelist != NIL).

Author: Baji Shaik <baji.pgdev@gmail.com>
---
 src/backend/commands/copyto.c      | 12 +++++++-----
 src/test/regress/expected/copy.out | 26 ++++++++++++++++++++++++++
 src/test/regress/sql/copy.sql      | 10 ++++++++++
 3 files changed, 43 insertions(+), 5 deletions(-)

diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 6755bb698de..fbcd73be785 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -1051,15 +1051,17 @@ BeginCopyTo(ParseState *pstate,
 	{
 		cstate->json_buf = makeStringInfo();
 
-		if (rel && list_length(cstate->attnumlist) < tupDesc->natts)
+		/*
+		 * Build a projected TupleDesc for JSON output when columns are
+		 * explicitly listed (possibly reordered) or generated columns are
+		 * excluded from the default list.
+		 */
+		if (rel && (attnamelist != NIL ||
+					list_length(cstate->attnumlist) < tupDesc->natts))
 		{
 			int			natts = list_length(cstate->attnumlist);
 			TupleDesc	resultDesc;
 
-			/*
-			 * Build a TupleDesc describing only the selected columns so that
-			 * composite_to_json() emits the right column names and types.
-			 */
 			resultDesc = CreateTemplateTupleDesc(natts);
 
 			foreach_int(attnum, cstate->attnumlist)
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 37498cdd6e7..2b0d601787a 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -73,6 +73,20 @@ copy copytest3 to stdout csv header;
 c1,"col with , comma","col with "" quote"
 1,a,1
 2,b,2
+-- column reordering
+copy copytest (filler, test, style) to stdout;
+1	abc\r\ndef	DOS
+2	abc\ndef	Unix
+3	abc\rdef	Mac
+4	a\\r\\\r\\\n\\nb	esc\\ape
+copy copytest (filler, test, style) to stdout (format csv);
+1,"abc
+def",DOS
+2,"abc
+def",Unix
+3,"abcdef",Mac
+4,"a\r\\
+\nb",esc\ape
 --- test copying in JSON mode with various styles
 copy (select 1 union all select 2) to stdout with (format json);
 {"?column?":1}
@@ -144,6 +158,18 @@ copy copytest (style, test, filler) to stdout (format json);
 {"style":"Unix","test":"abc\ndef","filler":2}
 {"style":"Mac","test":"abc\rdef","filler":3}
 {"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4}
+-- column subset with json format
+copy copytest (filler, style) to stdout (format json);
+{"filler":1,"style":"DOS"}
+{"filler":2,"style":"Unix"}
+{"filler":3,"style":"Mac"}
+{"filler":4,"style":"esc\\ape"}
+-- column reordering with json format
+copy copytest (filler, test, style) to stdout (format json);
+{"filler":1,"test":"abc\r\ndef","style":"DOS"}
+{"filler":2,"test":"abc\ndef","style":"Unix"}
+{"filler":3,"test":"abc\rdef","style":"Mac"}
+{"filler":4,"test":"a\\r\\\r\\\n\\nb","style":"esc\\ape"}
 -- should fail: force_array requires json format
 copy copytest to stdout (format csv, force_array true);
 ERROR:  COPY FORCE_ARRAY can only be used with JSON mode
diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql
index 094fd76c12b..e0580a5150a 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -82,6 +82,10 @@ this is just a line full of junk that would error out if parsed
 
 copy copytest3 to stdout csv header;
 
+-- column reordering
+copy copytest (filler, test, style) to stdout;
+copy copytest (filler, test, style) to stdout (format csv);
+
 --- test copying in JSON mode with various styles
 copy (select 1 union all select 2) to stdout with (format json);
 copy (select 1 as foo union all select 2) to stdout with (format json);
@@ -111,6 +115,12 @@ copy copytest from stdin(format json);
 -- column list with json format
 copy copytest (style, test, filler) to stdout (format json);
 
+-- column subset with json format
+copy copytest (filler, style) to stdout (format json);
+
+-- column reordering with json format
+copy copytest (filler, test, style) to stdout (format json);
+
 -- should fail: force_array requires json format
 copy copytest to stdout (format csv, force_array true);
 
-- 
2.50.1 (Apple Git-155)

