From 2f79b0179437bd18c2aaa550f71c80e477fcc0a7 Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Wed, 21 Oct 2026 10:44:06 -0700 Subject: [PATCH] Reject COPY TO of unlogged tables during recovery. Unlogged tables have no data on a standby, so reading one there must fail. SELECT and COPY (SELECT ...) TO already report a clear error, but COPY with a table name read the table directly and failed with a low-level error instead: ERROR: could not open file "base/5/16384": No such file or directory Check for this case in COPY TO and report the same error as SELECT. On v19 and later, also check each partition when copying from a partitioned table, so that an unlogged partition is reported too. Backpatch to all supported versions, since the low-level error looks like data corruption. Author: Jim Jones Reviewed-by: Zsolt Parragi Reviewed-by: Masahiko Sawada Discussion: https://postgr.es/m/b6d71815-d362-4579-bf07-73eca01aee6d@uni-muenster.de Backpatch-through: 14 --- src/backend/commands/copyto.c | 19 +++++++++++++++++++ src/test/recovery/t/001_stream_rep.pl | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index 667bd750ab5..33920bfd04e 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -21,6 +21,7 @@ #include "access/table.h" #include "access/tableam.h" #include "access/tupconvert.h" +#include "access/xlog.h" #include "catalog/pg_inherits.h" #include "commands/copyapi.h" #include "commands/progress.h" @@ -877,6 +878,19 @@ BeginCopyTo(ParseState *pstate, errhint("Try the COPY (SELECT ...) TO variant.")); } + /* Unlogged partitions are inaccessible during recovery. */ + if (RecoveryInProgress() && + get_rel_persistence(child) == RELPERSISTENCE_UNLOGGED) + { + char *relation_name = get_rel_name(child); + + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary or unlogged relations during recovery"), + errdetail("Partition \"%s\" is an unlogged table in partitioned table \"%s\".", + relation_name, RelationGetRelationName(rel))); + } + /* Exclude tables with no data */ if (RELKIND_HAS_PARTITIONS(relkind)) children = foreach_delete_current(children, child); @@ -889,6 +903,11 @@ BeginCopyTo(ParseState *pstate, RelationGetRelationName(rel)))); } + /* Temporary and unlogged relations are inaccessible during recovery. */ + if (rel != NULL && !RelationIsPermanent(rel) && RecoveryInProgress()) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary or unlogged relations during recovery"))); /* Allocate workspace and zero all fields */ cstate = palloc0_object(CopyToStateData); diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl index a4fa4b96c61..528ef853ca7 100644 --- a/src/test/recovery/t/001_stream_rep.pl +++ b/src/test/recovery/t/001_stream_rep.pl @@ -113,6 +113,33 @@ is( $node_standby_1->safe_psql( 't', 'pg_sequence_last_value() on unlogged sequence on standby 1'); +# Check that COPY TO rejects unlogged tables on standby, including when +# reached through a partitioned table, but still copies permanent ones +$node_primary->safe_psql( + 'postgres', q{ +CREATE UNLOGGED TABLE ultab (a int); +CREATE TABLE ulpart (a int) PARTITION BY RANGE (a); +CREATE UNLOGGED TABLE ulpart_1 PARTITION OF ulpart FOR VALUES FROM (0) TO (10); +CREATE TABLE lpart (a int) PARTITION BY RANGE (a); +CREATE TABLE lpart_1 PARTITION OF lpart FOR VALUES FROM (0) TO (10); +INSERT INTO lpart VALUES (1); +}); +$node_primary->wait_for_replay_catchup($node_standby_1); +my ($copy_ret, $copy_stdout, $copy_stderr) = + $node_standby_1->psql('postgres', 'COPY ultab TO STDOUT'); +like( + $copy_stderr, + qr/cannot access temporary or unlogged relations during recovery/, + 'COPY TO of unlogged table on standby 1'); +($copy_ret, $copy_stdout, $copy_stderr) = + $node_standby_1->psql('postgres', 'COPY ulpart TO STDOUT'); +like( + $copy_stderr, + qr/cannot access temporary or unlogged relations during recovery.*\n.*DETAIL: Partition "ulpart_1" is an unlogged table in partitioned table "ulpart"/, + 'COPY TO of partitioned table with unlogged partition on standby 1'); +is($node_standby_1->safe_psql('postgres', 'COPY lpart TO STDOUT'), + '1', 'COPY TO of partitioned table on standby 1'); + # Check that only READ-only queries can run on standbys is($node_standby_1->psql('postgres', 'INSERT INTO tab_int VALUES (1)'), 3, 'read-only queries on standby 1');