From b577c008566a2d295f84c847ae55fd65035a0487 Mon Sep 17 00:00:00 2001 From: Baji Shaik 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 --- 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)