From b5ff7ebea3141625b8699c85a3bf15972b1e1537 Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Mon, 22 Jun 2026 13:14:31 -0700 Subject: [PATCH v3 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