postgres.git / summary / log / commit / refs
commit f30d0c720f2ec979ab1b5b44b1f9f201d6efdf8c
Author: Andrew Dunstan <andrew@dunslane.net>
Date: Wed Apr 15 11:47:12 2026 +0000
Fix COPY TO FORMAT JSON to exclude generated columns.
COPY TO with FORMAT json was including generated columns in the
output, unlike TEXT and CSV formats. Virtual generated columns
appeared as null, and stored ones showed their computed values.
The JSON code path only built a restricted TupleDesc when an explicit
column list was given (attnamelist != NIL), but CopyGetAttnums()
also excludes generated columns from the default list. Fix by
checking whether the attnumlist is shorter than the full TupleDesc
instead.
Bug introduced in 7dadd38cda9.
Author: Satya Narlapuram <satya.narlapuram@gmail.com>
Reviewed-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/CAHg+QDcfpGDoPL3fvfjXRtfn=fny6DdJR6BAy6TpS1Xj2EZfXA@mail.gmail.com
src/backend/commands/copyto.c | 2 +-
src/test/regress/expected/generated_stored.out | 6 ++++++
src/test/regress/expected/generated_virtual.out | 6 ++++++
src/test/regress/sql/generated_stored.sql | 3 +++
src/test/regress/sql/generated_virtual.sql | 3 +++
5 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index f0e0147c665..85d15353647 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -1033,7 +1033,7 @@ BeginCopyTo(ParseState *pstate,
{
cstate->json_buf = makeStringInfo();
- if (attnamelist != NIL && rel)
+ if (rel && list_length(cstate->attnumlist) < tupDesc->natts)
{
int natts = list_length(cstate->attnumlist);
TupleDesc resultDesc;
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out
index 43cddeac373..4d329c60994 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_stored.out
@@ -541,6 +541,12 @@ SELECT * FROM gtest3 ORDER BY a;
4 | 12
(4 rows)
+-- COPY JSON should exclude generated columns, same as text/CSV
+COPY gtest1 TO stdout WITH (FORMAT json);
+{"a":1}
+{"a":2}
+{"a":3}
+{"a":4}
-- null values
CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
INSERT INTO gtest2 VALUES (1);
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 234061fa1f7..fc41c480d40 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -535,6 +535,12 @@ SELECT * FROM gtest3 ORDER BY a;
4 | 12
(4 rows)
+-- COPY JSON should exclude generated columns, same as text/CSV
+COPY gtest1 TO stdout WITH (FORMAT json);
+{"a":1}
+{"a":2}
+{"a":3}
+{"a":4}
-- null values
CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
INSERT INTO gtest2 VALUES (1);
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql
index 280021d79b7..1064839dcd2 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_stored.sql
@@ -239,6 +239,9 @@ COPY gtest3 (a, b) FROM stdin;
SELECT * FROM gtest3 ORDER BY a;
+-- COPY JSON should exclude generated columns, same as text/CSV
+COPY gtest1 TO stdout WITH (FORMAT json);
+
-- null values
CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
INSERT INTO gtest2 VALUES (1);
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index 4d9ad3c5dca..9b32413e3a9 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -239,6 +239,9 @@ COPY gtest3 (a, b) FROM stdin;
SELECT * FROM gtest3 ORDER BY a;
+-- COPY JSON should exclude generated columns, same as text/CSV
+COPY gtest1 TO stdout WITH (FORMAT json);
+
-- null values
CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
INSERT INTO gtest2 VALUES (1);
[parent: 3e2a1496bae6]