From a4c3c76effc9cc8e229d046af6eadca708daf06d Mon Sep 17 00:00:00 2001
From: Baji Shaik <baji.pgdev@gmail.com>
Date: Fri, 12 Jun 2026 17:41:57 -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      | 7 ++++++-
 src/test/regress/expected/copy.out | 8 ++++++++
 src/test/regress/sql/copy.sql      | 5 +++++
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 6755bb698de..7af4c6eafd3 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -1051,7 +1051,8 @@ BeginCopyTo(ParseState *pstate,
 	{
 		cstate->json_buf = makeStringInfo();
 
-		if (rel && list_length(cstate->attnumlist) < tupDesc->natts)
+		if (rel && (attnamelist != NIL ||
+					list_length(cstate->attnumlist) < tupDesc->natts))
 		{
 			int			natts = list_length(cstate->attnumlist);
 			TupleDesc	resultDesc;
@@ -1059,6 +1060,10 @@ BeginCopyTo(ParseState *pstate,
 			/*
 			 * Build a TupleDesc describing only the selected columns so that
 			 * composite_to_json() emits the right column names and types.
+			 *
+			 * This fires when the user gave an explicit column list (which may
+			 * subset or reorder columns) or when the default list excludes
+			 * generated columns.
 			 */
 			resultDesc = CreateTemplateTupleDesc(natts);
 
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 37498cdd6e7..74dd08e5af6 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -144,6 +144,14 @@ 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 list that reorders all columns must be honored in the JSON output,
+-- like text/CSV (the keys must appear in the requested order, not the table's
+-- physical order)
+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..aea41e7e7d0 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -111,6 +111,11 @@ copy copytest from stdin(format json);
 -- column list with json format
 copy copytest (style, test, filler) to stdout (format json);
 
+-- column list that reorders all columns must be honored in the JSON output,
+-- like text/CSV (the keys must appear in the requested order, not the table's
+-- physical order)
+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)

