From adad949fd2f6a721b26f915238305d6b71189f7f Mon Sep 17 00:00:00 2001 From: Baji Shaik Date: Mon, 15 Jun 2026 11:54:27 -0500 Subject: [PATCH v3 1/1] 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 --- src/backend/commands/copyto.c | 11 ++++++----- src/test/regress/expected/copy.out | 19 +++++++++++++++++++ src/test/regress/sql/copy.sql | 5 +++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index 6755bb698de..1024d0f40ba 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -1051,15 +1051,16 @@ 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 or generated columns are excluded. + */ + 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..5b3775a3cc2 100644 --- a/src/test/regress/expected/copy.out +++ b/src/test/regress/expected/copy.out @@ -73,6 +73,25 @@ copy copytest3 to stdout csv header; c1,"col with , comma","col with "" quote" 1,a,1 2,b,2 +-- testing explicit column order +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 +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"} --- test copying in JSON mode with various styles copy (select 1 union all select 2) to stdout with (format json); {"?column?":1} diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql index 094fd76c12b..ad1d3228342 100644 --- a/src/test/regress/sql/copy.sql +++ b/src/test/regress/sql/copy.sql @@ -82,6 +82,11 @@ this is just a line full of junk that would error out if parsed copy copytest3 to stdout csv header; +-- testing explicit column order +copy copytest (filler, test, style) to stdout; +copy copytest (filler, test, style) to stdout (format csv); +copy copytest (filler, test, style) to stdout (format json); + --- 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); -- 2.50.1 (Apple Git-155)