Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions doc/src/sgml/ref/pg_dump.sgml
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,40 @@ PostgreSQL documentation
</listitem>
</varlistentry>

<varlistentry>
<term><option>--create-empty-files-for-excluded-data</option></term>
<listitem>
<para>
When used together with <option>--exclude-table-data</option> or
<option>--exclude-table-data-and-children</option> in directory output
format (<option>-Fd</option> or <option>--format=directory</option>),
still create a <literal>TABLE DATA</literal> archive entry (including
the usual <command>COPY</command> statement) for each excluded table,
but do not dump the table's rows. A data file named after the table's
dump ID (for example <filename>3541.dat</filename>) is created
containing only a <literal>\.</literal> COPY end marker as a
placeholder.
</para>
<para>
This option is intended for workflows where excluded table data is
loaded separately after the dump is taken, for example by replacing
the placeholder data file with externally produced data before
restore.
</para>
<para>
<option>--create-empty-files-for-excluded-data</option> cannot be used
without <option>--exclude-table-data</option> or
<option>--exclude-table-data-and-children</option>. It is only
supported when directory output format is selected
(<option>-Fd</option> or <option>--format=directory</option>) and
data is being dumped as <command>COPY</command> (the default).
It cannot be used with <option>--inserts</option>,
<option>--column-inserts</option>, or
<option>--rows-per-insert</option>.
</para>
</listitem>
</varlistentry>

<varlistentry>
<term><option>--disable-dollar-quoting</option></term>
<listitem>
Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_dump/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ tests += {
't/006_pg_dump_compress.pl',
't/007_pg_dumpall.pl',
't/010_dump_connstr.pl',
't/012_pg_dump_empty_excluded_data.pl',
],
},
}
Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_dump/pg_backup.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ typedef struct _dumpOptions
int use_setsessauth;
int enable_row_security;
int load_via_partition_root;
bool create_empty_files_for_excluded_data;

/* default, if no "inclusion" switches appear, is to dump everything */
bool include_everything;
Expand Down
64 changes: 60 additions & 4 deletions src/bin/pg_dump/pg_dump.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#include "getopt_long.h"
#include "libpq/libpq-fs.h"
#include "parallel.h"
#include "pg_backup_archiver.h"
#include "pg_backup_db.h"
#include "pg_backup_utils.h"
#include "pg_dump.h"
Expand Down Expand Up @@ -493,6 +494,7 @@ main(int argc, char **argv)
{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
{"column-inserts", no_argument, &dopt.column_inserts, 1},
{"create-empty-files-for-excluded-data", no_argument, NULL, 26},
{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
Expand Down Expand Up @@ -799,6 +801,10 @@ main(int argc, char **argv)
dopt.restrict_key = pg_strdup(optarg);
break;

case 26:
dopt.create_empty_files_for_excluded_data = true;
break;

default:
/* getopt_long already emitted a complaint */
pg_log_error_hint("Try \"%s --help\" for more information.", progname);
Expand Down Expand Up @@ -886,9 +892,27 @@ main(int argc, char **argv)
"--on-conflict-do-nothing",
"--inserts", "--rows-per-insert", "--column-inserts");

if (dopt.create_empty_files_for_excluded_data &&
tabledata_exclude_patterns.head == NULL &&
tabledata_exclude_patterns_and_children.head == NULL)
pg_fatal("option %s requires option %s or %s",
"--create-empty-files-for-excluded-data",
"--exclude-table-data", "--exclude-table-data-and-children");

if (dopt.create_empty_files_for_excluded_data &&
dopt.dump_inserts != 0)
pg_fatal("option %s cannot be used with %s, %s, or %s",
"--create-empty-files-for-excluded-data",
"--inserts", "--column-inserts", "--rows-per-insert");

/* Identify archive format to emit */
archiveFormat = parseArchiveFormat(format, &archiveMode);

if (dopt.create_empty_files_for_excluded_data &&
archiveFormat != archDirectory)
pg_fatal("option %s is only supported by the directory format",
"--create-empty-files-for-excluded-data");

/* archiveFormat specific setup */
if (archiveFormat == archNull)
{
Expand Down Expand Up @@ -1329,6 +1353,10 @@ help(const char *progname)
printf(_(" -x, --no-privileges do not dump privileges (grant/revoke)\n"));
printf(_(" --binary-upgrade for use by upgrade utilities only\n"));
printf(_(" --column-inserts dump data as INSERT commands with column names\n"));
printf(_(" --create-empty-files-for-excluded-data\n"
" create empty data files for tables excluded\n"
" with --exclude-table-data (directory\n"
" format and COPY data only)\n"));
printf(_(" --disable-dollar-quoting disable dollar quoting, use SQL standard quoting\n"));
printf(_(" --disable-triggers disable triggers during data-only restore\n"));
printf(_(" --enable-row-security enable row security (dump only content user has\n"
Expand Down Expand Up @@ -2355,6 +2383,29 @@ selectDumpableObject(DumpableObject *dobj, Archive *fout)
DUMP_COMPONENT_ALL : DUMP_COMPONENT_NONE;
}

/*
* Dump an empty data file for a table whose data was excluded with
* --exclude-table-data but --create-empty-files-for-excluded-data was set.
*/
static int
dumpTableData_empty(Archive *fout, const void *dcontext)
{
const TableDataInfo *tdinfo = dcontext;
const TableInfo *tbinfo = tdinfo->tdtable;

pg_log_info("creating empty data file for excluded table \"%s.%s\"",
tbinfo->dobj.namespace->dobj.name, tbinfo->dobj.name);

/*
* Emit the COPY end marker, as dumpTableData_copy() does for an empty
* table. Archive formats store raw COPY data in separate blobs/files.
*/
if (fout->dopt->dump_inserts == 0)
archprintf(fout, "\\.\n\n\n");

return 1;
}

/*
* Dump a table's contents for loading using the COPY command
* - this routine is called by the Archiver when it wants the table
Expand Down Expand Up @@ -2895,7 +2946,8 @@ dumpTableData(Archive *fout, const TableDataInfo *tdinfo)
if (dopt->dump_inserts == 0)
{
/* Dump/restore using COPY */
dumpFn = dumpTableData_copy;
dumpFn = tdinfo->emptyExcludedData ?
dumpTableData_empty : dumpTableData_copy;
/* must use 2 steps here 'cause fmtId is nonreentrant */
printfPQExpBuffer(copyBuf, "COPY %s ",
copyFrom);
Expand All @@ -2906,7 +2958,8 @@ dumpTableData(Archive *fout, const TableDataInfo *tdinfo)
else
{
/* Restore using INSERT */
dumpFn = dumpTableData_insert;
dumpFn = tdinfo->emptyExcludedData ?
dumpTableData_empty : dumpTableData_insert;
copyStmt = NULL;
}

Expand Down Expand Up @@ -3026,6 +3079,7 @@ static void
makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
{
TableDataInfo *tdinfo;
bool data_excluded;

/*
* Nothing to do if we already decided to dump the table. This will
Expand Down Expand Up @@ -3056,8 +3110,9 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
return;

/* Check that the data is not explicitly excluded */
if (simple_oid_list_member(&tabledata_exclude_oids,
tbinfo->dobj.catId.oid))
data_excluded = simple_oid_list_member(&tabledata_exclude_oids,
tbinfo->dobj.catId.oid);
if (data_excluded && !dopt->create_empty_files_for_excluded_data)
return;

/* OK, let's dump it */
Expand All @@ -3081,6 +3136,7 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
tdinfo->dobj.namespace = tbinfo->dobj.namespace;
tdinfo->tdtable = tbinfo;
tdinfo->filtercond = NULL; /* might get set later */
tdinfo->emptyExcludedData = data_excluded;
addObjectDependency(&tdinfo->dobj, tbinfo->dobj.dumpId);

/* A TableDataInfo contains data, of course */
Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_dump/pg_dump.h
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ typedef struct _tableDataInfo
DumpableObject dobj;
TableInfo *tdtable; /* link to table to dump */
char *filtercond; /* WHERE condition to limit rows dumped */
bool emptyExcludedData; /* excluded by --exclude-table-data */
} TableDataInfo;

typedef struct _indxInfo
Expand Down
141 changes: 141 additions & 0 deletions src/bin/pg_dump/t/012_pg_dump_empty_excluded_data.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@

# Copyright (c) 2026, PostgreSQL Global Development Group

use strict;
use warnings FATAL => 'all';

use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;

my $tempdir = PostgreSQL::Test::Utils::tempdir;

my $node = PostgreSQL::Test::Cluster->new('main');
$node->init;
$node->start;

my $src_db = 'empty_excl_src';
my $dst_db = 'empty_excl_dst';
my $dumpdir = "$tempdir/empty_excl_dump";

$node->safe_psql(
'postgres',
qq{CREATE DATABASE $src_db;
\\c $src_db
CREATE TABLE keep_data(id int);
CREATE TABLE skip_data(id int);
INSERT INTO keep_data VALUES (1), (2);
INSERT INTO skip_data VALUES (10), (20), (30);});

# Flag without --exclude-table-data must fail.
$node->command_fails(
[
'pg_dump',
'--no-sync',
'--format' => 'directory',
'--file' => "$tempdir/bad_dump",
'--create-empty-files-for-excluded-data',
$node->connstr($src_db),
],
'create-empty-files-for-excluded-data requires exclude-table-data');

# Flag requires directory output format.
$node->command_fails_like(
[
'pg_dump',
'--no-sync',
'--format' => 'custom',
'--file' => "$tempdir/bad_custom.dump",
'--exclude-table-data' => 'skip_data',
'--create-empty-files-for-excluded-data',
$node->connstr($src_db),
],
qr/create-empty-files-for-excluded-data.*only supported by the directory format/,
'create-empty-files-for-excluded-data requires directory format');

# Flag requires COPY-format data, not INSERT output.
my @incompatible_opts = (
{ label => 'inserts', extra => [ '--inserts' ] },
{ label => 'column-inserts', extra => [ '--column-inserts' ] },
{ label => 'rows-per-insert', extra => [ '--rows-per-insert' => 10 ] },
);
for my $case (@incompatible_opts)
{
$node->command_fails_like(
[
'pg_dump',
'--no-sync',
'--format' => 'directory',
'--file' => "$tempdir/bad_$case->{label}",
'--exclude-table-data' => 'skip_data',
'--create-empty-files-for-excluded-data',
@{ $case->{extra} },
$node->connstr($src_db),
],
qr/create-empty-files-for-excluded-data.*cannot be used with/,
"create-empty-files-for-excluded-data rejects $case->{label}");
}

$node->command_ok(
[
'pg_dump',
'--no-sync',
'--format' => 'directory',
'--compress' => 'none',
'--file' => $dumpdir,
'--exclude-table-data' => 'skip_data',
'--create-empty-files-for-excluded-data',
$node->connstr($src_db),
],
'directory dump with empty excluded table data files');

$node->command_like(
[ 'pg_restore', '--list', $dumpdir ],
qr/TABLE DATA public skip_data/,
'TOC lists TABLE DATA for excluded table');

my ($stdout, $stderr) = run_command([ 'pg_restore', '--list', $dumpdir ]);
my $skip_dumpid;
foreach my $line (split /\n/, $stdout)
{
if ($line =~ /TABLE DATA public skip_data/ && $line =~ /^(\d+);/)
{
$skip_dumpid = $1;
last;
}
}
ok(defined $skip_dumpid, 'found dump ID for excluded table');
like(
slurp_file("$dumpdir/${skip_dumpid}.dat"),
qr/^\\\.\n/,
'excluded table data file contains COPY end marker only')
if defined $skip_dumpid;

my @datfiles = grep { $_ !~ /\/toc\.dat$/ } glob("$dumpdir/*.dat");
cmp_ok(scalar(@datfiles), '==', 2, 'two table data files in dump');

my ($keep_dat) = grep { $_ ne "$dumpdir/${skip_dumpid}.dat" } @datfiles;
ok(defined $keep_dat && -s $keep_dat > 0,
'included table has a non-empty data file')
if defined $skip_dumpid;

$node->safe_psql('postgres', "CREATE DATABASE $dst_db");

$node->command_ok(
[
'pg_restore',
'--dbname' => $node->connstr($dst_db),
$dumpdir,
],
'restore dump with empty excluded data file');

is(
$node->safe_psql($dst_db, 'SELECT count(*) FROM keep_data'),
'2',
'included table data restored');
is(
$node->safe_psql($dst_db, 'SELECT count(*) FROM skip_data'),
'0',
'excluded table restored with no rows');

done_testing();