From c0e1e5986d10125b41f3fde10cdb3ba931db00b3 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 22 Jun 2026 09:23:09 -0700
Subject: [PATCH v2 3/4] Add an hook for custom COPY format option validation.

Author:
Reviewed-by:
Discussion: https://postgr.es/m/
---
 src/backend/commands/copy.c    | 13 ++++++++++++-
 src/backend/commands/copyapi.c | 10 ++++++++--
 src/include/commands/copyapi.h | 15 +++++++++++++--
 3 files changed, 33 insertions(+), 5 deletions(-)

diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 2fdba026ee0..45908d0c1e5 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -599,6 +599,7 @@ ProcessCopyOptions(ParseState *pstate,
 	 */
 	List	   *deferred_options = NIL;
 	ProcessOneOptionFn custom_process_option_fn = NULL;
+	ValidateOptionsFn custom_validate_options_fn = NULL;
 	char	   *custom_format_name = NULL;
 
 	/* Support external use for option sanity checking */
@@ -631,7 +632,8 @@ ProcessCopyOptions(ParseState *pstate,
 				opts_out->format = COPY_FORMAT_JSON;
 			else if (GetCopyCustomFormatRoutines(fmt, &opts_out->to_routine,
 												 &opts_out->from_routine,
-												 &custom_process_option_fn))
+												 &custom_process_option_fn,
+												 &custom_validate_options_fn))
 			{
 				opts_out->format = COPY_FORMAT_CUSTOM;
 				custom_format_name = fmt;
@@ -1126,6 +1128,15 @@ ProcessCopyOptions(ParseState *pstate,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("COPY format \"%s\" cannot be used with COPY TO",
 							custom_format_name)));
+
+		/*
+		 * Let the format validate its fully-parsed options as a whole.  This
+		 * runs even when no format-specific options were given, so a format
+		 * can reject incompatible core options or enforce cross-option
+		 * constraints.
+		 */
+		if (custom_validate_options_fn != NULL)
+			custom_validate_options_fn(opts_out, is_from);
 	}
 }
 
diff --git a/src/backend/commands/copyapi.c b/src/backend/commands/copyapi.c
index 168efbcf30b..0bd1cb71af2 100644
--- a/src/backend/commands/copyapi.c
+++ b/src/backend/commands/copyapi.c
@@ -28,6 +28,7 @@ typedef struct CopyCustomFormatEntry
 	const CopyToRoutine *to_routine;
 	const CopyFromRoutine *from_routine;
 	ProcessOneOptionFn option_fn;
+	ValidateOptionsFn validate_fn;
 } CopyCustomFormatEntry;
 
 static CopyCustomFormatEntry *CopyCustomFormatArray = NULL;
@@ -56,7 +57,8 @@ is_builtin_copy_format(const char *name)
  */
 void
 RegisterCopyCustomFormat(const char *name, const CopyToRoutine *to,
-						 const CopyFromRoutine *from, ProcessOneOptionFn option_fn)
+						 const CopyFromRoutine *from, ProcessOneOptionFn option_fn,
+						 ValidateOptionsFn validate_fn)
 {
 	Assert(name != NULL && name[0] != '\0');
 
@@ -102,6 +104,7 @@ RegisterCopyCustomFormat(const char *name, const CopyToRoutine *to,
 	CopyCustomFormatArray[CopyCustomFormatsAssigned].to_routine = to;
 	CopyCustomFormatArray[CopyCustomFormatsAssigned].from_routine = from;
 	CopyCustomFormatArray[CopyCustomFormatsAssigned].option_fn = option_fn;
+	CopyCustomFormatArray[CopyCustomFormatsAssigned].validate_fn = validate_fn;
 	CopyCustomFormatsAssigned++;
 }
 
@@ -111,7 +114,8 @@ RegisterCopyCustomFormat(const char *name, const CopyToRoutine *to,
  */
 bool
 GetCopyCustomFormatRoutines(const char *name, const CopyToRoutine **to,
-							const CopyFromRoutine **from, ProcessOneOptionFn * option_fn)
+							const CopyFromRoutine **from, ProcessOneOptionFn * option_fn,
+							ValidateOptionsFn * validate_fn)
 {
 	for (int i = 0; i < CopyCustomFormatsAssigned; i++)
 	{
@@ -123,6 +127,8 @@ GetCopyCustomFormatRoutines(const char *name, const CopyToRoutine **to,
 				*from = CopyCustomFormatArray[i].from_routine;
 			if (option_fn)
 				*option_fn = CopyCustomFormatArray[i].option_fn;
+			if (validate_fn)
+				*validate_fn = CopyCustomFormatArray[i].validate_fn;
 
 			return true;
 		}
diff --git a/src/include/commands/copyapi.h b/src/include/commands/copyapi.h
index 8eb5fe9c7dc..c47c89a858f 100644
--- a/src/include/commands/copyapi.h
+++ b/src/include/commands/copyapi.h
@@ -120,6 +120,15 @@ typedef struct CopyFromRoutine
 typedef bool (*ProcessOneOptionFn) (CopyFormatOptions *opts, bool is_from,
 									DefElem *option);
 
+/*
+ * Optional callback to validate a custom format's fully-parsed options as a
+ * whole. Invoked once from ProcessCopyOptions() after all options have been
+ * processed, so it can enforce cross-option constraints and reject
+ * incompatible core options. It runs even when no format-specific options were
+ * supplied. Reports problems with ereport().
+ */
+typedef void (*ValidateOptionsFn) (CopyFormatOptions *opts, bool is_from);
+
 /*
  * Register a COPY format under 'name', mapping it to its TO and/or FROM
  * routines and optional option/validation callbacks. Intended to be called
@@ -129,7 +138,8 @@ typedef bool (*ProcessOneOptionFn) (CopyFormatOptions *opts, bool is_from,
  */
 extern void RegisterCopyCustomFormat(const char *name, const CopyToRoutine *to,
 									 const CopyFromRoutine *from,
-									 ProcessOneOptionFn option_fn);
+									 ProcessOneOptionFn option_fn,
+									 ValidateOptionsFn validate_fn);
 
 /*
  * Look up a previously registered custom format. Returns false if 'name' is
@@ -137,6 +147,7 @@ extern void RegisterCopyCustomFormat(const char *name, const CopyToRoutine *to,
  */
 extern bool GetCopyCustomFormatRoutines(const char *name, const CopyToRoutine **to,
 										const CopyFromRoutine **from,
-										ProcessOneOptionFn * option_fn);
+										ProcessOneOptionFn * option_fn,
+										ValidateOptionsFn * validate_fn);
 
 #endif							/* COPYAPI_H */
-- 
2.54.0

