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