From 362ceb242c7b026bcbb077c910b26512b08e1e9d Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 10 Nov 2025 14:53:51 -0800
Subject: [PATCH 4/6] Refactor Copy option processing.

This commits refactors the COPY option handling to support custom COPY
option callback. Since extensions can define custom COPY format with
its own state data, it's necessary to pass the state data to the
option process callback. This commits separates ProcessCopyOptions
into ProcessCopyFromOptions and ProcessCopyToOptions and passes a
pointer to Copy{From,To}State respectively.

The subsequent patch introduces a new callback to allow extensions to
define their own option processing callback.
---
 contrib/file_fdw/file_fdw.c              |  2 +-
 src/backend/commands/copy.c              | 18 ++++---------
 src/backend/commands/copyfrom.c          | 31 ++++++++++++++++++++++-
 src/backend/commands/copyto.c            | 32 +++++++++++++++++++++++-
 src/include/commands/copyfrom_internal.h |  2 ++
 5 files changed, 69 insertions(+), 16 deletions(-)

diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index af2d86e5b5c..447fcb44241 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -338,7 +338,7 @@ file_fdw_validator(PG_FUNCTION_ARGS)
 	/*
 	 * Now apply the core COPY code's validation logic for more checks.
 	 */
-	ProcessCopyOptions(NULL, NULL, true, other_options);
+	ProcessCopyFromOptions(NULL, other_options, NULL);
 
 	/*
 	 * Either filename or program option is required for file_fdw foreign
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 28e878c3688..d4ee649c3c6 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -546,10 +546,8 @@ defGetCopyLogVerbosityChoice(DefElem *def, ParseState *pstate)
  * self-consistency of the options list.
  */
 void
-ProcessCopyOptions(ParseState *pstate,
-				   CopyFormatOptions *opts_out,
-				   bool is_from,
-				   List *options)
+ProcessCopyBuiltinOptions(List *options, CopyFormatOptions *opts_out,
+						  bool is_from, List **other_options, ParseState *pstate)
 {
 	bool		format_specified = false;
 	bool		freeze_specified = false;
@@ -559,10 +557,6 @@ ProcessCopyOptions(ParseState *pstate,
 	bool		reject_limit_specified = false;
 	ListCell   *option;
 
-	/* Support external use for option sanity checking */
-	if (opts_out == NULL)
-		opts_out = (CopyFormatOptions *) palloc0(sizeof(CopyFormatOptions));
-
 	opts_out->file_encoding = -1;
 
 	/* Extract options from the statement node tree */
@@ -583,6 +577,8 @@ ProcessCopyOptions(ParseState *pstate,
 				opts_out->csv_mode = true;
 			else if (strcmp(fmt, "binary") == 0)
 				opts_out->binary = true;
+			else if (FindCustomCopyFormat(fmt))
+				 /* just validate option value */ ;
 			else
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -731,11 +727,7 @@ ProcessCopyOptions(ParseState *pstate,
 			opts_out->reject_limit = defGetCopyRejectLimitOption(defel);
 		}
 		else
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("option \"%s\" not recognized",
-							defel->defname),
-					 parser_errposition(pstate, defel->location)));
+			*other_options = lappend(*other_options, defel);
 	}
 
 	/*
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 592be4fcb5d..6ca5466f244 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1705,7 +1705,7 @@ BeginCopyFrom(ParseState *pstate,
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Extract options from the statement node tree */
-	ProcessCopyOptions(pstate, &cstate->opts, true /* is_from */ , options);
+	ProcessCopyFromOptions(cstate, options, pstate);
 
 	/* Process the target relation */
 	cstate->rel = rel;
@@ -2028,3 +2028,32 @@ ClosePipeFromProgram(CopyFromState cstate)
 				 errdetail_internal("%s", wait_result_to_str(pclose_rc))));
 	}
 }
+
+void
+ProcessCopyFromOptions(CopyFromState cstate, List *options, ParseState *pstate)
+{
+	bool		temp_state = false;
+	List	   *other_options = NIL;
+	CopyFormatOptions *opts;
+
+	if (cstate == NULL)
+	{
+		cstate = create_copyfrom_state(pstate, options);
+		temp_state = true;
+	}
+
+	opts = &cstate->opts;
+
+	ProcessCopyBuiltinOptions(options, opts, true, &other_options, pstate);
+
+	foreach_node(DefElem, option, other_options)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("COPY format \"%s\" not recognized", option->defname),
+				 parser_errposition(pstate, option->location)));
+	}
+
+	if (temp_state)
+		pfree(cstate);
+}
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index b1b3ae141eb..ea31fa911f9 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -84,6 +84,7 @@ static void CopyAttributeOutCSV(CopyToStateTextLike * cstate, const char *string
 								bool use_quote);
 static void CopyRelationTo(CopyToState cstate, Relation rel, Relation root_rel,
 						   uint64 *processed);
+static void ProcessCopyToOptions(CopyToState cstate, List *options, ParseState *pstate);
 
 /* built-in format-specific routines */
 static Size CopyToEstimateStateTextLike(void);
@@ -785,7 +786,7 @@ BeginCopyTo(ParseState *pstate,
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Extract options from the statement node tree */
-	ProcessCopyOptions(pstate, &cstate->opts, false /* is_from */ , options);
+	ProcessCopyToOptions(cstate, options, pstate);
 
 	/* Process the source/target relation or query */
 	if (rel)
@@ -1570,3 +1571,32 @@ CreateCopyDestReceiver(void)
 
 	return (DestReceiver *) self;
 }
+
+static void
+ProcessCopyToOptions(CopyToState cstate, List *options, ParseState *pstate)
+{
+	bool		temp_state = false;
+	List	   *other_options = NIL;
+	CopyFormatOptions *opts;
+
+	if (cstate == NULL)
+	{
+		cstate = create_copyto_state(pstate, options);
+		temp_state = true;
+	}
+
+	opts = &cstate->opts;
+
+	ProcessCopyBuiltinOptions(options, opts, false, &other_options, pstate);
+
+	foreach_node(DefElem, option, options)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("COPY format \"%s\" not recognized", option->defname),
+				 parser_errposition(pstate, option->location)));
+	}
+
+	if (temp_state)
+		pfree(cstate);
+}
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index d2d19536151..f457e2bee50 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -100,6 +100,8 @@ typedef struct CopyFromStateBuiltins
 extern void ReceiveCopyBegin(CopyFromState cstate);
 extern void ReceiveCopyBinaryHeader(CopyFromState cstate);
 
+extern void ProcessCopyFromOptions(CopyFromState cstate, List *options, ParseState *pstate);
+
 /* One-row callbacks for built-in formats defined in copyfromparse.c */
 extern bool CopyFromTextOneRow(CopyFromState cstate, ExprContext *econtext,
 							   Datum *values, bool *nulls, CopyFromRowInfo * rowinfo);
-- 
2.47.3

