From 935612389741b593c6c5e75162af733a57738dc3 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Sat, 10 Oct 2026 10:47:49 +0000 Subject: [PATCH v1] pg_dump: Fix restore of empty zstd and LZ4 data files Commit e01fc6315f1 made pg_restore reject zstd and LZ4 streams which end before the final frame is complete. The compressed file handles only start a frame on the first write however, so a data file which is never written to is left empty. This is the case for every empty large object in a directory format dump, and such dumps could no longer be restored. Fix by treating a stream with no input at all as complete. The writer is left as is since dumps with empty data files already exist and must remain restorable. Backpatch to v19 like the erroneous commit. Author: Zsolt Parragi Discussion: https://postgr.es/m/5962B878-C43D-4EBC-9E95-1F945CE5E586@gmail.com Backpatch-through: 19 --- src/bin/pg_dump/compress_lz4.c | 6 ++++++ src/bin/pg_dump/compress_zstd.c | 6 ++++++ src/bin/pg_dump/t/006_pg_dump_compress.pl | 24 +++++++++++++++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c index a4ae1a4272f..4cebfa2e1bf 100644 --- a/src/bin/pg_dump/compress_lz4.c +++ b/src/bin/pg_dump/compress_lz4.c @@ -401,6 +401,12 @@ LZ4Stream_init(LZ4State *state, bool compressing) state->buffer = pg_malloc(state->buflen); state->outbuflen = DEFAULT_IO_BUFFER_SIZE; state->outbuf = pg_malloc(state->outbuflen); + + /* + * An empty file is a valid empty stream: the writer creates one when + * nothing is written, e.g. for an empty large object. + */ + state->frame_finished = true; } state->inited = true; diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c index 22e6096c367..a480f6aa363 100644 --- a/src/bin/pg_dump/compress_zstd.c +++ b/src/bin/pg_dump/compress_zstd.c @@ -310,6 +310,12 @@ Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on pg_fatal("could not initialize compression library"); return -1; } + + /* + * An empty file is a valid empty stream: the writer creates one when + * nothing is written, e.g. for an empty large object. + */ + zstdcs->frame_finished = true; } output->size = size; diff --git a/src/bin/pg_dump/t/006_pg_dump_compress.pl b/src/bin/pg_dump/t/006_pg_dump_compress.pl index 32475c05c18..686e68f46ba 100644 --- a/src/bin/pg_dump/t/006_pg_dump_compress.pl +++ b/src/bin/pg_dump/t/006_pg_dump_compress.pl @@ -471,6 +471,17 @@ my %tests = ( \QSELECT pg_catalog.lo_close(0);\E /xm, like => { %full_runs, }, + }, + + # An empty large object leaves an empty data file in directory format + 'LO create (empty)' => { + create_order => 51, + create_sql => 'SELECT pg_catalog.lo_create(0);', + regexp => qr/^ + \QSELECT pg_catalog.lo_open\E \('\d+',\ \d+\);\n + \QSELECT pg_catalog.lo_close(0);\E + /xm, + like => { %full_runs, }, },); ######################################### @@ -710,14 +721,23 @@ for my $method (qw(lz4 zstd)) : pack('H*', '28b52ffd'); my $custom_path = "$tempdir/compression_${method}_custom.dump"; my $custom_bad_path = "$tempdir/${method}_custom_bad.dump"; - my ($directory_path) = + my ($directory_path) = grep { -s $_ } glob("$tempdir/compression_${method}_dir/*.dat.$extension"); SKIP: { - skip "$method compression not supported by this build", 2 + skip "$method compression not supported by this build", 3 if !$supported; + # The empty large object is stored as an empty data file + $node->command_ok( + [ + 'pg_restore', + '--file' => "$tempdir/${method}_directory_good.sql", + "$tempdir/compression_${method}_dir" + ], + "$method directory archive with empty data file restores"); + copy($custom_path, $custom_bad_path) or die "could not copy $custom_path: $!"; truncate_custom_compressed_data($custom_bad_path, $magic, 0); -- 2.55.0