From 2718f015e44354dca5ba99927d6ef0aa2dbb3b1f Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 10 Nov 2025 22:18:28 -0800
Subject: [PATCH 6/6] Add regression test module for custom COPY format.

---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_copy_custom_format/.gitignore        |   3 +
 .../modules/test_copy_custom_format/Makefile  |  21 ++
 .../expected/test_copy_custom_format.out      |  97 ++++++++
 .../test_copy_custom_format/meson.build       |  35 +++
 .../sql/test_copy_custom_format.sql           |  34 +++
 .../test_copy_custom_format.c                 | 217 ++++++++++++++++++
 .../test_copy_custom_format.conf              |   1 +
 9 files changed, 410 insertions(+)
 create mode 100644 src/test/modules/test_copy_custom_format/.gitignore
 create mode 100644 src/test/modules/test_copy_custom_format/Makefile
 create mode 100644 src/test/modules/test_copy_custom_format/expected/test_copy_custom_format.out
 create mode 100644 src/test/modules/test_copy_custom_format/meson.build
 create mode 100644 src/test/modules/test_copy_custom_format/sql/test_copy_custom_format.sql
 create mode 100644 src/test/modules/test_copy_custom_format/test_copy_custom_format.c
 create mode 100644 src/test/modules/test_copy_custom_format/test_copy_custom_format.conf

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 902a7954101..710ccb74990 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -19,6 +19,7 @@ SUBDIRS = \
 		  test_bitmapset \
 		  test_bloomfilter \
 		  test_copy_callbacks \
+		  test_copy_custom_format \
 		  test_custom_rmgrs \
 		  test_ddl_deparse \
 		  test_dsa \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 14fc761c4cf..5a2e4e03b80 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -18,6 +18,7 @@ subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
+subdir('test_copy_custom_format')
 subdir('test_custom_rmgrs')
 subdir('test_ddl_deparse')
 subdir('test_dsa')
diff --git a/src/test/modules/test_copy_custom_format/.gitignore b/src/test/modules/test_copy_custom_format/.gitignore
new file mode 100644
index 00000000000..8d3af519eb1
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/.gitignore
@@ -0,0 +1,3 @@
+/log/
+/results/
+/tmp_check/
\ No newline at end of file
diff --git a/src/test/modules/test_copy_custom_format/Makefile b/src/test/modules/test_copy_custom_format/Makefile
new file mode 100644
index 00000000000..8ad1cebaba6
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/Makefile
@@ -0,0 +1,21 @@
+# src/test/modules/test_copy_custom_format/Makefile
+
+MODULE_big = test_copy_custom_format
+OBJS = \
+	$(WIN32RES) \
+	test_copy_custom_format.o
+PGFILEDESC = "test_copy_custom_format - test custom COPY FORMAT"
+
+REGRESS = test_copy_custom_format
+REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_copy_custom_format/test_copy_custom_format.conf
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_copy_custom_format
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_copy_custom_format/expected/test_copy_custom_format.out b/src/test/modules/test_copy_custom_format/expected/test_copy_custom_format.out
new file mode 100644
index 00000000000..a91ac293447
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/expected/test_copy_custom_format.out
@@ -0,0 +1,97 @@
+CREATE TABLE copy_data (a smallint, b integer, c bigint);
+INSERT INTO copy_data VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789);
+COPY copy_data FROM stdin WITH (FORMAT 'test_format');
+NOTICE:  CopyFromInFunc: attribute: smallint
+NOTICE:  CopyFromInFunc: attribute: integer
+NOTICE:  CopyFromInFunc: attribute: bigint
+NOTICE:  CopyFromStart: the number of attributes: 3
+NOTICE:  common_int 0 common_bool 0 from_str "(null)"
+NOTICE:  CopyFromOneRow
+NOTICE:  CopyFromEnd
+COPY copy_data TO stdout WITH (FORMAT 'test_format');
+NOTICE:  CopyToOutFunc: attribute: smallint
+NOTICE:  CopyToOutFunc: attribute: integer
+NOTICE:  CopyToOutFunc: attribute: bigint
+NOTICE:  CopyToStart: the number of attributes: 3
+NOTICE:  common_int 0 common_bool 0 to_str "(null)"
+NOTICE:  CopyToOneRow: the number of valid values: 3
+NOTICE:  CopyToOneRow: the number of valid values: 3
+NOTICE:  CopyToOneRow: the number of valid values: 3
+NOTICE:  CopyToEnd
+-- Error
+COPY copy_data FROM stdin WITH (FORMAT 'error');
+ERROR:  COPY format "error" not recognized
+LINE 1: COPY copy_data FROM stdin WITH (FORMAT 'error');
+                                        ^
+COPY copy_data FROM stdin WITH (FORMAT 'text', FORMAT 'error');
+ERROR:  conflicting or redundant options
+LINE 1: COPY copy_data FROM stdin WITH (FORMAT 'text', FORMAT 'error...
+                                                       ^
+-- Option handling, COPY FROM
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int 10);
+NOTICE:  CopyFromInFunc: attribute: smallint
+NOTICE:  CopyFromInFunc: attribute: integer
+NOTICE:  CopyFromInFunc: attribute: bigint
+NOTICE:  CopyFromStart: the number of attributes: 3
+NOTICE:  common_int 10 common_bool 0 from_str "(null)"
+NOTICE:  CopyFromOneRow
+NOTICE:  CopyFromEnd
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int -10);
+NOTICE:  CopyFromInFunc: attribute: smallint
+NOTICE:  CopyFromInFunc: attribute: integer
+NOTICE:  CopyFromInFunc: attribute: bigint
+NOTICE:  CopyFromStart: the number of attributes: 3
+NOTICE:  common_int -10 common_bool 0 from_str "(null)"
+NOTICE:  CopyFromOneRow
+NOTICE:  CopyFromEnd
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int 'a'); -- error
+ERROR:  common_int requires an integer value
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_bool 'true');
+NOTICE:  CopyFromInFunc: attribute: smallint
+NOTICE:  CopyFromInFunc: attribute: integer
+NOTICE:  CopyFromInFunc: attribute: bigint
+NOTICE:  CopyFromStart: the number of attributes: 3
+NOTICE:  common_int 0 common_bool 1 from_str "(null)"
+NOTICE:  CopyFromOneRow
+NOTICE:  CopyFromEnd
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_bool 'false');
+NOTICE:  CopyFromInFunc: attribute: smallint
+NOTICE:  CopyFromInFunc: attribute: integer
+NOTICE:  CopyFromInFunc: attribute: bigint
+NOTICE:  CopyFromStart: the number of attributes: 3
+NOTICE:  common_int 0 common_bool 0 from_str "(null)"
+NOTICE:  CopyFromOneRow
+NOTICE:  CopyFromEnd
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_bool 'hello'); -- error
+ERROR:  common_bool requires a Boolean value
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int 100, common_bool false, from_str 'from option');
+NOTICE:  CopyFromInFunc: attribute: smallint
+NOTICE:  CopyFromInFunc: attribute: integer
+NOTICE:  CopyFromInFunc: attribute: bigint
+NOTICE:  CopyFromStart: the number of attributes: 3
+NOTICE:  common_int 100 common_bool 0 from_str "from option"
+NOTICE:  CopyFromOneRow
+NOTICE:  CopyFromEnd
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', invalid 'option'); -- error
+ERROR:  COPY format "invalid" not recognized
+LINE 1: ... copy_data FROM stdin WITH (FORMAT 'test_format', invalid 'o...
+                                                             ^
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', format 'text'); -- error
+ERROR:  conflicting or redundant options
+LINE 1: ... copy_data FROM stdin WITH (FORMAT 'test_format', format 'te...
+                                                             ^
+-- Option handling, COPY FROM
+COPY copy_data TO stdout WITH (FORMAT 'test_format', common_int -10, common_bool false, to_str 'to option');
+NOTICE:  CopyToOutFunc: attribute: smallint
+NOTICE:  CopyToOutFunc: attribute: integer
+NOTICE:  CopyToOutFunc: attribute: bigint
+NOTICE:  CopyToStart: the number of attributes: 3
+NOTICE:  common_int -10 common_bool 0 to_str "to option"
+NOTICE:  CopyToOneRow: the number of valid values: 3
+NOTICE:  CopyToOneRow: the number of valid values: 3
+NOTICE:  CopyToOneRow: the number of valid values: 3
+NOTICE:  CopyToEnd
+COPY copy_data TO stdout WITH (FORMAT 'test_format', from_str 'to option'); -- error
+ERROR:  COPY format "from_str" not recognized
+LINE 1: ...Y copy_data TO stdout WITH (FORMAT 'test_format', from_str '...
+                                                             ^
diff --git a/src/test/modules/test_copy_custom_format/meson.build b/src/test/modules/test_copy_custom_format/meson.build
new file mode 100644
index 00000000000..6cc6c8b60fd
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+test_copy_custom_format_sources = files(
+'test_copy_custom_format.c',
+)
+
+if host_system == 'windows'
+  test_copy_custom_format_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_copy_custom_format',
+    '--FILEDESC', 'test_copy_custom_format - test custom COPY FORMAT',])
+endif
+
+test_copy_custom_format = shared_module('test_copy_custom_format',
+  test_copy_custom_format_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_copy_custom_format
+
+tests += {
+  'name': 'test_copy_custom_format',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_copy_custom_format',
+    ],
+    'regress_args': [
+      '--temp-config', files('test_copy_custom_format.conf'),
+    ],
+    # Disabled because these tests require
+    # "shared_preload_libraries=test_custom_copy_format", which typical
+    # runningcheck users do not have (e.g. buildfarm clients).
+    'runningcheck': false,
+  },
+}
diff --git a/src/test/modules/test_copy_custom_format/sql/test_copy_custom_format.sql b/src/test/modules/test_copy_custom_format/sql/test_copy_custom_format.sql
new file mode 100644
index 00000000000..b6564f0355b
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/sql/test_copy_custom_format.sql
@@ -0,0 +1,34 @@
+CREATE TABLE copy_data (a smallint, b integer, c bigint);
+INSERT INTO copy_data VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789);
+
+COPY copy_data FROM stdin WITH (FORMAT 'test_format');
+\.
+COPY copy_data TO stdout WITH (FORMAT 'test_format');
+
+-- Error
+COPY copy_data FROM stdin WITH (FORMAT 'error');
+COPY copy_data FROM stdin WITH (FORMAT 'text', FORMAT 'error');
+
+
+-- Option handling, COPY FROM
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int 10);
+\.
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int -10);
+\.
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int 'a'); -- error
+
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_bool 'true');
+\.
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_bool 'false');
+\.
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_bool 'hello'); -- error
+
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', common_int 100, common_bool false, from_str 'from option');
+\.
+
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', invalid 'option'); -- error
+COPY copy_data FROM stdin WITH (FORMAT 'test_format', format 'text'); -- error
+
+-- Option handling, COPY FROM
+COPY copy_data TO stdout WITH (FORMAT 'test_format', common_int -10, common_bool false, to_str 'to option');
+COPY copy_data TO stdout WITH (FORMAT 'test_format', from_str 'to option'); -- error
diff --git a/src/test/modules/test_copy_custom_format/test_copy_custom_format.c b/src/test/modules/test_copy_custom_format/test_copy_custom_format.c
new file mode 100644
index 00000000000..a63390e875b
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/test_copy_custom_format.c
@@ -0,0 +1,217 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_copy_custom_format.c
+ *		Code for testing custom COPY format.
+ *
+ * Portions Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_copy_custom_format/test_copy_custom_format.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "commands/copystate.h"
+#include "commands/copyapi.h"
+#include "commands/defrem.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+typedef struct TestCopyCommonOption
+{
+	int			common_int;
+	bool		common_bool;
+}			TestCopyCommonOption;
+
+typedef struct TestCopyFromState
+{
+	CopyFromStateData base;
+
+	TestCopyCommonOption common_opts;
+	char	   *from_str_option;
+}			TestCopyFromState;
+
+typedef struct TestCopyToState
+{
+	CopyToStateData base;
+
+	TestCopyCommonOption common_opts;
+	char	   *to_str_option;
+}			TestCopyToState;
+
+static Size
+TestCopyToEsimateSpace(void)
+{
+	return sizeof(TestCopyToState);
+}
+
+static Size
+TestCopyFromEsimateSpace(void)
+{
+	return sizeof(TestCopyFromState);
+}
+
+static bool
+TestCopyProcessCommonOption(TestCopyCommonOption * opt, DefElem *option)
+{
+	if (strcmp(option->defname, "common_int") == 0)
+	{
+		int			val = defGetInt32(option);
+
+		opt->common_int = val;
+
+		return true;
+	}
+	else if (strcmp(option->defname, "common_bool") == 0)
+	{
+		bool		val = defGetBoolean(option);
+
+		opt->common_bool = val;
+
+		return true;
+	}
+
+	return false;
+}
+
+static bool
+TestCopyFromProcessOneOption(CopyFromState ccstate, DefElem *option)
+{
+	TestCopyFromState *cstate = (TestCopyFromState *) ccstate;
+
+	if (TestCopyProcessCommonOption(&cstate->common_opts, option))
+	{
+		return true;
+	}
+	else if (strcmp(option->defname, "from_str") == 0)
+	{
+		char	   *val = defGetString(option);
+
+		cstate->from_str_option = val;
+		return true;
+	}
+
+	return false;
+}
+
+static bool
+TestCopyToProcessOneOption(CopyToState ccstate, DefElem *option)
+{
+	TestCopyToState *cstate = (TestCopyToState *) ccstate;
+
+	if (TestCopyProcessCommonOption(&cstate->common_opts, option))
+	{
+		return true;
+	}
+	else if (strcmp(option->defname, "to_str") == 0)
+	{
+		char	   *val = defGetString(option);
+
+		cstate->to_str_option = val;
+		return true;
+	}
+
+	return false;
+}
+
+static void
+TestCopyFromInFunc(CopyFromState cstate, Oid atttypid,
+				   FmgrInfo *finfo, Oid *typioparam)
+{
+	ereport(NOTICE,
+			errmsg("CopyFromInFunc: attribute: %s", format_type_be(atttypid)));
+}
+
+static void
+TestCopyFromStart(CopyFromState ccstate, TupleDesc tupDesc)
+{
+	TestCopyFromState *cstate = (TestCopyFromState *) ccstate;
+
+	ereport(NOTICE,
+			errmsg("CopyFromStart: the number of attributes: %d", tupDesc->natts));
+	ereport(NOTICE,
+			errmsg("common_int %d common_bool %d from_str \"%s\"",
+				   cstate->common_opts.common_int,
+				   cstate->common_opts.common_bool,
+				   cstate->from_str_option));
+}
+
+static bool
+TestCopyFromOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls,
+				   CopyFromRowInfo * rowinfo)
+{
+	ereport(NOTICE,
+			errmsg("CopyFromOneRow"));
+
+	return false;
+}
+
+static void
+TestCopyFromEnd(CopyFromState cstate)
+{
+	ereport(NOTICE,
+			errmsg("CopyFromEnd"));
+}
+
+static void
+TestCopyToOutFunc(CopyToState cstate, Oid atttypid, FmgrInfo *finfo)
+{
+	ereport(NOTICE,
+			errmsg("CopyToOutFunc: attribute: %s", format_type_be(atttypid)));
+}
+
+static void
+TestCopyToStart(CopyToState ccstate, TupleDesc tupDesc)
+{
+	TestCopyToState *cstate = (TestCopyToState *) ccstate;
+
+	ereport(NOTICE,
+			errmsg("CopyToStart: the number of attributes: %d", tupDesc->natts));
+	ereport(NOTICE,
+			errmsg("common_int %d common_bool %d to_str \"%s\"",
+				   cstate->common_opts.common_int,
+				   cstate->common_opts.common_bool,
+				   cstate->to_str_option));
+}
+
+static void
+TestCopyToOneRow(CopyToState cstate, TupleTableSlot *slot)
+{
+	ereport(NOTICE, (errmsg("CopyToOneRow: the number of valid values: %u", slot->tts_nvalid)));
+}
+
+static void
+TestCopyToEnd(CopyToState cstate)
+{
+	ereport(NOTICE, (errmsg("CopyToEnd")));
+}
+
+static const CopyToRoutine CopyToRoutineTestCopyFormat = {
+	.CopyToEstimateStateSpace = TestCopyToEsimateSpace,
+	.CopyToProcessOneOption = TestCopyToProcessOneOption,
+	.CopyToOutFunc = TestCopyToOutFunc,
+	.CopyToStart = TestCopyToStart,
+	.CopyToOneRow = TestCopyToOneRow,
+	.CopyToEnd = TestCopyToEnd,
+};
+
+
+static const CopyFromRoutine CopyFromRoutineTestCopyFormat = {
+	.CopyFromEstimateStateSpace = TestCopyFromEsimateSpace,
+	.CopyFromProcessOneOption = TestCopyFromProcessOneOption,
+	.CopyFromInFunc = TestCopyFromInFunc,
+	.CopyFromStart = TestCopyFromStart,
+	.CopyFromOneRow = TestCopyFromOneRow,
+	.CopyFromEnd = TestCopyFromEnd,
+};
+
+void
+_PG_init(void)
+{
+	RegisterCopyCustomFormat("test_format",
+							 &CopyFromRoutineTestCopyFormat,
+							 &CopyToRoutineTestCopyFormat);
+}
diff --git a/src/test/modules/test_copy_custom_format/test_copy_custom_format.conf b/src/test/modules/test_copy_custom_format/test_copy_custom_format.conf
new file mode 100644
index 00000000000..fb7cf22a3b1
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/test_copy_custom_format.conf
@@ -0,0 +1 @@
+shared_preload_libraries = 'test_copy_custom_format'
-- 
2.47.3

