From 20539ad10512ef45785c4bd70b93f94eec1125ba Mon Sep 17 00:00:00 2001
From: Sutou Kouhei <kou@clear-code.com>
Date: Fri, 3 Oct 2025 15:23:01 +0900
Subject: [PATCH] Split CopyToStateData to CopyToState{,Internal,Builtin}Data

---
 src/backend/commands/copyto.c  | 282 ++++++++++++++++++---------------
 src/include/commands/copyapi.h |  53 +++++++
 2 files changed, 211 insertions(+), 124 deletions(-)

diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..30298c0df0c 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -37,19 +37,36 @@
 #include "utils/snapmgr.h"
 
 /*
- * Represents the different dest cases we need to worry about at
- * the bottom level
+ * This struct contains the state variables used internally. All COPY TO
+ * routines including built-in format routines should not use this.
  */
-typedef enum CopyDest
+typedef struct CopyToStateInternalData
 {
-	COPY_FILE,					/* to file (or a piped program) */
-	COPY_FRONTEND,				/* to frontend */
-	COPY_CALLBACK,				/* to callback function */
-} CopyDest;
+	/* format-specific routines */
+	const CopyToRoutine *routine;
+
+	/* parameters from the COPY command */
+	QueryDesc  *queryDesc;		/* executable query to copy from */
+	char	   *filename;		/* filename, or NULL for STDOUT */
+	bool		is_program;		/* is 'filename' a program to popen? */
+
+	Node	   *whereClause;	/* WHERE condition (or NULL) */
+
+	/*
+	 * Working state
+	 */
+	MemoryContext copycontext;	/* per-copy execution context */
+	MemoryContext rowcontext;	/* per-row evaluation context */
+}			CopyToStateInternalData;
+typedef struct CopyToStateInternalData *CopyToStateInternal;
+
+#define CopyToStateInternalGetState(cstate_internal) \
+	((CopyToState) (((char *) cstate_internal) + sizeof(CopyToStateInternalData)))
+#define CopyToStateGetInternal(cstate) \
+	((CopyToStateInternal) (((char *) cstate) - sizeof(CopyToStateInternalData)))
 
 /*
- * This struct contains all the state variables used throughout a COPY TO
- * operation.
+ * This struct contains the state variables used by built-in format routines.
  *
  * Multi-byte encodings: all supported client-side encodings encode multi-byte
  * characters by having the first byte's high bit set. Subsequent bytes of the
@@ -62,40 +79,16 @@ typedef enum CopyDest
  * invoke pg_encoding_mblen() to skip over them. encoding_embeds_ascii is true
  * when we have to do it the hard way.
  */
-typedef struct CopyToStateData
+typedef struct CopyToStateBuiltinData
 {
-	/* format-specific routines */
-	const CopyToRoutine *routine;
+	CopyToStateData parent;
 
 	/* low-level state data */
-	CopyDest	copy_dest;		/* type of copy source/destination */
-	FILE	   *copy_file;		/* used if copy_dest == COPY_FILE */
-	StringInfo	fe_msgbuf;		/* used for all dests during COPY TO */
-
 	int			file_encoding;	/* file or remote side's character encoding */
 	bool		need_transcoding;	/* file encoding diff from server? */
 	bool		encoding_embeds_ascii;	/* ASCII can be non-first byte? */
-
-	/* parameters from the COPY command */
-	Relation	rel;			/* relation to copy to */
-	QueryDesc  *queryDesc;		/* executable query to copy from */
-	List	   *attnumlist;		/* integer list of attnums to copy */
-	char	   *filename;		/* filename, or NULL for STDOUT */
-	bool		is_program;		/* is 'filename' a program to popen? */
-	copy_data_dest_cb data_dest_cb; /* function for writing data */
-
-	CopyFormatOptions opts;
-	Node	   *whereClause;	/* WHERE condition (or NULL) */
-
-	/*
-	 * Working state
-	 */
-	MemoryContext copycontext;	/* per-copy execution context */
-
-	FmgrInfo   *out_functions;	/* lookup info for output functions */
-	MemoryContext rowcontext;	/* per-row evaluation context */
-	uint64		bytes_processed;	/* number of bytes processed so far */
-} CopyToStateData;
+}			CopyToStateBuiltinData;
+typedef struct CopyToStateBuiltinData *CopyToStateBuiltin;
 
 /* DestReceiver for COPY (query) TO */
 typedef struct
@@ -118,6 +111,7 @@ static void CopyAttributeOutCSV(CopyToState cstate, const char *string,
 								bool use_quote);
 
 /* built-in format-specific routines */
+static size_t CopyToBuiltinGetStateSize(void);
 static void CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc);
 static void CopyToTextLikeOutFunc(CopyToState cstate, Oid atttypid, FmgrInfo *finfo);
 static void CopyToTextOneRow(CopyToState cstate, TupleTableSlot *slot);
@@ -150,6 +144,7 @@ static void CopySendInt16(CopyToState cstate, int16 val);
 
 /* text format */
 static const CopyToRoutine CopyToRoutineText = {
+	.CopyToGetStateSize = CopyToBuiltinGetStateSize,
 	.CopyToStart = CopyToTextLikeStart,
 	.CopyToOutFunc = CopyToTextLikeOutFunc,
 	.CopyToOneRow = CopyToTextOneRow,
@@ -158,6 +153,7 @@ static const CopyToRoutine CopyToRoutineText = {
 
 /* CSV format */
 static const CopyToRoutine CopyToRoutineCSV = {
+	.CopyToGetStateSize = CopyToBuiltinGetStateSize,
 	.CopyToStart = CopyToTextLikeStart,
 	.CopyToOutFunc = CopyToTextLikeOutFunc,
 	.CopyToOneRow = CopyToCSVOneRow,
@@ -166,6 +162,7 @@ static const CopyToRoutine CopyToRoutineCSV = {
 
 /* binary format */
 static const CopyToRoutine CopyToRoutineBinary = {
+	.CopyToGetStateSize = CopyToBuiltinGetStateSize,
 	.CopyToStart = CopyToBinaryStart,
 	.CopyToOutFunc = CopyToBinaryOutFunc,
 	.CopyToOneRow = CopyToBinaryOneRow,
@@ -185,18 +182,27 @@ CopyToGetRoutine(const CopyFormatOptions *opts)
 	return &CopyToRoutineText;
 }
 
+/* Implementation of the allocate callback for all built-in formats */
+static size_t
+CopyToBuiltinGetStateSize(void)
+{
+	return sizeof(CopyToStateBuiltinData);
+}
+
 /* Implementation of the start callback for text and CSV formats */
 static void
 CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc)
 {
+	CopyToStateBuiltin cstate_builtin = (CopyToStateBuiltin) cstate;
+
 	/*
 	 * For non-binary copy, we need to convert null_print to file encoding,
 	 * because it will be sent directly with CopySendString.
 	 */
-	if (cstate->need_transcoding)
+	if (cstate_builtin->need_transcoding)
 		cstate->opts.null_print_client = pg_server_to_any(cstate->opts.null_print,
 														  cstate->opts.null_print_len,
-														  cstate->file_encoding);
+														  cstate_builtin->file_encoding);
 
 	/* if a header has been requested send the line */
 	if (cstate->opts.header_line == COPY_HEADER_TRUE)
@@ -401,7 +407,7 @@ SendCopyBegin(CopyToState cstate)
 	for (i = 0; i < natts; i++)
 		pq_sendint16(&buf, format); /* per-column formats */
 	pq_endmessage(&buf);
-	cstate->copy_dest = COPY_FRONTEND;
+	cstate->copy_dest = COPY_DEST_FRONTEND;
 }
 
 static void
@@ -444,16 +450,17 @@ CopySendChar(CopyToState cstate, char c)
 static void
 CopySendEndOfRow(CopyToState cstate)
 {
+	CopyToStateInternal cstate_internal = CopyToStateGetInternal(cstate);
 	StringInfo	fe_msgbuf = cstate->fe_msgbuf;
 
 	switch (cstate->copy_dest)
 	{
-		case COPY_FILE:
+		case COPY_DEST_FILE:
 			if (fwrite(fe_msgbuf->data, fe_msgbuf->len, 1,
 					   cstate->copy_file) != 1 ||
 				ferror(cstate->copy_file))
 			{
-				if (cstate->is_program)
+				if (cstate_internal->is_program)
 				{
 					if (errno == EPIPE)
 					{
@@ -482,11 +489,11 @@ CopySendEndOfRow(CopyToState cstate)
 							 errmsg("could not write to COPY file: %m")));
 			}
 			break;
-		case COPY_FRONTEND:
+		case COPY_DEST_FRONTEND:
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage(PqMsg_CopyData, fe_msgbuf->data, fe_msgbuf->len);
 			break;
-		case COPY_CALLBACK:
+		case COPY_DEST_CALLBACK:
 			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
@@ -507,7 +514,7 @@ CopySendTextLikeEndOfRow(CopyToState cstate)
 {
 	switch (cstate->copy_dest)
 	{
-		case COPY_FILE:
+		case COPY_DEST_FILE:
 			/* Default line termination depends on platform */
 #ifndef WIN32
 			CopySendChar(cstate, '\n');
@@ -515,7 +522,7 @@ CopySendTextLikeEndOfRow(CopyToState cstate)
 			CopySendString(cstate, "\r\n");
 #endif
 			break;
-		case COPY_FRONTEND:
+		case COPY_DEST_FRONTEND:
 			/* The FE/BE protocol uses \n as newline for all platforms */
 			CopySendChar(cstate, '\n');
 			break;
@@ -561,9 +568,10 @@ CopySendInt16(CopyToState cstate, int16 val)
 static void
 ClosePipeToProgram(CopyToState cstate)
 {
+	CopyToStateInternal cstate_internal = CopyToStateGetInternal(cstate);
 	int			pclose_rc;
 
-	Assert(cstate->is_program);
+	Assert(cstate_internal->is_program);
 
 	pclose_rc = ClosePipeStream(cstate->copy_file);
 	if (pclose_rc == -1)
@@ -575,7 +583,7 @@ ClosePipeToProgram(CopyToState cstate)
 		ereport(ERROR,
 				(errcode(ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
 				 errmsg("program \"%s\" failed",
-						cstate->filename),
+						cstate_internal->filename),
 				 errdetail_internal("%s", wait_result_to_str(pclose_rc))));
 	}
 }
@@ -586,23 +594,25 @@ ClosePipeToProgram(CopyToState cstate)
 static void
 EndCopy(CopyToState cstate)
 {
-	if (cstate->is_program)
+	CopyToStateInternal cstate_internal = CopyToStateGetInternal(cstate);
+
+	if (cstate_internal->is_program)
 	{
 		ClosePipeToProgram(cstate);
 	}
 	else
 	{
-		if (cstate->filename != NULL && FreeFile(cstate->copy_file))
+		if (cstate_internal->filename != NULL && FreeFile(cstate->copy_file))
 			ereport(ERROR,
 					(errcode_for_file_access(),
 					 errmsg("could not close file \"%s\": %m",
-							cstate->filename)));
+							cstate_internal->filename)));
 	}
 
 	pgstat_progress_end_command();
 
-	MemoryContextDelete(cstate->copycontext);
-	pfree(cstate);
+	MemoryContextDelete(cstate_internal->copycontext);
+	pfree(CopyToStateGetInternal(cstate));
 }
 
 /*
@@ -630,11 +640,16 @@ BeginCopyTo(ParseState *pstate,
 			List *attnamelist,
 			List *options)
 {
+	CopyToStateInternal cstate_internal;
 	CopyToState cstate;
+	CopyToStateBuiltin cstate_builtin;
 	bool		pipe = (filename == NULL && data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
+	MemoryContext copycontext;
 	MemoryContext oldcontext;
+	CopyFormatOptions opts = {0};
+	const CopyToRoutine *routine;
 	const int	progress_cols[] = {
 		PROGRESS_COPY_COMMAND,
 		PROGRESS_COPY_TYPE
@@ -686,24 +701,34 @@ BeginCopyTo(ParseState *pstate,
 	}
 
 
-	/* Allocate workspace and zero all fields */
-	cstate = (CopyToStateData *) palloc0(sizeof(CopyToStateData));
-
 	/*
 	 * We allocate everything used by a cstate in a new memory context. This
 	 * avoids memory leaks during repeated use of COPY in a query.
 	 */
-	cstate->copycontext = AllocSetContextCreate(CurrentMemoryContext,
-												"COPY",
-												ALLOCSET_DEFAULT_SIZES);
+	copycontext = AllocSetContextCreate(CurrentMemoryContext,
+										"COPY",
+										ALLOCSET_DEFAULT_SIZES);
 
-	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
+	oldcontext = MemoryContextSwitchTo(copycontext);
 
 	/* Extract options from the statement node tree */
-	ProcessCopyOptions(pstate, &cstate->opts, false /* is_from */ , options);
+	ProcessCopyOptions(pstate, &opts, false /* is_from */ , options);
 
-	/* Set format routine */
-	cstate->routine = CopyToGetRoutine(&cstate->opts);
+	/* Get format routine */
+	routine = CopyToGetRoutine(&opts);
+
+	/* Allocate workspace and set known values */
+	MemoryContextSwitchTo(oldcontext);
+	cstate_internal = (CopyToStateInternal) palloc0(sizeof(CopyToStateInternal) + routine->CopyToGetStateSize());
+	MemoryContextSwitchTo(copycontext);
+	cstate = CopyToStateInternalGetState(cstate_internal);
+	if (routine == &CopyToRoutineText || routine == &CopyToRoutineCSV || routine == &CopyToRoutineBinary)
+		cstate_builtin = (CopyToStateBuiltin) cstate;
+	else
+		cstate_builtin = NULL;
+	cstate_internal->copycontext = copycontext;
+	cstate->opts = opts;
+	cstate_internal->routine = routine;
 
 	/* Process the source/target relation or query */
 	if (rel)
@@ -835,19 +860,19 @@ BeginCopyTo(ParseState *pstate,
 		((DR_copy *) dest)->cstate = cstate;
 
 		/* Create a QueryDesc requesting no output */
-		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
-											GetActiveSnapshot(),
-											InvalidSnapshot,
-											dest, NULL, NULL, 0);
+		cstate_internal->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
+													 GetActiveSnapshot(),
+													 InvalidSnapshot,
+													 dest, NULL, NULL, 0);
 
 		/*
 		 * Call ExecutorStart to prepare the plan for execution.
 		 *
 		 * ExecutorStart computes a result tupdesc for us
 		 */
-		ExecutorStart(cstate->queryDesc, 0);
+		ExecutorStart(cstate_internal->queryDesc, 0);
 
-		tupDesc = cstate->queryDesc->tupDesc;
+		tupDesc = cstate_internal->queryDesc->tupDesc;
 	}
 
 	/* Generate or convert list of attributes to process */
@@ -883,31 +908,34 @@ BeginCopyTo(ParseState *pstate,
 		}
 	}
 
-	/* Use client encoding when ENCODING option is not specified. */
-	if (cstate->opts.file_encoding < 0)
-		cstate->file_encoding = pg_get_client_encoding();
-	else
-		cstate->file_encoding = cstate->opts.file_encoding;
+	if (cstate_builtin)
+	{
+		/* Use client encoding when ENCODING option is not specified. */
+		if (cstate->opts.file_encoding < 0)
+			cstate_builtin->file_encoding = pg_get_client_encoding();
+		else
+			cstate_builtin->file_encoding = cstate->opts.file_encoding;
 
-	/*
-	 * Set up encoding conversion info if the file and server encodings differ
-	 * (see also pg_server_to_any).
-	 */
-	if (cstate->file_encoding == GetDatabaseEncoding() ||
-		cstate->file_encoding == PG_SQL_ASCII)
-		cstate->need_transcoding = false;
-	else
-		cstate->need_transcoding = true;
+		/*
+		 * Set up encoding conversion info if the file and server encodings
+		 * differ (see also pg_server_to_any).
+		 */
+		if (cstate_builtin->file_encoding == GetDatabaseEncoding() ||
+			cstate_builtin->file_encoding == PG_SQL_ASCII)
+			cstate_builtin->need_transcoding = false;
+		else
+			cstate_builtin->need_transcoding = true;
 
-	/* See Multibyte encoding comment above */
-	cstate->encoding_embeds_ascii = PG_ENCODING_IS_CLIENT_ONLY(cstate->file_encoding);
+		/* See Multibyte encoding comment above */
+		cstate_builtin->encoding_embeds_ascii = PG_ENCODING_IS_CLIENT_ONLY(cstate_builtin->file_encoding);
+	}
 
-	cstate->copy_dest = COPY_FILE;	/* default */
+	cstate->copy_dest = COPY_DEST_FILE; /* default */
 
 	if (data_dest_cb)
 	{
 		progress_vals[1] = PROGRESS_COPY_TYPE_CALLBACK;
-		cstate->copy_dest = COPY_CALLBACK;
+		cstate->copy_dest = COPY_DEST_CALLBACK;
 		cstate->data_dest_cb = data_dest_cb;
 	}
 	else if (pipe)
@@ -920,18 +948,18 @@ BeginCopyTo(ParseState *pstate,
 	}
 	else
 	{
-		cstate->filename = pstrdup(filename);
-		cstate->is_program = is_program;
+		cstate_internal->filename = pstrdup(filename);
+		cstate_internal->is_program = is_program;
 
 		if (is_program)
 		{
 			progress_vals[1] = PROGRESS_COPY_TYPE_PROGRAM;
-			cstate->copy_file = OpenPipeStream(cstate->filename, PG_BINARY_W);
+			cstate->copy_file = OpenPipeStream(cstate_internal->filename, PG_BINARY_W);
 			if (cstate->copy_file == NULL)
 				ereport(ERROR,
 						(errcode_for_file_access(),
 						 errmsg("could not execute command \"%s\": %m",
-								cstate->filename)));
+								cstate_internal->filename)));
 		}
 		else
 		{
@@ -952,7 +980,7 @@ BeginCopyTo(ParseState *pstate,
 			oumask = umask(S_IWGRP | S_IWOTH);
 			PG_TRY();
 			{
-				cstate->copy_file = AllocateFile(cstate->filename, PG_BINARY_W);
+				cstate->copy_file = AllocateFile(cstate_internal->filename, PG_BINARY_W);
 			}
 			PG_FINALLY();
 			{
@@ -967,7 +995,7 @@ BeginCopyTo(ParseState *pstate,
 				ereport(ERROR,
 						(errcode_for_file_access(),
 						 errmsg("could not open file \"%s\" for writing: %m",
-								cstate->filename),
+								cstate_internal->filename),
 						 (save_errno == ENOENT || save_errno == EACCES) ?
 						 errhint("COPY TO instructs the PostgreSQL server process to write a file. "
 								 "You may want a client-side facility such as psql's \\copy.") : 0));
@@ -977,12 +1005,12 @@ BeginCopyTo(ParseState *pstate,
 				ereport(ERROR,
 						(errcode_for_file_access(),
 						 errmsg("could not stat file \"%s\": %m",
-								cstate->filename)));
+								cstate_internal->filename)));
 
 			if (S_ISDIR(st.st_mode))
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("\"%s\" is a directory", cstate->filename)));
+						 errmsg("\"%s\" is a directory", cstate_internal->filename)));
 		}
 	}
 
@@ -1004,12 +1032,14 @@ BeginCopyTo(ParseState *pstate,
 void
 EndCopyTo(CopyToState cstate)
 {
-	if (cstate->queryDesc != NULL)
+	CopyToStateInternal cstate_internal = CopyToStateGetInternal(cstate);
+
+	if (cstate_internal->queryDesc != NULL)
 	{
 		/* Close down the query and free resources. */
-		ExecutorFinish(cstate->queryDesc);
-		ExecutorEnd(cstate->queryDesc);
-		FreeQueryDesc(cstate->queryDesc);
+		ExecutorFinish(cstate_internal->queryDesc);
+		ExecutorEnd(cstate_internal->queryDesc);
+		FreeQueryDesc(cstate_internal->queryDesc);
 		PopActiveSnapshot();
 	}
 
@@ -1025,7 +1055,8 @@ EndCopyTo(CopyToState cstate)
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL && cstate->data_dest_cb == NULL);
+	CopyToStateInternal cstate_internal = CopyToStateGetInternal(cstate);
+	bool		pipe = (cstate_internal->filename == NULL && cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
@@ -1038,7 +1069,7 @@ DoCopyTo(CopyToState cstate)
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
 	else
-		tupDesc = cstate->queryDesc->tupDesc;
+		tupDesc = cstate_internal->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
 	cstate->opts.null_print_client = cstate->opts.null_print;	/* default */
 
@@ -1052,8 +1083,8 @@ DoCopyTo(CopyToState cstate)
 		int			attnum = lfirst_int(cur);
 		Form_pg_attribute attr = TupleDescAttr(tupDesc, attnum - 1);
 
-		cstate->routine->CopyToOutFunc(cstate, attr->atttypid,
-									   &cstate->out_functions[attnum - 1]);
+		cstate_internal->routine->CopyToOutFunc(cstate, attr->atttypid,
+												&cstate->out_functions[attnum - 1]);
 	}
 
 	/*
@@ -1062,11 +1093,11 @@ DoCopyTo(CopyToState cstate)
 	 * datatype output routines, and should be faster than retail pfree's
 	 * anyway.  (We don't need a whole econtext as CopyFrom does.)
 	 */
-	cstate->rowcontext = AllocSetContextCreate(CurrentMemoryContext,
-											   "COPY TO",
-											   ALLOCSET_DEFAULT_SIZES);
+	cstate_internal->rowcontext = AllocSetContextCreate(CurrentMemoryContext,
+														"COPY TO",
+														ALLOCSET_DEFAULT_SIZES);
 
-	cstate->routine->CopyToStart(cstate, tupDesc);
+	cstate_internal->routine->CopyToStart(cstate, tupDesc);
 
 	if (cstate->rel)
 	{
@@ -1101,13 +1132,13 @@ DoCopyTo(CopyToState cstate)
 	else
 	{
 		/* run the plan --- the dest receiver will send tuples */
-		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0);
-		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
+		ExecutorRun(cstate_internal->queryDesc, ForwardScanDirection, 0);
+		processed = ((DR_copy *) cstate_internal->queryDesc->dest)->processed;
 	}
 
-	cstate->routine->CopyToEnd(cstate);
+	cstate_internal->routine->CopyToEnd(cstate);
 
-	MemoryContextDelete(cstate->rowcontext);
+	MemoryContextDelete(cstate_internal->rowcontext);
 
 	if (fe_copy)
 		SendCopyEnd(cstate);
@@ -1121,15 +1152,16 @@ DoCopyTo(CopyToState cstate)
 static inline void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
+	CopyToStateInternal cstate_internal = CopyToStateGetInternal(cstate);
 	MemoryContext oldcontext;
 
-	MemoryContextReset(cstate->rowcontext);
-	oldcontext = MemoryContextSwitchTo(cstate->rowcontext);
+	MemoryContextReset(cstate_internal->rowcontext);
+	oldcontext = MemoryContextSwitchTo(cstate_internal->rowcontext);
 
 	/* Make sure the tuple is fully deconstructed */
 	slot_getallattrs(slot);
 
-	cstate->routine->CopyToOneRow(cstate, slot);
+	cstate_internal->routine->CopyToOneRow(cstate, slot);
 
 	MemoryContextSwitchTo(oldcontext);
 }
@@ -1146,13 +1178,14 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 static void
 CopyAttributeOutText(CopyToState cstate, const char *string)
 {
+	CopyToStateBuiltin cstate_builtin = (CopyToStateBuiltin) cstate;
 	const char *ptr;
 	const char *start;
 	char		c;
 	char		delimc = cstate->opts.delim[0];
 
-	if (cstate->need_transcoding)
-		ptr = pg_server_to_any(string, strlen(string), cstate->file_encoding);
+	if (cstate_builtin->need_transcoding)
+		ptr = pg_server_to_any(string, strlen(string), cstate_builtin->file_encoding);
 	else
 		ptr = string;
 
@@ -1170,7 +1203,7 @@ CopyAttributeOutText(CopyToState cstate, const char *string)
 	 * it's worth making two copies of it to get the IS_HIGHBIT_SET() test out
 	 * of the normal safe-encoding path.
 	 */
-	if (cstate->encoding_embeds_ascii)
+	if (cstate_builtin->encoding_embeds_ascii)
 	{
 		start = ptr;
 		while ((c = *ptr) != '\0')
@@ -1225,7 +1258,7 @@ CopyAttributeOutText(CopyToState cstate, const char *string)
 				start = ptr++;	/* we include char in next run */
 			}
 			else if (IS_HIGHBIT_SET(c))
-				ptr += pg_encoding_mblen(cstate->file_encoding, ptr);
+				ptr += pg_encoding_mblen(cstate_builtin->file_encoding, ptr);
 			else
 				ptr++;
 		}
@@ -1300,6 +1333,7 @@ static void
 CopyAttributeOutCSV(CopyToState cstate, const char *string,
 					bool use_quote)
 {
+	CopyToStateBuiltin cstate_builtin = (CopyToStateBuiltin) cstate;
 	const char *ptr;
 	const char *start;
 	char		c;
@@ -1312,8 +1346,8 @@ CopyAttributeOutCSV(CopyToState cstate, const char *string,
 	if (!use_quote && strcmp(string, cstate->opts.null_print) == 0)
 		use_quote = true;
 
-	if (cstate->need_transcoding)
-		ptr = pg_server_to_any(string, strlen(string), cstate->file_encoding);
+	if (cstate_builtin->need_transcoding)
+		ptr = pg_server_to_any(string, strlen(string), cstate_builtin->file_encoding);
 	else
 		ptr = string;
 
@@ -1342,8 +1376,8 @@ CopyAttributeOutCSV(CopyToState cstate, const char *string,
 					use_quote = true;
 					break;
 				}
-				if (IS_HIGHBIT_SET(c) && cstate->encoding_embeds_ascii)
-					tptr += pg_encoding_mblen(cstate->file_encoding, tptr);
+				if (IS_HIGHBIT_SET(c) && cstate_builtin->encoding_embeds_ascii)
+					tptr += pg_encoding_mblen(cstate_builtin->file_encoding, tptr);
 				else
 					tptr++;
 			}
@@ -1366,8 +1400,8 @@ CopyAttributeOutCSV(CopyToState cstate, const char *string,
 				CopySendChar(cstate, escapec);
 				start = ptr;	/* we include char in next run */
 			}
-			if (IS_HIGHBIT_SET(c) && cstate->encoding_embeds_ascii)
-				ptr += pg_encoding_mblen(cstate->file_encoding, ptr);
+			if (IS_HIGHBIT_SET(c) && cstate_builtin->encoding_embeds_ascii)
+				ptr += pg_encoding_mblen(cstate_builtin->file_encoding, ptr);
 			else
 				ptr++;
 		}
diff --git a/src/include/commands/copyapi.h b/src/include/commands/copyapi.h
index 2a2d2f9876b..7c536c74a18 100644
--- a/src/include/commands/copyapi.h
+++ b/src/include/commands/copyapi.h
@@ -16,12 +16,65 @@
 
 #include "commands/copy.h"
 
+/*
+ * Represents the different dest cases we need to worry about at
+ * the bottom level
+ */
+typedef enum CopyDest
+{
+	COPY_DEST_FILE,				/* to file (or a piped program) */
+	COPY_DEST_FRONTEND,			/* to frontend */
+	COPY_DEST_CALLBACK,			/* to callback function */
+} CopyDest;
+
+/*
+ * This struct contains the state variables used by PostgreSQL, built-in
+ * format routines and custom format routines.
+ */
+typedef struct CopyToStateData
+{
+	/* parameters from the COPY command */
+	Relation	rel;			/* relation to copy to */
+	List	   *attnumlist;		/* integer list of attnums to copy */
+	copy_data_dest_cb data_dest_cb; /* function for writing data */
+	CopyFormatOptions opts;
+	FmgrInfo   *out_functions;	/* lookup info for output functions */
+
+	/* low-level state data */
+	CopyDest	copy_dest;		/* type of copy source/destination */
+	FILE	   *copy_file;		/* used if copy_dest == COPY_FILE */
+	StringInfo	fe_msgbuf;		/* used for all dests during COPY TO */
+
+	/*
+	 * Working state
+	 */
+	uint64		bytes_processed;	/* number of bytes processed so far */
+} CopyToStateData;
+
 /*
  * API structure for a COPY TO format implementation. Note this must be
  * allocated in a server-lifetime manner, typically as a static const struct.
  */
 typedef struct CopyToRoutine
 {
+	/*
+	 * Return state size for this routine.
+	 *
+	 * If this routine uses CopyToStateData as-is, `return
+	 * sizeof(CopyToStateData)` can be used.
+	 *
+	 * If this routine needs additional data than CopyToStateData, a new
+	 * struct based on CopyToStateData can be used something like:
+	 *
+	 * typedef struct MyCopyToStateDate {
+	 *     struct CopyToStateData parent;
+	 *     int define_additional_members_here;
+	 * } MyCopyToStateData;
+	 *
+	 * In the case, this callback returns `sizeof(MyCopyToStateData)`.
+	 */
+	size_t		(*CopyToGetStateSize) (void);
+
 	/*
 	 * Set output function information. This callback is called once at the
 	 * beginning of COPY TO.
-- 
2.51.0