From c531737641b7c0951b29b80144362168d1f96aa1 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Fri, 6 Mar 2026 17:30:23 +0800
Subject: [PATCH v26 4/4] COPY TO JSON support column lists

Author: Andrew Dunstan <andrew@dunslane.net>
Reviewed-by: jian he <jian.universality@gmail.com>

discussion: https://postgr.es/m/CALvfUkBxTYy5uWPFVwpk_7ii2zgT07t3d-yR_cy4sfrrLU%3Dkcg%40mail.gmail.com
discussion: https://postgr.es/m/6a04628d-0d53-41d9-9e35-5a8dc302c34c@joeconway.com
---
 src/backend/commands/copyto.c      | 54 +++++++++++++++++++++++++-----
 src/test/regress/expected/copy.out | 15 +++++++--
 src/test/regress/sql/copy.sql      |  5 ++-
 3 files changed, 63 insertions(+), 11 deletions(-)

diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 4ea44daee0a..a45ef6bcab3 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -89,6 +89,8 @@ typedef struct CopyToStateData
 	bool		json_row_delim_needed;	/* need delimiter before next row */
 	StringInfo	json_buf;		/* reusable buffer for JSON output, it is
 								 * initliazed in BeginCopyTo  */
+	TupleDesc	tupDesc;		/* Descriptor for the COPY TO column list.
+								 * This is only necessary for JSON output. */
 	copy_data_dest_cb data_dest_cb; /* function for writing data */
 
 	CopyFormatOptions opts;
@@ -359,11 +361,17 @@ CopyToJsonOneRow(CopyToState cstate, TupleTableSlot *slot)
 	/*
 	 * composite_to_json() requires a stable TupleDesc. The slot's descriptor
 	 * (slot->tts_tupleDescriptor) may change during the execution of a SELECT
-	 * query, using cstate->queryDesc instead. No need worry this if COPY TO
-	 * is directly from a table.
+	 * query, using cstate->queryDesc instead.
+	 *
+	 * No need worry this if COPY TO is directly from a table, howeever when a
+	 * direct COPY TO from a table with a subset of columns in JSON mode, the
+	 * default slot's descriptor is obviously not OK, use the dedicated
+	 * TupleDesc constructed in BeginCopyTO.
 	 */
-	if (!cstate->rel)
-		slot->tts_tupleDescriptor = cstate->queryDesc->tupDesc;
+	if (cstate->rel && (RelationGetDescr(cstate->rel) != cstate->tupDesc))
+		ReleaseTupleDesc(slot->tts_tupleDescriptor);
+
+	slot->tts_tupleDescriptor = cstate->tupDesc;
 
 	resetStringInfo(cstate->json_buf);
 
@@ -839,6 +847,7 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 		cstate->partitions = children;
+		cstate->tupDesc = tupDesc;
 	}
 	else
 	{
@@ -976,6 +985,7 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = cstate->queryDesc->tupDesc;
 		tupDesc = BlessTupleDesc(tupDesc);
+		cstate->tupDesc = tupDesc;
 	}
 
 	/* Generate or convert list of attributes to process */
@@ -986,10 +996,38 @@ BeginCopyTo(ParseState *pstate,
 	{
 		cstate->json_buf = makeStringInfo();
 
-		if (attnamelist != NIL)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("column selection is not supported in JSON mode"));
+		if (attnamelist != NIL && rel)
+		{
+			char	   *attname;
+			Oid			atttypid;
+			int32		atttypmod;
+			int			attdim;
+			TupleDesc	resultDesc;
+
+			/*
+			 * allocate a new tuple descriptor
+			 */
+			resultDesc = CreateTemplateTupleDesc(list_length(cstate->attnumlist));
+
+			foreach_int(attnum, cstate->attnumlist)
+			{
+				Form_pg_attribute attr = TupleDescAttr(tupDesc, attnum - 1);
+
+				attname = NameStr(attr->attname);
+				atttypid = attr->atttypid;
+				atttypmod = attr->atttypmod;
+				attdim = attr->attndims;
+
+				TupleDescInitEntry(resultDesc,
+								   foreach_current_index(attnum) + 1,
+								   attname,
+								   atttypid,
+								   atttypmod,
+								   attdim);
+			}
+
+			cstate->tupDesc = BlessTupleDesc(resultDesc);
+		}
 	}
 
 	num_phys_attrs = tupDesc->natts;
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 309a33ca2e7..3ffb08bfada 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -129,8 +129,6 @@ copy copytest to stdout (format json, reject_limit 1);
 ERROR:  COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE
 copy copytest from stdin(format json);
 ERROR:  COPY JSON mode cannot be used with COPY FROM
-copy copytest (style) to stdout (format json);
-ERROR:  column selection is not supported in JSON mode
 -- all of the above should yield error
 -- should fail: force_array requires json format
 copy copytest to stdout (format csv, force_array true);
@@ -155,6 +153,19 @@ copy copytest to stdout (format json, force_array false);
 {"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 with json format
+copy copytest (style, filler) to stdout (format json);
+{"style":"DOS","filler":1667391763}
+{"style":"Unix","filler":1701055075}
+{"style":"Mac","filler":1667391761}
+{"style":"esc\\ape","filler":1918656791}
+copy copytest (style, filler) to stdout (format json,  force_array true);
+[
+ {"style":"DOS","filler":1667391763}
+,{"style":"Unix","filler":1701055075}
+,{"style":"Mac","filler":1667391761}
+,{"style":"esc\\ape","filler":1918656791}
+]
 -- embedded escaped characters
 create temp table copyjsontest (
     id bigserial,
diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql
index 8a20907dd4c..dae97bebb7d 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -104,7 +104,6 @@ copy copytest to stdout (format json, force_null *);
 copy copytest to stdout (format json, on_error ignore);
 copy copytest to stdout (format json, reject_limit 1);
 copy copytest from stdin(format json);
-copy copytest (style) to stdout (format json);
 -- all of the above should yield error
 
 -- should fail: force_array requires json format
@@ -115,6 +114,10 @@ copy copytest to stdout (format json, force_array);
 copy copytest to stdout (format json, force_array true);
 copy copytest to stdout (format json, force_array false);
 
+-- column list with json format
+copy copytest (style, filler) to stdout (format json);
+copy copytest (style, filler) to stdout (format json,  force_array true);
+
 -- embedded escaped characters
 create temp table copyjsontest (
     id bigserial,
-- 
2.34.1

