v2-0004-Add-test-module-for-COPY-custom-format.patch
text/x-patch
Filename: v2-0004-Add-test-module-for-COPY-custom-format.patch
Type: text/x-patch
Part: 1
Patch
Format: format-patch
Series: patch v2-0004
Subject: Add test module for COPY custom format.
| File | + | − |
|---|---|---|
| src/test/modules/Makefile | 1 | 0 |
| src/test/modules/meson.build | 1 | 0 |
| src/test/modules/test_copy_custom_format/expected/test_copy_custom_format.out | 105 | 0 |
| src/test/modules/test_copy_custom_format/Makefile | 20 | 0 |
| src/test/modules/test_copy_custom_format/meson.build | 32 | 0 |
| src/test/modules/test_copy_custom_format/sql/test_copy_custom_format.sql | 32 | 0 |
| src/test/modules/test_copy_custom_format/test_copy_custom_format.c | 169 | 0 |
| src/tools/pgindent/typedefs.list | 1 | 0 |
From d31d8cef68d10cb1817446af9a1e492ce88808e9 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 22 Jun 2026 13:14:31 -0700
Subject: [PATCH v2 4/4] Add test module for COPY custom format.
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch-through:
---
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
.../modules/test_copy_custom_format/Makefile | 20 +++
.../expected/test_copy_custom_format.out | 105 +++++++++++
.../test_copy_custom_format/meson.build | 32 ++++
.../sql/test_copy_custom_format.sql | 32 ++++
.../test_copy_custom_format.c | 169 ++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
8 files changed, 361 insertions(+)
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
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 0a74ab5c86f..6dcb66174f5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -23,6 +23,7 @@ SUBDIRS = \
test_cloexec \
test_checksums \
test_copy_callbacks \
+ test_copy_custom_format \
test_custom_rmgrs \
test_custom_stats \
test_custom_types \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 4bca42bb370..adfa413fe58 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -23,6 +23,7 @@ subdir('test_bloomfilter')
subdir('test_cloexec')
subdir('test_checksums')
subdir('test_copy_callbacks')
+subdir('test_copy_custom_format')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
subdir('test_custom_stats')
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..68a2a04ff09
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/Makefile
@@ -0,0 +1,20 @@
+# 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
+
+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..817ca3fa60f
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/expected/test_copy_custom_format.out
@@ -0,0 +1,105 @@
+LOAD 'test_copy_custom_format';
+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 TO stdout WITH (format 'test_format'); -- Start, OutFunc x3, OneRow x3, End
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToOutFunc: attribute: bigint
+NOTICE: CopyToStart: the number of attributes of table: 3, the number of attributes to output: 3
+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 FROM stdin WITH (format 'test_format'); -- InFunc x3, Start, OneRow, End
+NOTICE: CopyFromInFunc: attribute: smallint
+NOTICE: CopyFromInFunc: attribute: integer
+NOTICE: CopyFromInFunc: attribute: bigint
+NOTICE: CopyFromStart: the number of attributes of table: 3, the number of attributes to input: 3
+NOTICE: CopyFromOneRow
+NOTICE: CopyFromEnd
+COPY copy_data (a, b) TO stdout WITH (format 'test_format'); -- Start: natts 2
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToStart: the number of attributes of table: 3, the number of attributes to output: 2
+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 (SELECT a FROM copy_data) TO stdout WITH (format 'test_format'); -- Start: natts 1
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToStart: the number of attributes of table: 1, the number of attributes to output: 1
+NOTICE: CopyToOneRow: the number of valid values: 1
+NOTICE: CopyToOneRow: the number of valid values: 1
+NOTICE: CopyToOneRow: the number of valid values: 1
+NOTICE: CopyToEnd
+COPY copy_data TO stdout WITH (format 'nonexistent'); -- ERROR: not recognized
+ERROR: COPY format "nonexistent" not recognized
+LINE 1: COPY copy_data TO stdout WITH (format 'nonexistent');
+ ^
+COPY copy_data TO stdout WITH (format 'text', format 'csv'); -- ERROR: conflicting
+ERROR: conflicting or redundant options
+LINE 1: COPY copy_data TO stdout WITH (format 'text', format 'csv');
+ ^
+COPY copy_data TO stdout WITH (format 'test_format', bogus 1); -- ERROR
+ERROR: COPY format "test_format" does not accept option "bogus"
+LINE 1: ...PY copy_data TO stdout WITH (format 'test_format', bogus 1);
+ ^
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 5); -- OK
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToOutFunc: attribute: bigint
+NOTICE: CopyToStart: the number of attributes of table: 3, the number of attributes to output: 3
+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', max_attributes 3); -- OK
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToOutFunc: attribute: bigint
+NOTICE: CopyToStart: the number of attributes of table: 3, the number of attributes to output: 3
+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', max_attributes 2); -- ERROR: 3 columns exceeds 2
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToOutFunc: attribute: bigint
+ERROR: relation has 3 columns, exceeds max_attributes 2
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 0); -- ERROR: positive
+ERROR: "max_attributes" must be a positive integer
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes -1); -- ERROR
+ERROR: "max_attributes" must be a positive integer
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 'x'); -- ERROR: integer required
+ERROR: max_attributes requires an integer value
+COPY copy_data FROM stdin WITH (format 'test_format', freeze true, disallow_freeze true); -- ERROR (validate)
+ERROR: FREEZE cannot be used with "disallow_freeze" option
+COPY copy_data FROM stdin WITH (format 'test_format', disallow_freeze true); -- OK
+NOTICE: CopyFromInFunc: attribute: smallint
+NOTICE: CopyFromInFunc: attribute: integer
+NOTICE: CopyFromInFunc: attribute: bigint
+NOTICE: CopyFromStart: the number of attributes of table: 3, the number of attributes to input: 3
+NOTICE: CopyFromOneRow
+NOTICE: CopyFromEnd
+-- The built-in options are handled in the same way of built-in formats.
+COPY copy_data TO stdout WITH (format 'test_format', delimiter ',');
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToOutFunc: attribute: bigint
+NOTICE: CopyToStart: the number of attributes of table: 3, the number of attributes to output: 3
+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', quote '"');
+NOTICE: CopyToOutFunc: attribute: smallint
+NOTICE: CopyToOutFunc: attribute: integer
+NOTICE: CopyToOutFunc: attribute: bigint
+NOTICE: CopyToStart: the number of attributes of table: 3, the number of attributes to output: 3
+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', freeze true); -- ERROR: FREEZE with COPY TO
+ERROR: COPY FREEZE cannot be used with COPY TO
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..a231ed57649
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/meson.build
@@ -0,0 +1,32 @@
+# 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',
+ ],
+ # 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..59f58fa55a2
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/sql/test_copy_custom_format.sql
@@ -0,0 +1,32 @@
+LOAD 'test_copy_custom_format';
+
+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 TO stdout WITH (format 'test_format'); -- Start, OutFunc x3, OneRow x3, End
+COPY copy_data FROM stdin WITH (format 'test_format'); -- InFunc x3, Start, OneRow, End
+\.
+
+COPY copy_data (a, b) TO stdout WITH (format 'test_format'); -- Start: natts 2
+COPY (SELECT a FROM copy_data) TO stdout WITH (format 'test_format'); -- Start: natts 1
+
+COPY copy_data TO stdout WITH (format 'nonexistent'); -- ERROR: not recognized
+COPY copy_data TO stdout WITH (format 'text', format 'csv'); -- ERROR: conflicting
+
+COPY copy_data TO stdout WITH (format 'test_format', bogus 1); -- ERROR
+
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 5); -- OK
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 3); -- OK
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 2); -- ERROR: 3 columns exceeds 2
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 0); -- ERROR: positive
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes -1); -- ERROR
+COPY copy_data TO stdout WITH (format 'test_format', max_attributes 'x'); -- ERROR: integer required
+
+COPY copy_data FROM stdin WITH (format 'test_format', freeze true, disallow_freeze true); -- ERROR (validate)
+COPY copy_data FROM stdin WITH (format 'test_format', disallow_freeze true); -- OK
+\.
+
+-- The built-in options are handled in the same way of built-in formats.
+COPY copy_data TO stdout WITH (format 'test_format', delimiter ',');
+COPY copy_data TO stdout WITH (format 'test_format', quote '"');
+COPY copy_data TO stdout WITH (format 'test_format', freeze true); -- ERROR: FREEZE with COPY TO
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..ca25832fcb0
--- /dev/null
+++ b/src/test/modules/test_copy_custom_format/test_copy_custom_format.c
@@ -0,0 +1,169 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_copy_custom_format.c
+ * Code for testing custom COPY format.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_copy_custom_format/test_copy_custom_format.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "commands/copy.h"
+#include "commands/copyapi.h"
+#include "commands/copy_state.h"
+#include "commands/defrem.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+typedef struct TestCopyOptions
+{
+ int max_attributes;
+ bool disallow_freeze;
+} TestCopyOptions;
+
+static bool
+TestCopyProcessOneOption(CopyFormatOptions *opts, bool is_from, DefElem *option)
+{
+ TestCopyOptions *t = (TestCopyOptions *) opts->format_private_opts;
+
+ if (t == NULL)
+ {
+ t = palloc0_object(TestCopyOptions);
+ opts->format_private_opts = (void *) t;
+ }
+
+ if (strcmp(option->defname, "max_attributes") == 0)
+ {
+ int val = defGetInt32(option);
+
+ if (val < 1)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"max_attributes\" must be a positive integer"));
+
+ t->max_attributes = val;
+ return true;
+ }
+ else if (strcmp(option->defname, "disallow_freeze") == 0)
+ {
+ t->disallow_freeze = defGetBoolean(option);
+ return true;
+ }
+
+ return false;
+}
+
+static void
+TestCopyValidateOptions(CopyFormatOptions *opts, bool is_from)
+{
+ TestCopyOptions *t = (TestCopyOptions *) opts->format_private_opts;
+
+ if (!t)
+ return;
+
+ if (t->disallow_freeze && opts->freeze)
+ ereport(ERROR,
+ errmsg("FREEZE cannot be used with \"disallow_freeze\" option"));
+}
+
+static void
+TestCopyFromInFunc(CopyFromState cstate, Oid atttypid, FmgrInfo *finfo, Oid *typioparam)
+{
+ ereport(NOTICE,
+ errmsg("CopyFromInFunc: attribute: %s", format_type_be(atttypid)));
+}
+
+static void
+check_max_attributes(CopyFormatOptions *opts, TupleDesc tupDesc)
+{
+ TestCopyOptions *t = (TestCopyOptions *) opts->format_private_opts;
+
+ if (t != NULL && t->max_attributes > 0 && tupDesc->natts > t->max_attributes)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("relation has %d columns, exceeds max_attributes %d",
+ tupDesc->natts, t->max_attributes));
+}
+
+static void
+TestCopyFromStart(CopyFromState cstate, TupleDesc tupDesc)
+{
+ check_max_attributes(&cstate->opts, tupDesc);
+
+ ereport(NOTICE,
+ errmsg("CopyFromStart: the number of attributes of table: %d, the number of attributes to input: %d",
+ tupDesc->natts, list_length(cstate->attnumlist)));
+}
+
+static bool
+TestCopyFromOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls)
+{
+ 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 cstate, TupleDesc tupDesc)
+{
+ check_max_attributes(&cstate->opts, tupDesc);
+
+ ereport(NOTICE,
+ errmsg("CopyToStart: the number of attributes of table: %d, the number of attributes to output: %d",
+ tupDesc->natts, list_length(cstate->attnumlist)));
+}
+
+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 TestCopyToRoutine = {
+ .CopyToOutFunc = TestCopyToOutFunc,
+ .CopyToStart = TestCopyToStart,
+ .CopyToOneRow = TestCopyToOneRow,
+ .CopyToEnd = TestCopyToEnd,
+};
+
+
+static const CopyFromRoutine TestCopyFromRoutine = {
+ .CopyFromInFunc = TestCopyFromInFunc,
+ .CopyFromStart = TestCopyFromStart,
+ .CopyFromOneRow = TestCopyFromOneRow,
+ .CopyFromEnd = TestCopyFromEnd,
+};
+
+void
+_PG_init(void)
+{
+ RegisterCopyCustomFormat("test_format",
+ &TestCopyToRoutine,
+ &TestCopyFromRoutine,
+ &TestCopyProcessOneOption,
+ &TestCopyValidateOptions);
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5263710e451..552669abc5f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3179,6 +3179,7 @@ Tcl_Obj
Tcl_Size
Tcl_Time
TempNamespaceStatus
+TestCopyOptions
TestDSMRegistryHashEntry
TestDSMRegistryStruct
TestDecodingData
--
2.54.0