=== Applying patches on top of PostgreSQL commit ID 2da74d8d6400975bf73fb0df97c3943ad3ed9a36 === /etc/rc.d/jail: WARNING: Per-jail configuration via jail_* variables is obsolete. Please consider migrating to /etc/jail.conf. Thu Apr 3 11:47:29 UTC 2025 On branch cf/4681 nothing to commit, working tree clean === using 'git am' to apply patch ./v39-0001-Export-CopyDest-as-private-data.patch === Applying: Export CopyDest as private data === using 'git am' to apply patch ./v39-0002-Add-support-for-adding-custom-COPY-format.patch === Applying: Add support for adding custom COPY format error: sha1 information is lacking or useless (src/backend/commands/copyfrom.c). error: could not build fake ancestor hint: Use 'git am --show-current-patch=diff' to see the failed patch Patch failed at 0001 Add support for adding custom COPY format When you have resolved this problem, run "git am --continue". If you prefer to skip this patch, run "git am --skip" instead. To restore the original branch and stop patching, run "git am --abort". === using patch(1) to apply patch ./v39-0002-Add-support-for-adding-custom-COPY-format.patch === patch: unrecognized option `--no-backup-if-mismatch' usage: patch [-bCcEeflNnRstuv] [-B backup-prefix] [-D symbol] [-d directory] [-F max-fuzz] [-i patchfile] [-o out-file] [-p strip-count] [-r rej-name] [-V t | nil | never | none] [-x number] [-z backup-ext] [--posix] [origfile [patchfile]] patch binary = true; else - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("COPY format \"%s\" not recognized", fmt), - parser_errposition(pstate, defel->location))); + { + List *qualified_format; + Oid arg_types[1]; + Oid handler = InvalidOid; + + qualified_format = stringToQualifiedNameList(fmt, NULL); + arg_types[0] = INTERNALOID; + handler = LookupFuncName(qualified_format, 1, + arg_types, true); + if (!OidIsValid(handler)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY format \"%s\" not recognized", fmt), + parser_errposition(pstate, defel->location))); + + /* check that handler has correct return type */ + if (get_func_rettype(handler) != COPY_HANDLEROID) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("function %s must return type %s", + fmt, "copy_handler"), + parser_errposition(pstate, defel->location))); + + opts_out->handler = handler; + } } else if (strcmp(defel->defname, "freeze") == 0) { diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index b4dad744547..3d86e8a8328 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -129,6 +129,7 @@ static void CopyFromBinaryEnd(CopyFromState cstate); /* text format */ static const CopyFromRoutine CopyFromRoutineText = { + .type = T_CopyFromRoutine, .CopyFromInFunc = CopyFromTextLikeInFunc, .CopyFromStart = CopyFromTextLikeStart, .CopyFromOneRow = CopyFromTextOneRow, @@ -137,6 +138,7 @@ static const CopyFromRoutine CopyFromRoutineText = { /* CSV format */ static const CopyFromRoutine CopyFromRoutineCSV = { + .type = T_CopyFromRoutine, .CopyFromInFunc = CopyFromTextLikeInFunc, .CopyFromStart = CopyFromTextLikeStart, .CopyFromOneRow = CopyFromCSVOneRow, @@ -145,6 +147,7 @@ static const CopyFromRoutine CopyFromRoutineCSV = { /* binary format */ static const CopyFromRoutine CopyFromRoutineBinary = { + .type = T_CopyFromRoutine, .CopyFromInFunc = CopyFromBinaryInFunc, .CopyFromStart = CopyFromBinaryStart, .CopyFromOneRow = CopyFromBinaryOneRow, @@ -155,7 +158,22 @@ static const CopyFromRoutine CopyFromRoutineBinary = { static const CopyFromRoutine * CopyFromGetRoutine(const CopyFormatOptions *opts) { - if (opts->csv_mode) + if (OidIsValid(opts->handler)) + { + Datum datum; + Node *routine; + + datum = OidFunctionCall1(opts->handler, BoolGetDatum(true)); + routine = (Node *) DatumGetPointer(datum); + if (routine == NULL || !IsA(routine, CopyFromRoutine)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY handler function %s.%s did not return CopyFromRoutine struct", + get_namespace_name(get_func_namespace(opts->handler)), + get_func_name(opts->handler)))); + return castNode(CopyFromRoutine, routine); + } + else if (opts->csv_mode) return &CopyFromRoutineCSV; else if (opts->binary) return &CopyFromRoutineBinary; diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index 05ad87d8220..b7ff6466ce3 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -37,56 +37,6 @@ #include "utils/rel.h" #include "utils/snapmgr.h" -/* - * This struct contains all the state variables used throughout a COPY TO - * operation. - * - * Multi-byte encodings: all supported client-side encodings encode multi-byte - * characters by having the first byte's high bit set. Subsequent bytes of the - * character can have the high bit not set. When scanning data in such an - * encoding to look for a match to a single-byte (ie ASCII) character, we must - * use the full pg_encoding_mblen() machinery to skip over multibyte - * characters, else we might find a false match to a trailing byte. In - * supported server encodings, there is no possibility of a false match, and - * it's faster to make useless comparisons to trailing bytes than it is to - * invoke pg_encoding_mblen() to skip over them. encoding_embeds_ascii is true - * when we have to do it the hard way. - */ -typedef struct CopyToStateData -{ - /* format-specific routines */ - const CopyToRoutine *routine; - - /* low-level state data */ - CopyDest copy_dest; /* type of copy source/destination */ - FILE *copy_file; /* used if copy_dest == COPY_FILE */ - StringInfo fe_msgbuf; /* used for all dests during COPY TO */ - - int file_encoding; /* file or remote side's character encoding */ - bool need_transcoding; /* file encoding diff from server? */ - bool encoding_embeds_ascii; /* ASCII can be non-first byte? */ - - /* parameters from the COPY command */ - Relation rel; /* relation to copy to */ - QueryDesc *queryDesc; /* executable query to copy from */ - List *attnumlist; /* integer list of attnums to copy */ - char *filename; /* filename, or NULL for STDOUT */ - bool is_program; /* is 'filename' a program to popen? */ - copy_data_dest_cb data_dest_cb; /* function for writing data */ - - CopyFormatOptions opts; - Node *whereClause; /* WHERE condition (or NULL) */ - - /* - * Working state - */ - MemoryContext copycontext; /* per-copy execution context */ - - FmgrInfo *out_functions; /* lookup info for output functions */ - MemoryContext rowcontext; /* per-row evaluation context */ - uint64 bytes_processed; /* number of bytes processed so far */ -} CopyToStateData; - /* DestReceiver for COPY (query) TO */ typedef struct { @@ -140,6 +90,7 @@ static void CopySendInt16(CopyToState cstate, int16 val); /* text format */ static const CopyToRoutine CopyToRoutineText = { + .type = T_CopyToRoutine, .CopyToStart = CopyToTextLikeStart, .CopyToOutFunc = CopyToTextLikeOutFunc, .CopyToOneRow = CopyToTextOneRow, @@ -148,6 +99,7 @@ static const CopyToRoutine CopyToRoutineText = { /* CSV format */ static const CopyToRoutine CopyToRoutineCSV = { + .type = T_CopyToRoutine, .CopyToStart = CopyToTextLikeStart, .CopyToOutFunc = CopyToTextLikeOutFunc, .CopyToOneRow = CopyToCSVOneRow, @@ -156,6 +108,7 @@ static const CopyToRoutine CopyToRoutineCSV = { /* binary format */ static const CopyToRoutine CopyToRoutineBinary = { + .type = T_CopyToRoutine, .CopyToStart = CopyToBinaryStart, .CopyToOutFunc = CopyToBinaryOutFunc, .CopyToOneRow = CopyToBinaryOneRow, @@ -166,7 +119,22 @@ static const CopyToRoutine CopyToRoutineBinary = { static const CopyToRoutine * CopyToGetRoutine(const CopyFormatOptions *opts) { - if (opts->csv_mode) + if (OidIsValid(opts->handler)) + { + Datum datum; + Node *routine; + + datum = OidFunctionCall1(opts->handler, BoolGetDatum(false)); + routine = (Node *) DatumGetPointer(datum); + if (routine == NULL || !IsA(routine, CopyToRoutine)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY handler function %s.%s did not return CopyToRoutine struct", + get_namespace_name(get_func_namespace(opts->handler)), + get_func_name(opts->handler)))); + return castNode(CopyToRoutine, routine); + } + else if (opts->csv_mode) return &CopyToRoutineCSV; else if (opts->binary) return &CopyToRoutineBinary; diff --git a/src/backend/nodes/Makefile b/src/backend/nodes/Makefile index 77ddb9ca53f..dc6c1087361 100644 --- a/src/backend/nodes/Makefile +++ b/src/backend/nodes/Makefile @@ -50,6 +50,7 @@ node_headers = \ access/sdir.h \ access/tableam.h \ access/tsmapi.h \ + commands/copyapi.h \ commands/event_trigger.h \ commands/trigger.h \ executor/tuptable.h \ diff --git a/src/backend/nodes/gen_node_support.pl b/src/backend/nodes/gen_node_support.pl old mode 100644 new mode 100755 index f6229089cd1..ffe21462bec --- a/src/backend/nodes/gen_node_support.pl +++ b/src/backend/nodes/gen_node_support.pl @@ -62,6 +62,7 @@ my @all_input_files = qw( access/sdir.h access/tableam.h access/tsmapi.h + commands/copyapi.h commands/event_trigger.h commands/trigger.h executor/tuptable.h @@ -86,6 +87,7 @@ my @nodetag_only_files = qw( access/sdir.h access/tableam.h access/tsmapi.h + commands/copyapi.h commands/event_trigger.h commands/trigger.h executor/tuptable.h diff --git a/src/backend/utils/adt/pseudotypes.c b/src/backend/utils/adt/pseudotypes.c index 317a1f2b282..f2ebc21ca56 100644 --- a/src/backend/utils/adt/pseudotypes.c +++ b/src/backend/utils/adt/pseudotypes.c @@ -370,6 +370,7 @@ PSEUDOTYPE_DUMMY_IO_FUNCS(fdw_handler); PSEUDOTYPE_DUMMY_IO_FUNCS(table_am_handler); PSEUDOTYPE_DUMMY_IO_FUNCS(index_am_handler); PSEUDOTYPE_DUMMY_IO_FUNCS(tsm_handler); +PSEUDOTYPE_DUMMY_IO_FUNCS(copy_handler); PSEUDOTYPE_DUMMY_IO_FUNCS(internal); PSEUDOTYPE_DUMMY_IO_FUNCS(anyelement); PSEUDOTYPE_DUMMY_IO_FUNCS(anynonarray); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index a28a15993a2..e7147df367b 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7864,6 +7864,12 @@ { oid => '3312', descr => 'I/O', proname => 'tsm_handler_out', prorettype => 'cstring', proargtypes => 'tsm_handler', prosrc => 'tsm_handler_out' }, +{ oid => '8753', descr => 'I/O', + proname => 'copy_handler_in', proisstrict => 'f', prorettype => 'copy_handler', + proargtypes => 'cstring', prosrc => 'copy_handler_in' }, +{ oid => '8754', descr => 'I/O', + proname => 'copy_handler_out', prorettype => 'cstring', + proargtypes => 'copy_handler', prosrc => 'copy_handler_out' }, { oid => '267', descr => 'I/O', proname => 'table_am_handler_in', proisstrict => 'f', prorettype => 'table_am_handler', proargtypes => 'cstring', diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat index 6dca77e0a22..bddf9fb4fbe 100644 --- a/src/include/catalog/pg_type.dat +++ b/src/include/catalog/pg_type.dat @@ -633,6 +633,12 @@ typcategory => 'P', typinput => 'tsm_handler_in', typoutput => 'tsm_handler_out', typreceive => '-', typsend => '-', typalign => 'i' }, +{ oid => '8752', + descr => 'pseudo-type for the result of a COPY TO/FROM handler function', + typname => 'copy_handler', typlen => '4', typbyval => 't', typtype => 'p', + typcategory => 'P', typinput => 'copy_handler_in', + typoutput => 'copy_handler_out', typreceive => '-', typsend => '-', + typalign => 'i' }, { oid => '269', descr => 'pseudo-type for the result of a table AM handler function', typname => 'table_am_handler', typlen => '4', typbyval => 't', typtype => 'p', diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index 06dfdfef721..6df1f8a3b9b 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -87,9 +87,10 @@ typedef struct CopyFormatOptions CopyLogVerbosityChoice log_verbosity; /* verbosity of logged messages */ int64 reject_limit; /* maximum tolerable number of errors */ List *convert_select; /* list of column names (can be NIL) */ + Oid handler; /* handler function for custom format routine */ } CopyFormatOptions; -/* These are private in commands/copy[from|to].c */ +/* These are private in commands/copy[from|to]_internal.h */ typedef struct CopyFromStateData *CopyFromState; typedef struct CopyToStateData *CopyToState; diff --git a/src/include/commands/copyapi.h b/src/include/commands/copyapi.h index 2a2d2f9876b..53ad3337f86 100644 --- a/src/include/commands/copyapi.h +++ b/src/include/commands/copyapi.h @@ -22,6 +22,8 @@ */ typedef struct CopyToRoutine { + NodeTag type; + /* * Set output function information. This callback is called once at the * beginning of COPY TO. @@ -60,6 +62,8 @@ typedef struct CopyToRoutine */ typedef struct CopyFromRoutine { + NodeTag type; + /* * Set input function information. This callback is called once at the * beginning of COPY FROM. diff --git a/src/include/commands/copyto_internal.h b/src/include/commands/copyto_internal.h index 42ddb37a8a2..12c4a0f5979 100644 --- a/src/include/commands/copyto_internal.h +++ b/src/include/commands/copyto_internal.h @@ -14,6 +14,11 @@ #ifndef COPYTO_INTERNAL_H #define COPYTO_INTERNAL_H +#include "commands/copy.h" +#include "executor/execdesc.h" +#include "executor/tuptable.h" +#include "nodes/execnodes.h" + /* * Represents the different dest cases we need to worry about at * the bottom level @@ -25,4 +30,54 @@ typedef enum CopyDest COPY_DEST_CALLBACK, /* to callback function */ } CopyDest; +/* + * This struct contains all the state variables used throughout a COPY TO + * operation. + * + * Multi-byte encodings: all supported client-side encodings encode multi-byte + * characters by having the first byte's high bit set. Subsequent bytes of the + * character can have the high bit not set. When scanning data in such an + * encoding to look for a match to a single-byte (ie ASCII) character, we must + * use the full pg_encoding_mblen() machinery to skip over multibyte + * characters, else we might find a false match to a trailing byte. In + * supported server encodings, there is no possibility of a false match, and + * it's faster to make useless comparisons to trailing bytes than it is to + * invoke pg_encoding_mblen() to skip over them. encoding_embeds_ascii is true + * when we have to do it the hard way. + */ +typedef struct CopyToStateData +{ + /* format-specific routines */ + const CopyToRoutine *routine; + + /* low-level state data */ + CopyDest copy_dest; /* type of copy source/destination */ + FILE *copy_file; /* used if copy_dest == COPY_FILE */ + StringInfo fe_msgbuf; /* used for all dests during COPY TO */ + + int file_encoding; /* file or remote side's character encoding */ + bool need_transcoding; /* file encoding diff from server? */ + bool encoding_embeds_ascii; /* ASCII can be non-first byte? */ + + /* parameters from the COPY command */ + Relation rel; /* relation to copy to */ + QueryDesc *queryDesc; /* executable query to copy from */ + List *attnumlist; /* integer list of attnums to copy */ + char *filename; /* filename, or NULL for STDOUT */ + bool is_program; /* is 'filename' a program to popen? */ + copy_data_dest_cb data_dest_cb; /* function for writing data */ + + CopyFormatOptions opts; + Node *whereClause; /* WHERE condition (or NULL) */ + + /* + * Working state + */ + MemoryContext copycontext; /* per-copy execution context */ + + FmgrInfo *out_functions; /* lookup info for output functions */ + MemoryContext rowcontext; /* per-row evaluation context */ + uint64 bytes_processed; /* number of bytes processed so far */ +} CopyToStateData; + #endif /* COPYTO_INTERNAL_H */ diff --git a/src/include/nodes/meson.build b/src/include/nodes/meson.build index d1ca24dd32f..96e70e7f38b 100644 --- a/src/include/nodes/meson.build +++ b/src/include/nodes/meson.build @@ -12,6 +12,7 @@ node_support_input_i = [ 'access/sdir.h', 'access/tableam.h', 'access/tsmapi.h', + 'commands/copyapi.h', 'commands/event_trigger.h', 'commands/trigger.h', 'executor/tuptable.h', diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index aa1d27bbed3..9bf5d58cdae 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -17,6 +17,7 @@ SUBDIRS = \ test_aio \ test_bloomfilter \ test_copy_callbacks \ + test_copy_format \ test_custom_rmgrs \ test_ddl_deparse \ test_dsa \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 9de0057bd1d..5fd06de2737 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -16,6 +16,7 @@ subdir('ssl_passphrase_callback') subdir('test_aio') subdir('test_bloomfilter') subdir('test_copy_callbacks') +subdir('test_copy_format') subdir('test_custom_rmgrs') subdir('test_ddl_deparse') subdir('test_dsa') diff --git a/src/test/modules/test_copy_format/.gitignore b/src/test/modules/test_copy_format/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/src/test/modules/test_copy_format/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_copy_format/Makefile b/src/test/modules/test_copy_format/Makefile new file mode 100644 index 00000000000..8497f91624d --- /dev/null +++ b/src/test/modules/test_copy_format/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_copy_format/Makefile + +MODULE_big = test_copy_format +OBJS = \ + $(WIN32RES) \ + test_copy_format.o +PGFILEDESC = "test_copy_format - test custom COPY FORMAT" + +EXTENSION = test_copy_format +DATA = test_copy_format--1.0.sql + +REGRESS = test_copy_format + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_copy_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_format/expected/invalid.out b/src/test/modules/test_copy_format/expected/invalid.out new file mode 100644 index 00000000000..306c9928431 --- /dev/null +++ b/src/test/modules/test_copy_format/expected/invalid.out @@ -0,0 +1,61 @@ +CREATE SCHEMA test_schema; +CREATE EXTENSION test_copy_format WITH SCHEMA test_schema; +CREATE TABLE public.test (a smallint, b integer, c bigint); +INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); +SET search_path = public,test_schema; +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_wrong_input_type'); +ERROR: COPY format "test_copy_format_wrong_input_type" not recognized +LINE 1: COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_w... + ^ +COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wrong_input_type'); +ERROR: COPY format "test_copy_format_wrong_input_type" not recognized +LINE 1: COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wr... + ^ +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_wrong_return_type'); +ERROR: function test_copy_format_wrong_return_type must return type copy_handler +LINE 1: COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_w... + ^ +COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wrong_return_type'); +ERROR: function test_copy_format_wrong_return_type must return type copy_handler +LINE 1: COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wr... + ^ +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_wrong_return_value'); +ERROR: COPY handler function test_schema.test_copy_format_wrong_return_value did not return CopyFromRoutine struct +COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wrong_return_value'); +ERROR: COPY handler function test_schema.test_copy_format_wrong_return_value did not return CopyToRoutine struct +RESET search_path; +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format_wrong_input_type'); +ERROR: COPY format "test_schema.test_copy_format_wrong_input_type" not recognized +LINE 1: COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_c... + ^ +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format_wrong_input_type'); +ERROR: COPY format "test_schema.test_copy_format_wrong_input_type" not recognized +LINE 1: COPY public.test TO stdout WITH (FORMAT 'test_schema.test_co... + ^ +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format_wrong_return_type'); +ERROR: function test_schema.test_copy_format_wrong_return_type must return type copy_handler +LINE 1: COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_c... + ^ +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format_wrong_return_type'); +ERROR: function test_schema.test_copy_format_wrong_return_type must return type copy_handler +LINE 1: COPY public.test TO stdout WITH (FORMAT 'test_schema.test_co... + ^ +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format_wrong_return_value'); +ERROR: COPY handler function test_schema.test_copy_format_wrong_return_value did not return CopyFromRoutine struct +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format_wrong_return_value'); +ERROR: COPY handler function test_schema.test_copy_format_wrong_return_value did not return CopyToRoutine struct +COPY public.test FROM stdin WITH (FORMAT 'nonexistent'); +ERROR: COPY format "nonexistent" not recognized +LINE 1: COPY public.test FROM stdin WITH (FORMAT 'nonexistent'); + ^ +COPY public.test TO stdout WITH (FORMAT 'nonexistent'); +ERROR: COPY format "nonexistent" not recognized +LINE 1: COPY public.test TO stdout WITH (FORMAT 'nonexistent'); + ^ +COPY public.test FROM stdin WITH (FORMAT 'invalid.qualified.name'); +ERROR: cross-database references are not implemented: invalid.qualified.name +COPY public.test TO stdout WITH (FORMAT 'invalid.qualified.name'); +ERROR: cross-database references are not implemented: invalid.qualified.name +DROP TABLE public.test; +DROP EXTENSION test_copy_format; +DROP SCHEMA test_schema; diff --git a/src/test/modules/test_copy_format/expected/no_schema.out b/src/test/modules/test_copy_format/expected/no_schema.out new file mode 100644 index 00000000000..d5903632b2e --- /dev/null +++ b/src/test/modules/test_copy_format/expected/no_schema.out @@ -0,0 +1,23 @@ +CREATE EXTENSION test_copy_format; +CREATE TABLE public.test (a smallint, b integer, c bigint); +INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format'); +NOTICE: test_copy_format: is_from=true +NOTICE: CopyFromInFunc: attribute: smallint +NOTICE: CopyFromInFunc: attribute: integer +NOTICE: CopyFromInFunc: attribute: bigint +NOTICE: CopyFromStart: the number of attributes: 3 +NOTICE: CopyFromOneRow +NOTICE: CopyFromEnd +COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); +NOTICE: test_copy_format: is_from=false +NOTICE: CopyToOutFunc: attribute: smallint +NOTICE: CopyToOutFunc: attribute: integer +NOTICE: CopyToOutFunc: attribute: bigint +NOTICE: CopyToStart: the number of attributes: 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 +DROP TABLE public.test; +DROP EXTENSION test_copy_format; diff --git a/src/test/modules/test_copy_format/expected/schema.out b/src/test/modules/test_copy_format/expected/schema.out new file mode 100644 index 00000000000..698189fbeae --- /dev/null +++ b/src/test/modules/test_copy_format/expected/schema.out @@ -0,0 +1,56 @@ +CREATE SCHEMA test_schema; +CREATE EXTENSION test_copy_format WITH SCHEMA test_schema; +CREATE TABLE public.test (a smallint, b integer, c bigint); +INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); +-- Qualified name +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format'); +NOTICE: test_copy_format: is_from=true +NOTICE: CopyFromInFunc: attribute: smallint +NOTICE: CopyFromInFunc: attribute: integer +NOTICE: CopyFromInFunc: attribute: bigint +NOTICE: CopyFromStart: the number of attributes: 3 +NOTICE: CopyFromOneRow +NOTICE: CopyFromEnd +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format'); +NOTICE: test_copy_format: is_from=false +NOTICE: CopyToOutFunc: attribute: smallint +NOTICE: CopyToOutFunc: attribute: integer +NOTICE: CopyToOutFunc: attribute: bigint +NOTICE: CopyToStart: the number of attributes: 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 +-- No schema, no search path +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format'); +ERROR: COPY format "test_copy_format" not recognized +LINE 1: COPY public.test FROM stdin WITH (FORMAT 'test_copy_format')... + ^ +COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); +ERROR: COPY format "test_copy_format" not recognized +LINE 1: COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); + ^ +-- No schema, with search path +SET search_path = test_schema,public; +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format'); +NOTICE: test_copy_format: is_from=true +NOTICE: CopyFromInFunc: attribute: smallint +NOTICE: CopyFromInFunc: attribute: integer +NOTICE: CopyFromInFunc: attribute: bigint +NOTICE: CopyFromStart: the number of attributes: 3 +NOTICE: CopyFromOneRow +NOTICE: CopyFromEnd +COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); +NOTICE: test_copy_format: is_from=false +NOTICE: CopyToOutFunc: attribute: smallint +NOTICE: CopyToOutFunc: attribute: integer +NOTICE: CopyToOutFunc: attribute: bigint +NOTICE: CopyToStart: the number of attributes: 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 +RESET search_path; +DROP TABLE public.test; +DROP EXTENSION test_copy_format; +DROP SCHEMA test_schema; diff --git a/src/test/modules/test_copy_format/meson.build b/src/test/modules/test_copy_format/meson.build new file mode 100644 index 00000000000..8010659585b --- /dev/null +++ b/src/test/modules/test_copy_format/meson.build @@ -0,0 +1,35 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +test_copy_format_sources = files( + 'test_copy_format.c', +) + +if host_system == 'windows' + test_copy_format_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_copy_format', + '--FILEDESC', 'test_copy_format - test custom COPY FORMAT',]) +endif + +test_copy_format = shared_module('test_copy_format', + test_copy_format_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_copy_format + +test_install_data += files( + 'test_copy_format.control', + 'test_copy_format--1.0.sql', +) + +tests += { + 'name': 'test_copy_format', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'invalid', + 'no_schema', + 'schema', + ], + }, +} diff --git a/src/test/modules/test_copy_format/sql/invalid.sql b/src/test/modules/test_copy_format/sql/invalid.sql new file mode 100644 index 00000000000..e475f6a38c6 --- /dev/null +++ b/src/test/modules/test_copy_format/sql/invalid.sql @@ -0,0 +1,29 @@ +CREATE SCHEMA test_schema; +CREATE EXTENSION test_copy_format WITH SCHEMA test_schema; +CREATE TABLE public.test (a smallint, b integer, c bigint); +INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); + +SET search_path = public,test_schema; +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_wrong_input_type'); +COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wrong_input_type'); +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_wrong_return_type'); +COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wrong_return_type'); +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format_wrong_return_value'); +COPY public.test TO stdout WITH (FORMAT 'test_copy_format_wrong_return_value'); +RESET search_path; + +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format_wrong_input_type'); +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format_wrong_input_type'); +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format_wrong_return_type'); +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format_wrong_return_type'); +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format_wrong_return_value'); +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format_wrong_return_value'); + +COPY public.test FROM stdin WITH (FORMAT 'nonexistent'); +COPY public.test TO stdout WITH (FORMAT 'nonexistent'); +COPY public.test FROM stdin WITH (FORMAT 'invalid.qualified.name'); +COPY public.test TO stdout WITH (FORMAT 'invalid.qualified.name'); + +DROP TABLE public.test; +DROP EXTENSION test_copy_format; +DROP SCHEMA test_schema; diff --git a/src/test/modules/test_copy_format/sql/no_schema.sql b/src/test/modules/test_copy_format/sql/no_schema.sql new file mode 100644 index 00000000000..1e049f799f0 --- /dev/null +++ b/src/test/modules/test_copy_format/sql/no_schema.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION test_copy_format; +CREATE TABLE public.test (a smallint, b integer, c bigint); +INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format'); +\. +COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); +DROP TABLE public.test; +DROP EXTENSION test_copy_format; diff --git a/src/test/modules/test_copy_format/sql/schema.sql b/src/test/modules/test_copy_format/sql/schema.sql new file mode 100644 index 00000000000..ab9492158e1 --- /dev/null +++ b/src/test/modules/test_copy_format/sql/schema.sql @@ -0,0 +1,24 @@ +CREATE SCHEMA test_schema; +CREATE EXTENSION test_copy_format WITH SCHEMA test_schema; +CREATE TABLE public.test (a smallint, b integer, c bigint); +INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); + +-- Qualified name +COPY public.test FROM stdin WITH (FORMAT 'test_schema.test_copy_format'); +\. +COPY public.test TO stdout WITH (FORMAT 'test_schema.test_copy_format'); + +-- No schema, no search path +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format'); +COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); + +-- No schema, with search path +SET search_path = test_schema,public; +COPY public.test FROM stdin WITH (FORMAT 'test_copy_format'); +\. +COPY public.test TO stdout WITH (FORMAT 'test_copy_format'); +RESET search_path; + +DROP TABLE public.test; +DROP EXTENSION test_copy_format; +DROP SCHEMA test_schema; diff --git a/src/test/modules/test_copy_format/test_copy_format--1.0.sql b/src/test/modules/test_copy_format/test_copy_format--1.0.sql new file mode 100644 index 00000000000..c1a137181f8 --- /dev/null +++ b/src/test/modules/test_copy_format/test_copy_format--1.0.sql @@ -0,0 +1,24 @@ +/* src/test/modules/test_copy_format/test_copy_format--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_copy_format" to load this file. \quit + +CREATE FUNCTION test_copy_format(internal) + RETURNS copy_handler + AS 'MODULE_PATHNAME', 'test_copy_format' + LANGUAGE C; + +CREATE FUNCTION test_copy_format_wrong_input_type(bool) + RETURNS copy_handler + AS 'MODULE_PATHNAME', 'test_copy_format' + LANGUAGE C; + +CREATE FUNCTION test_copy_format_wrong_return_type(internal) + RETURNS bool + AS 'MODULE_PATHNAME', 'test_copy_format' + LANGUAGE C; + +CREATE FUNCTION test_copy_format_wrong_return_value(internal) + RETURNS copy_handler + AS 'MODULE_PATHNAME', 'test_copy_format_wrong_return_value' + LANGUAGE C; diff --git a/src/test/modules/test_copy_format/test_copy_format.c b/src/test/modules/test_copy_format/test_copy_format.c new file mode 100644 index 00000000000..1d754201336 --- /dev/null +++ b/src/test/modules/test_copy_format/test_copy_format.c @@ -0,0 +1,113 @@ +/*-------------------------------------------------------------------------- + * + * test_copy_format.c + * Code for testing custom COPY format. + * + * Portions Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_copy_format/test_copy_format.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "commands/copyapi.h" +#include "commands/defrem.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +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 cstate, TupleDesc tupDesc) +{ + ereport(NOTICE, (errmsg("CopyFromStart: the number of attributes: %d", tupDesc->natts))); +} + +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 const CopyFromRoutine CopyFromRoutineTestCopyFormat = { + .type = T_CopyFromRoutine, + .CopyFromInFunc = TestCopyFromInFunc, + .CopyFromStart = TestCopyFromStart, + .CopyFromOneRow = TestCopyFromOneRow, + .CopyFromEnd = TestCopyFromEnd, +}; + +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) +{ + ereport(NOTICE, (errmsg("CopyToStart: the number of attributes: %d", tupDesc->natts))); +} + +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 = { + .type = T_CopyToRoutine, + .CopyToOutFunc = TestCopyToOutFunc, + .CopyToStart = TestCopyToStart, + .CopyToOneRow = TestCopyToOneRow, + .CopyToEnd = TestCopyToEnd, +}; + +PG_FUNCTION_INFO_V1(test_copy_format); +Datum +test_copy_format(PG_FUNCTION_ARGS) +{ + bool is_from = PG_GETARG_BOOL(0); + + ereport(NOTICE, + (errmsg("test_copy_format: is_from=%s", is_from ? "true" : "false"))); + + if (is_from) + PG_RETURN_POINTER(&CopyFromRoutineTestCopyFormat); + else + PG_RETURN_POINTER(&CopyToRoutineTestCopyFormat); +} + +PG_FUNCTION_INFO_V1(test_copy_format_wrong_return_value); +Datum +test_copy_format_wrong_return_value(PG_FUNCTION_ARGS) +{ + bool is_from = PG_GETARG_BOOL(0); + + if (is_from) + PG_RETURN_CSTRING(pstrdup("is_from=true")); + else + PG_RETURN_CSTRING(pstrdup("is_from=false")); +} diff --git a/src/test/modules/test_copy_format/test_copy_format.control b/src/test/modules/test_copy_format/test_copy_format.control new file mode 100644 index 00000000000..f05a6362358 --- /dev/null +++ b/src/test/modules/test_copy_format/test_copy_format.control @@ -0,0 +1,4 @@ +comment = 'Test code for custom COPY format' +default_version = '1.0' +module_pathname = '$libdir/test_copy_format' +relocatable = true === Patch was already applied, skipping commit === === using 'git am' to apply patch ./v39-0003-Add-support-for-implementing-custom-COPY-handler.patch === fatal: previous rebase directory .git/rebase-apply still exists but mbox given. Unstaged changes after reset: M src/backend/commands/copy.c M src/backend/commands/copyfrom.c M src/backend/commands/copyto.c M src/backend/nodes/Makefile M src/backend/nodes/gen_node_support.pl M src/backend/utils/adt/pseudotypes.c M src/include/catalog/pg_proc.dat M src/include/catalog/pg_type.dat M src/include/commands/copy.h M src/include/commands/copyapi.h M src/include/commands/copyto_internal.h M src/include/nodes/meson.build M src/test/modules/Makefile M src/test/modules/meson.build Removing src/test/modules/test_copy_format/ === using patch(1) to apply patch ./v39-0003-Add-support-for-implementing-custom-COPY-handler.patch === patch: unrecognized option `--no-backup-if-mismatch' usage: patch [-bCcEeflNnRstuv] [-B backup-prefix] [-D symbol] [-d directory] [-F max-fuzz] [-i patchfile] [-o out-file] [-p strip-count] [-r rej-name] [-V t | nil | never | none] [-x number] [-z backup-ext] [--posix] [origfile [patchfile]] patch