Hi -hackers,

>From time to time we hit some corruption issue and usually we end up checking
for corruption with COPY to /dev/null and/or with verify_heapam(). Both seem
to detect different kind of corruption types, so I'm assuming they are somehow
complementary (e.g. seems that COPY is slower in cached case, but exercises
TOAST way harder than the amcheck routing even with check_toast=>true). Also
there's usecase that we often ask people to 'just run pg_dump -j', but that
requires -Fd <dir> which then requires plenty of disk space if you want to
verify whole DB in parallel, you need plenty of space (which is unrealistic on
big installations)

I was thining if we could add COPY <t> TO BLACKHOLE, so we could get rid of
those two limitations / inefficencies. When starting I was hoping for more than
11-15% runtime optimization (see below), but at least it visible and bigger
benefit seems to be coming from being able to do something like:
   `pg_dump -j <N> -Fp -f /dev/null`
which today is impossible today due to:
   pg_dump: error: parallel backup only supported by the directory format

0002 allows to do: `pg_dump -j <N> -Fb` and generates no output(if no errors)
and takes no space.

-- hot:
postgres=# COPY pgbench_accounts to '/dev/null';
COPY 10000000
Time: 1576.752 ms (00:01.577)
postgres=# COPY pgbench_accounts to '/dev/null';
COPY 10000000
Time: 1539.565 ms (00:01.540)
postgres=# COPY pgbench_accounts to '/dev/null';
COPY 10000000
Time: 1587.900 ms (00:01.588)

postgres=# COPY pgbench_accounts to blackhole;
COPY 10000000
Time: 1365.206 ms (00:01.365)
postgres=# COPY pgbench_accounts to blackhole;
COPY 10000000
Time: 1370.007 ms (00:01.370)
postgres=# COPY pgbench_accounts to blackhole;
COPY 10000000
Time: 1367.661 ms (00:01.368)
postgres=#

so ~1.14x

-- cold (after 3 to drop_caches sysctl + buffercache_evict_all):
postgres=# select * from  verify_heapam('pgbench_accounts',
check_toast => true);
[..]
Time: 1747.927 ms (00:01.748)

-- cold (after 3 to drop_caches sysctl + buffercache_evict_all):
postgres=# COPY pgbench_accounts to blackhole;
COPY 10000000
Time: 1429.400 ms (00:01.429)

-- cold (after 3 to drop_caches sysctl + buffercache_evict_all):
postgres=# COPY pgbench_accounts to '/dev/null';
COPY 10000000
Time: 1600.803 ms (00:01.601)

yields ~1.11x

Patch attached, no docs yet there, as I'm not sure community finds it useful.

-J.
From 461b88423d629bd2dc4175a1bcded75b78a80a60 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Tue, 24 Mar 2026 07:42:58 +0100
Subject: [PATCH v1 1/2] Add "COPY TO BLACKHOLE"

Add BLACKHOLE COPY mode which saves some CPU cycles when not outputting
data anywhere which is helpful when trying to assess if relation(s) are
free of corruption. Follow-up commit will teach pg_dump to use this
in it's parallel (-j) pg_dump mode.

Author: Jakub Wartak <[email protected]>
Reviewed-by:
Discussion:
---
 src/backend/commands/copy.c                   |  6 +++++
 src/backend/commands/copyto.c                 | 24 +++++++++++++++--
 src/backend/parser/gram.y                     | 27 ++++++++++++++++---
 src/include/commands/copy.h                   |  1 +
 src/include/commands/progress.h               |  1 +
 src/include/nodes/parsenodes.h                |  1 +
 src/include/parser/kwlist.h                   |  1 +
 .../test_copy_callbacks/test_copy_callbacks.c |  2 +-
 src/test/regress/expected/copy.out            |  9 +++++++
 src/test/regress/sql/copy.sql                 |  9 +++++++
 10 files changed, 75 insertions(+), 6 deletions(-)

diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 003b70852bb..3090fa04433 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -110,6 +110,11 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		}
 	}
 
+	if (stmt->is_blackhole && is_from)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("COPY FROM BLACKHOLE is not supported")));
+
 	if (stmt->relation)
 	{
 		LOCKMODE	lockmode = is_from ? RowExclusiveLock : AccessShareLock;
@@ -376,6 +381,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 
 		cstate = BeginCopyTo(pstate, rel, query, relid,
 							 stmt->filename, stmt->is_program,
+							 stmt->is_blackhole,
 							 NULL, stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index ffed63a2986..6dba2d712a0 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -51,6 +51,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_FRONTEND,				/* to frontend */
 	COPY_CALLBACK,				/* to callback function */
+	COPY_BLACKHOLE,				/* to nowhere */
 } CopyDest;
 
 /*
@@ -88,6 +89,7 @@ typedef struct CopyToStateData
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
+	bool		is_blackhole;	/* is destination BLACKHOLE? */
 	bool		json_row_delim_needed;	/* need delimiter before next row */
 	StringInfo	json_buf;		/* reusable buffer for JSON output,
 								 * initialized in BeginCopyTo */
@@ -631,6 +633,8 @@ CopySendEndOfRow(CopyToState cstate)
 		case COPY_CALLBACK:
 			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_BLACKHOLE:
+			break;
 	}
 
 	/* Update the progress */
@@ -772,6 +776,7 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			bool is_blackhole,
 			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
@@ -1118,8 +1123,14 @@ BeginCopyTo(ParseState *pstate,
 	cstate->encoding_embeds_ascii = PG_ENCODING_IS_CLIENT_ONLY(cstate->file_encoding);
 
 	cstate->copy_dest = COPY_FILE;	/* default */
+	cstate->is_blackhole = is_blackhole;
 
-	if (data_dest_cb)
+	if (is_blackhole)
+	{
+		progress_vals[1] = PROGRESS_COPY_TYPE_BLACKHOLE;
+		cstate->copy_dest = COPY_BLACKHOLE;
+	}
+	else if (data_dest_cb)
 	{
 		progress_vals[1] = PROGRESS_COPY_TYPE_CALLBACK;
 		cstate->copy_dest = COPY_CALLBACK;
@@ -1240,7 +1251,7 @@ EndCopyTo(CopyToState cstate)
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL && cstate->data_dest_cb == NULL);
+	bool		pipe = (cstate->filename == NULL && cstate->data_dest_cb == NULL && !cstate->is_blackhole);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
@@ -1249,6 +1260,15 @@ DoCopyTo(CopyToState cstate)
 
 	if (fe_copy)
 		SendCopyBegin(cstate);
+	else if (cstate->is_blackhole && whereToSendOutput == DestRemote)
+	{
+		/*
+		 * If we are in a blackhole copy from the frontend, we don't send a
+		 * CopyBegin message, but we still need to make sure psql or other
+		 * clients don't hang waiting for it. If we don't send CopyBegin,
+		 * client will just see the CommandComplete message later.
+		 */
+	}
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..faaa92b5742 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -747,7 +747,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	AGGREGATE ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY ARRAY AS ASC
 	ASENSITIVE ASSERTION ASSIGNMENT ASYMMETRIC ATOMIC AT ATTACH ATTRIBUTE AUTHORIZATION
 
-	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
+	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT BLACKHOLE
 	BOOLEAN_P BOTH BREADTH BY
 
 	CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
@@ -3558,7 +3558,16 @@ CopyStmt:	COPY opt_binary qualified_name opt_column_list
 					n->attlist = $4;
 					n->is_from = $5;
 					n->is_program = $6;
-					n->filename = $7;
+					if ($7 == (char *) -1)
+					{
+						n->filename = NULL;
+						n->is_blackhole = true;
+					}
+					else
+					{
+						n->filename = $7;
+						n->is_blackhole = false;
+					}
 					n->whereClause = $11;
 
 					if (n->is_program && n->filename == NULL)
@@ -3593,7 +3602,16 @@ CopyStmt:	COPY opt_binary qualified_name opt_column_list
 					n->attlist = NIL;
 					n->is_from = false;
 					n->is_program = $6;
-					n->filename = $7;
+					if ($7 == (char *) -1)
+					{
+						n->filename = NULL;
+						n->is_blackhole = true;
+					}
+					else
+					{
+						n->filename = $7;
+						n->is_blackhole = false;
+					}
 					n->options = $9;
 
 					if (n->is_program && n->filename == NULL)
@@ -3625,6 +3643,7 @@ copy_file_name:
 			Sconst									{ $$ = $1; }
 			| STDIN									{ $$ = NULL; }
 			| STDOUT								{ $$ = NULL; }
+			| BLACKHOLE								{ $$ = (char *) -1; }
 		;
 
 copy_options: copy_opt_list							{ $$ = $1; }
@@ -18843,6 +18862,7 @@ unreserved_keyword:
 			| BACKWARD
 			| BEFORE
 			| BEGIN_P
+			| BLACKHOLE
 			| BREADTH
 			| BY
 			| CACHE
@@ -19413,6 +19433,7 @@ bare_label_keyword:
 			| BIGINT
 			| BINARY
 			| BIT
+			| BLACKHOLE
 			| BOOLEAN_P
 			| BOTH
 			| BREADTH
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index abecfe51098..4ecb387a929 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -131,6 +131,7 @@ extern DestReceiver *CreateCopyDestReceiver(void);
  */
 extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *raw_query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   bool is_blackhole,
 							   copy_data_dest_cb data_dest_cb, List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 2a12920c75f..dce5a87e84b 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -187,6 +187,7 @@
 #define PROGRESS_COPY_TYPE_PROGRAM 2
 #define PROGRESS_COPY_TYPE_PIPE 3
 #define PROGRESS_COPY_TYPE_CALLBACK 4
+#define PROGRESS_COPY_TYPE_BLACKHOLE 5
 
 /* Progress parameters for PROGRESS_DATACHECKSUMS */
 #define PROGRESS_DATACHECKSUMS_PHASE		0
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 91377a6cde3..f9adc99a4dd 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2786,6 +2786,7 @@ typedef struct CopyStmt
 								 * for all columns */
 	bool		is_from;		/* TO or FROM */
 	bool		is_program;		/* is 'filename' a program to popen? */
+	bool		is_blackhole;	/* is 'filename' BLACKHOLE? */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	List	   *options;		/* List of DefElem nodes */
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 51ead54f015..e569def836d 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -61,6 +61,7 @@ PG_KEYWORD("between", BETWEEN, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("bigint", BIGINT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("binary", BINARY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("bit", BIT, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("blackhole", BLACKHOLE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("boolean", BOOLEAN_P, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("both", BOTH, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("breadth", BREADTH, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/modules/test_copy_callbacks/test_copy_callbacks.c b/src/test/modules/test_copy_callbacks/test_copy_callbacks.c
index f6b113e3e98..e79b94f6cb8 100644
--- a/src/test/modules/test_copy_callbacks/test_copy_callbacks.c
+++ b/src/test/modules/test_copy_callbacks/test_copy_callbacks.c
@@ -37,7 +37,7 @@ test_copy_to_callback(PG_FUNCTION_ARGS)
 	CopyToState cstate;
 	int64		processed;
 
-	cstate = BeginCopyTo(NULL, rel, NULL, RelationGetRelid(rel), NULL, false,
+	cstate = BeginCopyTo(NULL, rel, NULL, RelationGetRelid(rel), NULL, false, false,
 						 to_cb, NIL, NIL);
 	processed = DoCopyTo(cstate);
 	EndCopyTo(cstate);
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 37498cdd6e7..0a44205bf88 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -605,3 +605,12 @@ id	val
 1	11
 2	12
 DROP TABLE pp_dropcol;
+-- Test COPY TO BLACKHOLE
+CREATE TABLE copy_blackhole_test (a int, b text);
+INSERT INTO copy_blackhole_test SELECT g, 'text' || g FROM generate_series(1, 10) g;
+COPY copy_blackhole_test TO BLACKHOLE;
+COPY (SELECT * FROM copy_blackhole_test) TO BLACKHOLE;
+-- COPY FROM BLACKHOLE should fail
+COPY copy_blackhole_test FROM BLACKHOLE;
+ERROR:  COPY FROM BLACKHOLE is not supported
+DROP TABLE copy_blackhole_test;
diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql
index 094fd76c12b..16edb97db73 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -544,3 +544,12 @@ ALTER TABLE pp_dropcol ATTACH PARTITION pp_dropcol_1 FOR VALUES FROM (1) TO (10)
 INSERT INTO pp_dropcol VALUES (1, 11), (2, 12);
 COPY pp_dropcol TO stdout(header);
 DROP TABLE pp_dropcol;
+
+-- Test COPY TO BLACKHOLE
+CREATE TABLE copy_blackhole_test (a int, b text);
+INSERT INTO copy_blackhole_test SELECT g, 'text' || g FROM generate_series(1, 10) g;
+COPY copy_blackhole_test TO BLACKHOLE;
+COPY (SELECT * FROM copy_blackhole_test) TO BLACKHOLE;
+-- COPY FROM BLACKHOLE should fail
+COPY copy_blackhole_test FROM BLACKHOLE;
+DROP TABLE copy_blackhole_test;
-- 
2.43.0

From e161270b1db1b5206e2d5b560de38eebf2f99560 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Fri, 24 Apr 2026 13:09:01 +0200
Subject: [PATCH v1 2/2] Add BLACKHOLE destination format in pg_dump (-Fb)

Enable BLACKHOLE in pg_dump along with parlallel mode. This allows
for a parallelized fast logical check of the whole database if all of
the relations are readable. In addition it does not require the free
disk space to write output files, contrary to the directory format.

Author: Jakub Wartak <[email protected]>
Reviewed-by:
Discussion:
---
 src/bin/pg_dump/Makefile              |   1 +
 src/bin/pg_dump/meson.build           |   1 +
 src/bin/pg_dump/pg_backup.h           |   1 +
 src/bin/pg_dump/pg_backup_archiver.c  |   4 +
 src/bin/pg_dump/pg_backup_archiver.h  |   1 +
 src/bin/pg_dump/pg_backup_blackhole.c | 185 ++++++++++++++++++++++++++
 src/bin/pg_dump/pg_dump.c             |  49 +++++--
 7 files changed, 234 insertions(+), 8 deletions(-)
 create mode 100644 src/bin/pg_dump/pg_backup_blackhole.c

diff --git a/src/bin/pg_dump/Makefile b/src/bin/pg_dump/Makefile
index 79073b0a0ea..d38a2dd7eba 100644
--- a/src/bin/pg_dump/Makefile
+++ b/src/bin/pg_dump/Makefile
@@ -36,6 +36,7 @@ OBJS = \
 	filter.o \
 	parallel.o \
 	pg_backup_archiver.o \
+	pg_backup_blackhole.o \
 	pg_backup_custom.o \
 	pg_backup_db.o \
 	pg_backup_directory.o \
diff --git a/src/bin/pg_dump/meson.build b/src/bin/pg_dump/meson.build
index 7c9a475963b..82786b4b8bc 100644
--- a/src/bin/pg_dump/meson.build
+++ b/src/bin/pg_dump/meson.build
@@ -11,6 +11,7 @@ pg_dump_common_sources = files(
   'filter.c',
   'parallel.c',
   'pg_backup_archiver.c',
+  'pg_backup_blackhole.c',
   'pg_backup_custom.c',
   'pg_backup_db.c',
   'pg_backup_directory.c',
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 28e7ff6fa16..0c5c5bd34a2 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -43,6 +43,7 @@ typedef enum _archiveFormat
 	archTar = 3,
 	archNull = 4,
 	archDirectory = 5,
+	archBlackhole = 6,
 } ArchiveFormat;
 
 typedef enum _archiveMode
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 2fd773ad84f..2dd7ea88913 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2505,6 +2505,10 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt,
 			InitArchiveFmt_Tar(AH);
 			break;
 
+		case archBlackhole:
+			InitArchiveFmt_Blackhole(AH);
+			break;
+
 		default:
 			pg_fatal("unrecognized file format \"%d\"", AH->format);
 	}
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index 1218bf6a6a1..cbad5d7cb69 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -464,6 +464,7 @@ extern void InitArchiveFmt_Custom(ArchiveHandle *AH);
 extern void InitArchiveFmt_Null(ArchiveHandle *AH);
 extern void InitArchiveFmt_Directory(ArchiveHandle *AH);
 extern void InitArchiveFmt_Tar(ArchiveHandle *AH);
+extern void InitArchiveFmt_Blackhole(ArchiveHandle *AH);
 
 extern void ReconnectToServer(ArchiveHandle *AH, const char *dbname);
 extern void IssueCommandPerBlob(ArchiveHandle *AH, TocEntry *te,
diff --git a/src/bin/pg_dump/pg_backup_blackhole.c b/src/bin/pg_dump/pg_backup_blackhole.c
new file mode 100644
index 00000000000..5b596dbb250
--- /dev/null
+++ b/src/bin/pg_dump/pg_backup_blackhole.c
@@ -0,0 +1,185 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_backup_blackhole.c
+ *
+ *	Implementation of an archive that is never saved and never outputs anything.
+ *	It is used by pg_dump to execute COPY TO BLACKHOLE commands.
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_dump/pg_backup_blackhole.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include "parallel.h"
+#include "pg_backup_archiver.h"
+#include "pg_backup_utils.h"
+
+static int	_WorkerJobDumpDirectory(ArchiveHandle *AH, TocEntry *te);
+static void _ArchiveEntry(ArchiveHandle *AH, TocEntry *te);
+static void _StartData(ArchiveHandle *AH, TocEntry *te);
+static void _Clone(ArchiveHandle *AH);
+static void _DeClone(ArchiveHandle *AH);
+static void _WriteData(ArchiveHandle *AH, const void *data, size_t dLen);
+static void _EndData(ArchiveHandle *AH, TocEntry *te);
+static int	_WriteByte(ArchiveHandle *AH, const int i);
+static void _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len);
+static void _CloseArchive(ArchiveHandle *AH);
+static void _PrintTocData(ArchiveHandle *AH, TocEntry *te);
+static void _StartLOs(ArchiveHandle *AH, TocEntry *te);
+static void _StartLO(ArchiveHandle *AH, TocEntry *te, Oid oid);
+static void _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid);
+static void _EndLOs(ArchiveHandle *AH, TocEntry *te);
+
+/*
+ *	Initializer
+ */
+void
+InitArchiveFmt_Blackhole(ArchiveHandle *AH)
+{
+	/* Assuming static functions, this can be copied for each format. */
+	AH->ArchiveEntryPtr = _ArchiveEntry;
+	AH->StartDataPtr = _StartData;
+
+	AH->WriteDataPtr = _WriteData;
+	AH->EndDataPtr = _EndData;
+	AH->WriteBytePtr = _WriteByte;
+	AH->WriteBufPtr = _WriteBuf;
+	AH->ClosePtr = _CloseArchive;
+	AH->ReopenPtr = NULL;
+	AH->PrintTocDataPtr = _PrintTocData;
+
+	AH->StartLOsPtr = _StartLOs;
+	AH->StartLOPtr = _StartLO;
+	AH->EndLOPtr = _EndLO;
+	AH->EndLOsPtr = _EndLOs;
+
+	AH->ClonePtr = _Clone;
+	AH->DeClonePtr = _DeClone;
+
+	/* no parallel dump in the custom archive, only parallel restore */
+	AH->WorkerJobDumpPtr = _WorkerJobDumpDirectory;
+
+	if (AH->mode == archModeRead)
+		pg_fatal("this format cannot be read");
+}
+
+static int
+_WorkerJobDumpDirectory(ArchiveHandle *AH, TocEntry *te)
+{
+	WriteDataChunksForTocEntry(AH, te);
+
+	/* Return nothing */
+	return 0;
+}
+
+/*
+ * Those must be non-NULL, because CloneArchive() / DeCloneArchive() invokes
+ * them.
+ */
+static void
+_Clone(ArchiveHandle *AH)
+{
+	/* Do nothing */
+}
+
+static void
+_DeClone(ArchiveHandle *AH)
+{
+	/* Do nothing */
+}
+
+static void
+_ArchiveEntry(ArchiveHandle *AH, TocEntry *te)
+{
+	/* Do nothing */
+}
+
+static void
+_StartData(ArchiveHandle *AH, TocEntry *te)
+{
+	/* Do nothing */
+}
+
+static void
+_WriteData(ArchiveHandle *AH, const void *data, size_t dLen)
+{
+	/* Do nothing */
+}
+
+static void
+_EndData(ArchiveHandle *AH, TocEntry *te)
+{
+	/* Do nothing */
+}
+
+static void
+_StartLOs(ArchiveHandle *AH, TocEntry *te)
+{
+	/* Do nothing */
+}
+
+static void
+_StartLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
+{
+	/* Do nothing */
+}
+
+static void
+_EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
+{
+	/* Do nothing */
+}
+
+static void
+_EndLOs(ArchiveHandle *AH, TocEntry *te)
+{
+	/* Do nothing */
+}
+
+static void
+_PrintTocData(ArchiveHandle *AH, TocEntry *te)
+{
+	if (te->dataDumper)
+	{
+		AH->currToc = te;
+		te->dataDumper((Archive *) AH, te->dataDumperArg);
+		AH->currToc = NULL;
+	}
+}
+
+static int
+_WriteByte(ArchiveHandle *AH, const int i)
+{
+	return 0;
+}
+
+static void
+_WriteBuf(ArchiveHandle *AH, const void *buf, size_t len)
+{
+	/* Do nothing */
+}
+
+/*
+ * Close the archive.
+ *
+ * When writing the archive, this is the routine that actually starts
+ * the process of saving it to files.
+ */
+static void
+_CloseArchive(ArchiveHandle *AH)
+{
+	ParallelState *pstate;
+
+	if (AH->mode != archModeWrite)
+		return;
+
+	/*
+	 * WriteDataChunks() calls TocEntry's dataDumper (dumpTableData_copy) that
+	 * issues COPY table TO BLACKHOLE.
+	 */
+	pstate = ParallelBackupStart(AH);
+	WriteDataChunks(AH, pstate);
+	ParallelBackupEnd(AH, pstate);
+}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d56dcc701ce..3950dc56227 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -958,8 +958,11 @@ main(int argc, char **argv)
 	if (!plainText)
 		dopt.outputCreateDB = 1;
 
-	/* Parallel backup only in the directory archive format so far */
-	if (archiveFormat != archDirectory && numWorkers > 1)
+	/*
+	 * Parallel backup only in the BLACKHOLE or the directory archive format
+	 * so far
+	 */
+	if (numWorkers > 1 && archiveFormat != archDirectory && archiveFormat != archBlackhole)
 		pg_fatal("parallel backup only supported by the directory format");
 
 	/* Open the output file */
@@ -1296,8 +1299,8 @@ help(const char *progname)
 
 	printf(_("\nGeneral options:\n"));
 	printf(_("  -f, --file=FILENAME          output file or directory name\n"));
-	printf(_("  -F, --format=c|d|t|p         output file format (custom, directory, tar,\n"
-			 "                               plain text (default))\n"));
+	printf(_("  -F, --format=c|d|t|p|b       output file format (custom, directory, tar,\n"
+			 "                               plain text (default), blackhole)\n"));
 	printf(_("  -j, --jobs=NUM               use this many parallel jobs to dump\n"));
 	printf(_("  -v, --verbose                verbose mode\n"));
 	printf(_("  -V, --version                output version information, then exit\n"));
@@ -1634,6 +1637,8 @@ parseArchiveFormat(const char *format, ArchiveMode *mode)
 		archiveFormat = archTar;
 	else if (pg_strcasecmp(format, "tar") == 0)
 		archiveFormat = archTar;
+	else if (pg_strcasecmp(format, "b") == 0 || pg_strcasecmp(format, "blackhole") == 0)
+		archiveFormat = archBlackhole;
 	else
 		pg_fatal("invalid output format \"%s\" specified", format);
 	return archiveFormat;
@@ -2367,6 +2372,7 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 	const TableInfo *tbinfo = tdinfo->tdtable;
 	const char *classname = tbinfo->dobj.name;
 	PQExpBuffer q = createPQExpBuffer();
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
 
 	/*
 	 * Note: can't use getThreadLocalPQExpBuffer() here, we're calling fmtId
@@ -2378,6 +2384,14 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 	int			ret;
 	char	   *copybuf;
 	const char *column_list;
+	char	   *copy_dest = "stdout";
+	bool		blackhole = false;
+
+	if (AH->format == archBlackhole)
+	{
+		copy_dest = "BLACKHOLE";
+		blackhole = true;
+	}
 
 	pg_log_info("dumping contents of table \"%s.%s\"",
 				tbinfo->dobj.namespace->dobj.name, classname);
@@ -2414,16 +2428,35 @@ dumpTableData_copy(Archive *fout, const void *dcontext)
 		else
 			appendPQExpBufferStr(q, "* ");
 
-		appendPQExpBuffer(q, "FROM %s %s) TO stdout;",
+		appendPQExpBuffer(q, "FROM %s %s) TO %s;",
 						  fmtQualifiedDumpable(tbinfo),
-						  tdinfo->filtercond ? tdinfo->filtercond : "");
+						  tdinfo->filtercond ? tdinfo->filtercond : "",
+						  copy_dest);
 	}
 	else
 	{
-		appendPQExpBuffer(q, "COPY %s %s TO stdout;",
+		appendPQExpBuffer(q, "COPY %s %s TO %s;",
 						  fmtQualifiedDumpable(tbinfo),
-						  column_list);
+						  column_list,
+						  copy_dest);
 	}
+
+	/*
+	 * COPY TO BLACKHOLE discards rows server-side and never sends a
+	 * CopyOutResponse, so it completes with PGRES_COMMAND_OK and there is no
+	 * client-side message to receive.
+	 */
+	if (blackhole)
+	{
+		res = ExecuteSqlQuery(fout, q->data, PGRES_COMMAND_OK);
+		PQclear(res);
+		destroyPQExpBuffer(clistBuf);
+		destroyPQExpBuffer(q);
+		if (tbinfo->relkind == RELKIND_FOREIGN_TABLE)
+			set_restrict_relation_kind(fout, "view, foreign-table");
+		return 1;
+	}
+
 	res = ExecuteSqlQuery(fout, q->data, PGRES_COPY_OUT);
 	PQclear(res);
 	destroyPQExpBuffer(clistBuf);
-- 
2.43.0

Reply via email to