From effb923d9dec8fd4a5102fee80e52d65d86747c8 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 27 Jun 2026 16:48:35 -0400 Subject: [PATCH] COPY TO FORMAT JSON: respect column list order When COPY TO with FORMAT json is given an explicit column list that names all columns in a different order, the JSON output incorrectly used the table's physical column order instead of the requested order. This happened because BeginCopyTo() only built a restricted TupleDesc when list_length(attnumlist) < tupDesc->natts. When all columns are listed (just reordered), this condition was false and no projected TupleDesc was built, causing CopyToJsonOneRow() to emit columns in physical order. Fix by also building the projected TupleDesc when an explicit column list was provided (attnamelist != NIL), even if it names all columns. Author: Baji Shaik Reviewed-by: Andrew Dunstan Discussion: https://postgr.es/m/CA+fm-ROd4cNKM524n6EdgtZ9xOzOHJDNv8J_9Mvr2+2t1qWSDw@mail.gmail.com --- src/backend/commands/copyto.c | 14 +++++++++----- src/test/regress/expected/copy.out | 9 +++++++++ src/test/regress/sql/copy.sql | 9 +++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index 6755bb698de..d30cad019c7 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -1051,15 +1051,19 @@ BeginCopyTo(ParseState *pstate, { cstate->json_buf = makeStringInfo(); - if (rel && list_length(cstate->attnumlist) < tupDesc->natts) + /* + * Build a projected TupleDesc describing only the selected columns + * so that composite_to_json() emits the right column names and + * types; needed when an explicit column list was given (possibly + * with a different column order) or when generated columns are + * excluded from the output. + */ + 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..ace38225623 100644 --- a/src/test/regress/expected/copy.out +++ b/src/test/regress/expected/copy.out @@ -73,6 +73,15 @@ copy copytest3 to stdout csv header; c1,"col with , comma","col with "" quote" 1,a,1 2,b,2 +-- testing explicit column order +create temp table copytest_order (a int, b int, c int); +copy copytest_order from stdin; +copy copytest_order (c, b, a) to stdout; +3 2 1 +copy copytest_order (c, b, a) to stdout (format csv); +3,2,1 +copy copytest_order (c, b, a) to stdout (format json); +{"c":3,"b":2,"a":1} --- 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..507b822946f 100644 --- a/src/test/regress/sql/copy.sql +++ b/src/test/regress/sql/copy.sql @@ -82,6 +82,15 @@ this is just a line full of junk that would error out if parsed copy copytest3 to stdout csv header; +-- testing explicit column order +create temp table copytest_order (a int, b int, c int); +copy copytest_order from stdin; +1 2 3 +\. +copy copytest_order (c, b, a) to stdout; +copy copytest_order (c, b, a) to stdout (format csv); +copy copytest_order (c, b, a) 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);