pax_global_header00006660000000000000000000000064122056503330014511gustar00rootroot0000000000000052 comment=bf0365b81f509e6f75ca677d45ba03e0bbffdf7d plsh-1.20130823/000077500000000000000000000000001220565033300130625ustar00rootroot00000000000000plsh-1.20130823/.travis.yml000066400000000000000000000005351220565033300151760ustar00rootroot00000000000000language: c before_install: - wget https://gist.github.com/petere/5893799/raw/22814fa7d28c0a81434677ab68236f8565644648/apt.postgresql.org.sh - wget https://gist.github.com/petere/6023944/raw/50cfc5be1a61553ef11fabdc9e90da1574b00fdf/pg-travis-multiversion-test.sh - sudo sh ./apt.postgresql.org.sh script: bash ./pg-travis-multiversion-test.sh plsh-1.20130823/COPYING000066400000000000000000000021021220565033300141100ustar00rootroot00000000000000PL/sh Procedural Language Handler for PostgreSQL Copyright © 2012 by Peter Eisentraut Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL THE AUTHOR(S) OR ANY CONTRIBUTOR(S) BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE AUTHOR(S) OR CONTRIBUTOR(S) HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE AUTHOR(S) AND CONTRIBUTOR(S) SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE AUTHOR(S) AND CONTRIBUTOR(S) HAVE NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. plsh-1.20130823/Makefile000066400000000000000000000021401220565033300145170ustar00rootroot00000000000000PG_CONFIG = pg_config pg_version := $(word 2,$(shell $(PG_CONFIG) --version)) extensions_supported = $(filter-out 6.% 7.% 8.% 9.0%,$(pg_version)) inline_supported = $(filter-out 6.% 7.% 8.%,$(pg_version)) event_trigger_supported = $(filter-out 6.% 7.% 8.% 9.0% 9.1% 9.2%,$(pg_version)) MODULE_big = plsh OBJS = plsh.o extension_version = 2 DATA = $(if $(extensions_supported),plsh--unpackaged--1.sql plsh--1--2.sql,plsh.sql) DATA_built = $(if $(extensions_supported),plsh--$(extension_version).sql) EXTENSION = plsh EXTRA_CLEAN = plsh.sql plsh--$(extension_version).sql REGRESS = init function trigger crlf psql $(if $(inline_supported),inline) $(if $(event_trigger_supported),event_trigger) REGRESS_OPTS = --inputdir=test PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) override CFLAGS := $(filter-out -Wmissing-prototypes,$(CFLAGS)) all: plsh.sql plsh.sql: $(if $(inline_supported),plsh-inline.sql,plsh-noinline.sql) cp $< $@ plsh--$(extension_version).sql: plsh.sql cp $< $@ version = $(shell git describe --tags) dist: git archive --prefix=plsh-$(version)/ -o plsh-$(version).tar.gz -9 HEAD plsh-1.20130823/NEWS000066400000000000000000000034571220565033300135720ustar00rootroot00000000000000* Version 1.20130823 - Works with PostgreSQL 8.3 through 9.3. - Added event trigger support (requires 9.3). * Version 1.20130205 - Works with PostgreSQL 8.3 through 9.3devel. - Added inline handler (DO support). - The extension is now really relocatable and no longer installed into pg_catalog. This was broken before. * Version 1.20121226 - Works with PostgreSQL 8.3 through 9.3devel. - Libpq environment variables and PATH are set to simplify calling back into the database. - The TMPDIR environment variable is used if set, instead of /tmp. - TRUNCATE triggers are supported. * Version 1.20121018 - Works with PostgreSQL 8.3 through 9.3devel. Earlier versions are no longer tested but might still work. - Trigger data is now available as environment variables. - Fixed statement triggers. - Some fixes for Windows line endings. - Improved test suite. - Fixed a bug where a zombie process was left when the script exited with an error. * Version 1.20120414 - I am now following this version numbering system: http://kitenet.net/~joey/blog/entry/version_numbers/ - Works with PostgreSQL 8.0 through 9.1, and probably 9.2-to-be as well. - Build system was changed to pgxs. - Supports installing as extension with PostgreSQL 9.1+. - Some files were renamed for more consistency. * Version 1.3 Released 2007-12-15 - Works with PostgreSQL 7.4 through 8.3. * Version 1.2 Released 2005-11-11 - Works with PostgreSQL 7.4, 8.0, and 8.1. * Version 1.1 Released 2005-05-28 - Works with PostgreSQL 7.4 and 8.0. - Has validator function, checking function body for basic syntactical correctness. - Supports modern error reporting with codes and detail messages. - Uses pg_config to detect PostgreSQL installation for build. * Version 1.0 Released 2002-12-17 - Works with PostgreSQL 7.3. plsh-1.20130823/README.md000066400000000000000000000101111220565033300143330ustar00rootroot00000000000000PL/sh Procedural Language Handler for PostgreSQL ================================================ PL/sh is a procedural language handler for PostgreSQL that allows you to write stored procedures in a shell of your choice. For example, CREATE FUNCTION concat(text, text) RETURNS text AS ' #!/bin/sh echo "$1$2" ' LANGUAGE plsh; The first line must be a `#!`-style line that indicates the shell to use. The rest of the function body will be executed by that shell in a separate process. The arguments are available as `$1`, `$2`, etc., as usual. (This is the shell's syntax. If your shell uses something different then that's what you need to use.) The return value will become what is printed to the standard output, with a newline stripped. If nothing is printed, a null value is returned. If anything is printed to the standard error, then the function aborts with an error and the message is printed. If the script does not exit with status 0 then an error is raised as well. The shell script can do anything you want, but you can't access the database directly. Trigger functions are also possible, but they can't change the rows. Needless to say, this language should not be declared as `TRUSTED`. The distribution also contains a test suite in the directory `test/`, which contains a simplistic demonstration of the functionality. I'm interested if anyone is using this. Peter Eisentraut Database Access --------------- You can't access the database directly from PL/sh through something like SPI, but PL/sh sets up libpq environment variables so that you can easily call `psql` back into the same database, for example CREATE FUNCTION query (x int) RETURNS text LANGUAGE plsh AS $$ #!/bin/sh psql -At -c "select b from pbar where a = $1" $$; Note: The "bin" directory is prepended to the path, but only if the `PATH` environment variable is already set. Triggers -------- In a trigger procedure, trigger data is available to the script through environment variables (analogous to PL/pgSQL): * `PLSH_TG_NAME`: trigger name * `PLSH_TG_WHEN`: `BEFORE`, `INSTEAD OF`, or `AFTER` * `PLSH_TG_LEVEL`: `ROW` or `STATEMENT` * `PLSH_TG_OP`: `DELETE`, `INSERT`, `UPDATE`, or `TRUNCATE` * `PLSH_TG_TABLE_NAME`: name of the table the trigger is acting on * `PLSH_TG_TABLE_SCHEMA`: schema name of the table the trigger is acting on Event Triggers -------------- In an event trigger procedure, the event trigger data is available to the script through the following environment variables: * `PLSH_TG_EVENT`: event name * `PLSH_TG_TAG`: command tag Inline Handler -------------- PL/sh supports the `DO` command. For example: DO E'#!/bin/sh\nrm -f /tmp/file' LANGUAGE plsh; Installation ------------ You need to have PostgreSQL 8.3 or later, and you need to have the server include files installed. To build and install PL/sh, use this procedure: make make install The include files are found using the `pg_config` program that is included in the PostgreSQL installation. To use a different PostgreSQL installation, point configure to a different `pg_config` like so: make PG_CONFIG=/else/where/pg_config make install PG_CONFIG=/else/where/pg_config Note that generally server-side modules such as this one have to be recompiled for every major PostgreSQL version (that is, 8.4, 9.0, ...). To declare the language in a database, use the extension system with PostgreSQL version 9.1 or later. Run CREATE EXTENSION plsh; inside the database of choice. To upgrade from a previous installation that doesn't use the extension system, use CREATE EXTENSION plsh FROM unpackaged; Use `DROP EXTENSION` to remove it. With versions prior to PostgreSQL 9.1, use psql -d DBNAME -f .../share/contrib/plsh.sql with a server running. To drop it, use `droplang plsh`, or `DROP FUNCTION plsh_handler(); DROP LANGUAGE plsh;` if you want to do it manually. Test suite ---------- [![Build Status](https://secure.travis-ci.org/petere/plsh.png)](http://travis-ci.org/petere/plsh) To run the test suite, execute make installcheck after installation. plsh-1.20130823/plsh--1--2.sql000066400000000000000000000003301220565033300151740ustar00rootroot00000000000000CREATE FUNCTION plsh_inline_handler(internal) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE OR REPLACE LANGUAGE plsh HANDLER plsh_handler INLINE plsh_inline_handler VALIDATOR plsh_validator; plsh-1.20130823/plsh--unpackaged--1.sql000066400000000000000000000002341220565033300171400ustar00rootroot00000000000000ALTER EXTENSION plsh ADD PROCEDURAL LANGUAGE plsh; ALTER EXTENSION plsh ADD FUNCTION plsh_handler(); ALTER EXTENSION plsh ADD FUNCTION plsh_validator(oid); plsh-1.20130823/plsh-inline.sql000066400000000000000000000006761220565033300160360ustar00rootroot00000000000000CREATE FUNCTION plsh_handler() RETURNS language_handler AS '$libdir/plsh' LANGUAGE C; CREATE FUNCTION plsh_inline_handler(internal) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE FUNCTION plsh_validator(oid) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE LANGUAGE plsh HANDLER plsh_handler INLINE plsh_inline_handler VALIDATOR plsh_validator; COMMENT ON LANGUAGE plsh IS 'PL/sh procedural language'; plsh-1.20130823/plsh-noinline.sql000066400000000000000000000004751220565033300163700ustar00rootroot00000000000000CREATE FUNCTION plsh_handler() RETURNS language_handler AS '$libdir/plsh' LANGUAGE C; CREATE FUNCTION plsh_validator(oid) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE LANGUAGE plsh HANDLER plsh_handler VALIDATOR plsh_validator; COMMENT ON LANGUAGE plsh IS 'PL/sh procedural language'; plsh-1.20130823/plsh.c000066400000000000000000000404731220565033300142040ustar00rootroot00000000000000/* * PL/sh language handler * * Copyright © 2012 by Peter Eisentraut * See the COPYING file for details. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(PG_VERSION_NUM) && PG_VERSION_NUM >= 90300 #include #include #define HAVE_EVENT_TRIGGERS 1 #endif #include #include #include #include #include #include PG_MODULE_MAGIC; #define _textout(x) (DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(&x)))) #ifndef HAVE_EVENT_TRIGGERS typedef void EventTriggerData; #define CALLED_AS_EVENT_TRIGGER(x) 0 #endif static char * handler_internal2(const char *tempfile, char * const * arguments, const char *proname, TriggerData *trigger_data, EventTriggerData *event_trigger_data); /* * Convert the C string "input" to a Datum of type "typeoid". */ static Datum cstring_to_type(char * input, Oid typeoid) { HeapTuple typetuple; Form_pg_type pg_type_entry; Datum ret; typetuple = SearchSysCache(TYPEOID, ObjectIdGetDatum(typeoid), 0, 0, 0); if (!HeapTupleIsValid(typetuple)) elog(ERROR, "cache lookup failed for type %u", typeoid); pg_type_entry = (Form_pg_type) GETSTRUCT(typetuple); ret = OidFunctionCall3(pg_type_entry->typinput, CStringGetDatum(input), 0, -1); ReleaseSysCache(typetuple); PG_RETURN_DATUM(ret); } /* * Convert the Datum "input" that is of type "typeoid" to a C string. */ static char * type_to_cstring(Datum input, Oid typeoid) { HeapTuple typetuple; Form_pg_type pg_type_entry; Datum ret; typetuple = SearchSysCache(TYPEOID, ObjectIdGetDatum(typeoid), 0, 0, 0); if (!HeapTupleIsValid(typetuple)) elog(ERROR, "cache lookup failed for type %u", typeoid); pg_type_entry = (Form_pg_type) GETSTRUCT(typetuple); ret = OidFunctionCall3(pg_type_entry->typoutput, input, 0, -1); ReleaseSysCache(typetuple); return DatumGetCString(ret); } #define SPLIT_MAX 64 /* * Split the "string" at space boundaries. The number of resulting * strings is in argcp, the actual strings in argv. argcp should be * allocated to expect SPLIT_MAX strings. "string" will be clobbered. */ static void split_string(char *argv[], int *argcp, char *string) { char * s = string; while (s && *s && *argcp < SPLIT_MAX) { while (*s == ' ') ++s; if (*s == '\0') break; argv[(*argcp)++] = s; while (*s && *s != ' ') ++s; if (*s) *s++ = '\0'; } } /* * Find shell and arguments in source code */ void parse_shell_and_arguments(const char *sourcecode, int *argcp, char **arguments, const char **restp) { const char *rest; size_t len; char * s; /* * Accept one blank line at the start, to allow coding like this: * CREATE FUNCTION .... AS ' * #!/bin/sh * ... * ' LANGUAGE plsh; */ while (sourcecode[0] == '\n' || sourcecode[0] == '\r') sourcecode++; elog(DEBUG2, "source code of function:\n%s", sourcecode); if (strlen(sourcecode) < 3 || (strncmp(sourcecode, "#!/", 3) != 0 && strncmp(sourcecode, "#! /", 4) != 0)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid start of script: %-.10s...", sourcecode), errdetail("Script code must start with \"#!/\" or \"#! /\"."))); rest = sourcecode + strcspn(sourcecode, "/"); len = strcspn(rest, "\n\r"); s = palloc(len + 1); strncpy(s, rest, len); s[len] = '\0'; rest += len; if (*rest) rest++; *argcp = 0; split_string(arguments, argcp, s); *restp = rest; elog(DEBUG2, "using shell \"%s\"", arguments[0]); } /* * Read from "file" until EOF or error. Return the content in * palloc'ed memory. On error return NULL and set errno. */ static char * read_from_file(FILE * file) { char * buffer = NULL; ssize_t len = 0; do { char buf[512]; ssize_t l; l = fread(buf, 1, 512, file); if (buffer) buffer = repalloc(buffer, len + l + 1); else buffer = palloc(l + 1); strncpy(buffer + len, buf, l); buffer[len + l] = '\0'; len += l; if (feof(file)) { break; } if (ferror(file)) { return NULL; break; } } while(1); return buffer; } static char * write_to_tempfile(const char *data) { char *tmpdir_envvar; static char tempfile[MAXPGPATH]; int fd; FILE * file; if ((tmpdir_envvar = getenv("TMPDIR"))) snprintf(tempfile, sizeof(tempfile), "%s/plsh.XXXXXX", tmpdir_envvar); else strcpy(tempfile, "/tmp/plsh-XXXXXX"); fd = mkstemp(tempfile); if (fd == -1) ereport(ERROR, (errcode_for_file_access(), errmsg("could not create temporary file \"%s\": %m", tempfile))); file = fdopen(fd, "w"); if (!file) { close(fd); remove(tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not open file stream to temporary file: %m"))); } fprintf(file, "%s", data); if (ferror(file)) { fclose(file); remove(tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not write script to temporary file: %m"))); } fclose(file); elog(DEBUG2, "source code is now in file \"%s\"", tempfile); return tempfile; } /* * Set environment variables corresponding to trigger data */ static void set_trigger_data_envvars(TriggerData *trigdata) { const char *tg_when_str = NULL; const char *tg_level_str = NULL; const char *tg_op_str = NULL; setenv("PLSH_TG_NAME", trigdata->tg_trigger->tgname, 1); if (TRIGGER_FIRED_BEFORE(trigdata->tg_event)) tg_when_str = "BEFORE"; #ifdef TRIGGER_FIRED_INSTEAD else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event)) tg_when_str = "INSTEAD OF"; #endif else if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) tg_when_str = "AFTER"; if (tg_when_str) setenv("PLSH_TG_WHEN", tg_when_str, 1); if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) tg_level_str = "ROW"; else if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) tg_level_str = "STATEMENT"; if (tg_level_str) setenv("PLSH_TG_LEVEL", tg_level_str, 1); if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) tg_op_str = "DELETE"; else if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) tg_op_str = "INSERT"; else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) tg_op_str = "UPDATE"; #ifdef TRIGGER_FIRED_BY_TRUNCATE else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) tg_op_str = "TRUNCATE"; #endif if (tg_op_str) setenv("PLSH_TG_OP", tg_op_str, 1); setenv("PLSH_TG_TABLE_NAME", NameStr(trigdata->tg_relation->rd_rel->relname), 1); setenv("PLSH_TG_TABLE_SCHEMA", get_namespace_name(trigdata->tg_relation->rd_rel->relnamespace), 1); } /* * Set environment variables corresponding to event trigger data */ static void set_event_trigger_data_envvars(EventTriggerData *evttrigdata) { #ifdef HAVE_EVENT_TRIGGERS setenv("PLSH_TG_EVENT", evttrigdata->event, 1); setenv("PLSH_TG_TAG", evttrigdata->tag, 1); #endif } /* * Set environment variables for libpq access */ void set_libpq_envvars(void) { setenv("PGAPPNAME", "plsh", 1); unsetenv("PGCLIENTENCODING"); setenv("PGDATABASE", get_database_name(MyDatabaseId), 1); #if PG_VERSION_NUM >= 90300 if (Unix_socket_directories) { char *rawstring; List *elemlist; rawstring = pstrdup(Unix_socket_directories); if (!SplitDirectoriesString(rawstring, ',', &elemlist)) ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid list syntax for \"unix_socket_directories\""))); if (list_length(elemlist)) setenv("PGHOST", linitial(elemlist), 1); else setenv("PGHOST", "localhost", 0); } #else if (UnixSocketDir && *UnixSocketDir) setenv("PGHOST", UnixSocketDir, 1); #endif else setenv("PGHOST", "localhost", 0); { char buf[16]; sprintf(buf, "%u", PostPortNumber); setenv("PGPORT", buf, 1); } if (getenv("PATH")) { char buf[MAXPGPATH]; char *p; strlcpy(buf, my_exec_path, sizeof(buf)); p = strrchr(buf, '/'); snprintf(p, sizeof(buf) - (p - buf), ":%s", getenv("PATH")); setenv("PATH", buf, 1); } } /* * Block and wait for the script to finish */ static int wait_and_cleanup(pid_t child_pid, const char *tempfile) { pid_t dead; int child_status; do dead = wait(&child_status); while (dead > 0 && dead != child_pid); remove(tempfile); if (dead != child_pid) ereport(ERROR, (errcode_for_file_access(), errmsg("wait failed: %m"))); return child_status; } /* * Internal handler function */ Datum handler_internal(Oid function_oid, FunctionCallInfo fcinfo, bool execute) { HeapTuple proctuple; Form_pg_proc pg_proc_entry; const char * sourcecode; const char * rest; char *tempfile; int i; int argc; char * arguments[FUNC_MAX_ARGS + 2]; char * ret; HeapTuple returntuple = NULL; Datum prosrcdatum; bool isnull; proctuple = SearchSysCache(PROCOID, ObjectIdGetDatum(function_oid), 0, 0, 0); if (!HeapTupleIsValid(proctuple)) elog(ERROR, "cache lookup failed for function %u", function_oid); prosrcdatum = SysCacheGetAttr(PROCOID, proctuple, Anum_pg_proc_prosrc, &isnull); if (isnull) elog(ERROR, "null prosrc"); sourcecode = DatumGetCString(DirectFunctionCall1(textout, prosrcdatum)); parse_shell_and_arguments(sourcecode, &argc, arguments, &rest); /* validation stops here */ if (!execute) { ReleaseSysCache(proctuple); PG_RETURN_VOID(); } tempfile = write_to_tempfile(rest); arguments[argc++] = tempfile; /* evaluate arguments */ pg_proc_entry = (Form_pg_proc) GETSTRUCT(proctuple); if (CALLED_AS_TRIGGER(fcinfo)) { TriggerData *trigdata = (TriggerData *) fcinfo->context; Trigger *trigger = trigdata->tg_trigger; TupleDesc tupdesc = trigdata->tg_relation->rd_att; HeapTuple oldtuple = trigdata->tg_trigtuple; /* first the CREATE TRIGGER fixed arguments */ for (i = 0; i < trigger->tgnargs; i++) { arguments[argc++] = trigger->tgargs[i]; } if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) for (i = 0; i < tupdesc->natts; i++) { char * s; bool isnull; Datum attr; attr = heap_getattr(oldtuple, i + 1, tupdesc, &isnull); if (isnull) s = ""; else s = type_to_cstring(attr, tupdesc->attrs[i]->atttypid); elog(DEBUG2, "arg %d is \"%s\" (type %u)", i, s, tupdesc->attrs[i]->atttypid); arguments[argc++] = s; } /* since we can't alter the tuple anyway, set up a return tuple right now */ if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) returntuple = trigdata->tg_trigtuple; else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) returntuple = trigdata->tg_trigtuple; else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) returntuple = trigdata->tg_newtuple; #ifdef TRIGGER_FIRED_BY_TRUNCATE else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) returntuple = trigdata->tg_trigtuple; #endif else elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, UPDATE, or TRUNCATE"); } else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) { /* nothing */ } else /* not trigger */ { for (i = 0; i < pg_proc_entry->pronargs; i++) { char * s; if (PG_ARGISNULL(i)) s = ""; else s = type_to_cstring(PG_GETARG_DATUM(i), pg_proc_entry->proargtypes.values[i]); elog(DEBUG2, "arg %d is \"%s\"", i, s); arguments[argc++] = s; } } /* terminate list */ arguments[argc] = NULL; ret = handler_internal2(tempfile, arguments, NameStr(pg_proc_entry->proname), CALLED_AS_TRIGGER(fcinfo) ? (TriggerData *) fcinfo->context : NULL, CALLED_AS_EVENT_TRIGGER(fcinfo) ? (EventTriggerData *) fcinfo->context : NULL); ReleaseSysCache(proctuple); if (CALLED_AS_TRIGGER(fcinfo)) { PG_RETURN_DATUM(PointerGetDatum(returntuple)); } else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) { PG_RETURN_NULL(); } else { if (ret) PG_RETURN_DATUM(cstring_to_type(ret, pg_proc_entry->prorettype)); else PG_RETURN_NULL(); } } static char * handler_internal2(const char *tempfile, char * const * arguments, const char *proname, TriggerData *trigger_data, EventTriggerData *event_trigger_data) { int stdout_pipe[2]; int stderr_pipe[2]; pid_t child_pid; int child_status; FILE * file; char * stdout_buffer; char * stderr_buffer; size_t len; bool return_null; /* start process voodoo */ if (pipe(stdout_pipe) == -1) { remove(tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not make pipe: %m"))); } if (pipe(stderr_pipe) == -1) { remove(tempfile); close(stdout_pipe[0]); close(stdout_pipe[1]); ereport(ERROR, (errcode_for_file_access(), errmsg("could not make pipe: %m"))); } child_pid = fork(); if (child_pid == -1) /* fork failed */ { remove(tempfile); close(stdout_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[0]); close(stderr_pipe[1]); ereport(ERROR, (errcode_for_file_access(), errmsg("fork failed: %m"))); } else if (child_pid == 0) /* child */ { /* close reading end */ close(stdout_pipe[0]); close(stderr_pipe[0]); dup2(stdout_pipe[1], 1); dup2(stderr_pipe[1], 2); close(stdout_pipe[1]); close(stderr_pipe[1]); if (trigger_data) set_trigger_data_envvars(trigger_data); if (event_trigger_data) set_event_trigger_data_envvars(event_trigger_data); set_libpq_envvars(); execv(arguments[0], arguments); ereport(ERROR, (errcode_for_file_access(), errmsg("could not exec: %m"))); } /* parent continues... */ close(stdout_pipe[1]); /* writing end */ close(stderr_pipe[1]); /* fetch return value from stdout */ return_null = false; file = fdopen(stdout_pipe[0], "r"); if (!file) { close(stdout_pipe[0]); close(stderr_pipe[0]); wait_and_cleanup(child_pid, tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not open file stream to stdout pipe: %m"))); } stdout_buffer = read_from_file(file); fclose(file); if (!stdout_buffer) { close(stderr_pipe[0]); wait_and_cleanup(child_pid, tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not read script's stdout: %m"))); } len = strlen(stdout_buffer); if (len == 0) return_null = true; /* strip one trailing newline */ else if (stdout_buffer[len - 1] == '\n') stdout_buffer[len - 1] = '\0'; elog(DEBUG2, "stdout was \"%s\"", stdout_buffer); /* print stderr as error */ file = fdopen(stderr_pipe[0], "r"); if (!file) { close(stderr_pipe[0]); wait_and_cleanup(child_pid, tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not open file stream to stderr pipe: %m"))); } stderr_buffer = read_from_file(file); fclose(file); if (!stderr_buffer) { wait_and_cleanup(child_pid, tempfile); ereport(ERROR, (errcode_for_file_access(), errmsg("could not read script's stderr: %m"))); } len = strlen(stderr_buffer); if (stderr_buffer[len - 1] == '\n') stderr_buffer[len - 1] = '\0'; if (stderr_buffer[0] != '\0') { wait_and_cleanup(child_pid, tempfile); ereport(ERROR, (errmsg("%s: %s", proname, stderr_buffer))); } child_status = wait_and_cleanup(child_pid, tempfile); if (WIFEXITED(child_status)) { if (WEXITSTATUS(child_status) != 0) ereport(ERROR, (errmsg("script exited with status %d", WEXITSTATUS(child_status)))); } if (WIFSIGNALED(child_status)) { ereport(ERROR, (errmsg("script was terminated by signal %d", (int)WTERMSIG(child_status)))); } if (return_null) return NULL; else return stdout_buffer; } /* * The PL handler */ PG_FUNCTION_INFO_V1(plsh_handler); Datum plsh_handler(PG_FUNCTION_ARGS) { return handler_internal(fcinfo->flinfo->fn_oid, fcinfo, true); } /* * Validator function */ PG_FUNCTION_INFO_V1(plsh_validator); Datum plsh_validator(PG_FUNCTION_ARGS) { return handler_internal(PG_GETARG_OID(0), fcinfo, false); } #if CATALOG_VERSION_NO >= 200909221 /* * Inline handler */ PG_FUNCTION_INFO_V1(plsh_inline_handler); Datum plsh_inline_handler(PG_FUNCTION_ARGS) { InlineCodeBlock *codeblock = (InlineCodeBlock *) DatumGetPointer(PG_GETARG_DATUM(0)); int argc; char * arguments[FUNC_MAX_ARGS + 2]; const char *rest; char *tempfile; parse_shell_and_arguments(codeblock->source_text, &argc, arguments, &rest); tempfile = write_to_tempfile(rest); arguments[argc++] = tempfile; arguments[argc] = NULL; handler_internal2(tempfile, arguments, "inline code block", NULL, NULL); PG_RETURN_VOID(); } #endif plsh-1.20130823/plsh.control000066400000000000000000000001151220565033300154270ustar00rootroot00000000000000comment = 'PL/sh procedural language' default_version = 2 relocatable = true plsh-1.20130823/test/000077500000000000000000000000001220565033300140415ustar00rootroot00000000000000plsh-1.20130823/test/expected/000077500000000000000000000000001220565033300156425ustar00rootroot00000000000000plsh-1.20130823/test/expected/crlf.out000066400000000000000000000002441220565033300173210ustar00rootroot00000000000000-- CR/LF test CREATE FUNCTION crlf_test() RETURNS text LANGUAGE plsh AS E'\r\n#!/bin/sh\r\necho OK\r\n'; SELECT crlf_test(); crlf_test ----------- OK\r (1 row) plsh-1.20130823/test/expected/event_trigger.out000066400000000000000000000012021220565033300212320ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test CREATE FUNCTION evttrigger() RETURNS event_trigger AS $$ #!/bin/sh ( echo "---" for arg do echo "Arg is '$arg'" done printenv | LC_ALL=C sort | grep '^PLSH_TG_' ) >> /tmp/plsh-test/bar chmod a+r /tmp/plsh-test/bar exit 0 $$ LANGUAGE plsh; CREATE EVENT TRIGGER testtrigger ON ddl_command_start EXECUTE PROCEDURE evttrigger(); CREATE TABLE test (a int, b text); DROP TABLE test; DROP EVENT TRIGGER testtrigger; \! cat /tmp/plsh-test/bar --- PLSH_TG_EVENT=ddl_command_start PLSH_TG_TAG=CREATE TABLE --- PLSH_TG_EVENT=ddl_command_start PLSH_TG_TAG=DROP TABLE \! rm -r /tmp/plsh-test plsh-1.20130823/test/expected/function.out000066400000000000000000000031431220565033300202210ustar00rootroot00000000000000CREATE FUNCTION valtest(text) RETURNS text AS 'foo' LANGUAGE plsh; ERROR: invalid start of script: foo... DETAIL: Script code must start with "#!/" or "#! /". CREATE FUNCTION shtest (text, text) RETURNS text AS ' #!/bin/sh echo "One: $1 Two: $2" if test "$1" = "$2"; then echo ''this is an error'' 1>&2 fi exit 0 ' LANGUAGE plsh; SELECT shtest('foo', 'bar'); shtest ------------------- One: foo Two: bar (1 row) SELECT shtest('xxx', 'xxx'); ERROR: shtest: this is an error SELECT shtest('null', NULL); shtest ----------------- One: null Two: (1 row) CREATE FUNCTION return_null() RETURNS text LANGUAGE plsh AS '#!/bin/sh'; SELECT return_null() IS NULL; ?column? ---------- t (1 row) CREATE FUNCTION self_exit(int) RETURNS void LANGUAGE plsh AS ' #!/bin/sh exit $1 '; SELECT self_exit(77); ERROR: script exited with status 77 CREATE FUNCTION self_signal(int) RETURNS void LANGUAGE plsh AS ' #!/bin/sh kill -$1 $$ '; SELECT self_signal(15); ERROR: script was terminated by signal 15 CREATE FUNCTION shell_args() RETURNS text LANGUAGE plsh AS ' #!/bin/sh -e -x false true '; SELECT shell_args(); ERROR: shell_args: + false CREATE FUNCTION shell_args2() RETURNS text LANGUAGE plsh AS ' #!/bin/sh -e -x'; SELECT shell_args2(); shell_args2 ------------- (1 row) CREATE FUNCTION shell_args3() RETURNS text LANGUAGE plsh AS ' #!/bin/sh '; SELECT shell_args3(); shell_args3 ------------- (1 row) CREATE FUNCTION perl_concat(text, text) RETURNS text LANGUAGE plsh AS ' #!/usr/bin/perl print $ARGV[0] . $ARGV[1]; '; SELECT perl_concat('pe', 'rl'); perl_concat ------------- perl (1 row) plsh-1.20130823/test/expected/init.out000066400000000000000000000007061220565033300173410ustar00rootroot00000000000000\i plsh.sql CREATE FUNCTION plsh_handler() RETURNS language_handler AS '$libdir/plsh' LANGUAGE C; CREATE FUNCTION plsh_inline_handler(internal) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE FUNCTION plsh_validator(oid) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE LANGUAGE plsh HANDLER plsh_handler INLINE plsh_inline_handler VALIDATOR plsh_validator; COMMENT ON LANGUAGE plsh IS 'PL/sh procedural language'; plsh-1.20130823/test/expected/init_1.out000066400000000000000000000005061220565033300175570ustar00rootroot00000000000000\i plsh.sql CREATE FUNCTION plsh_handler() RETURNS language_handler AS '$libdir/plsh' LANGUAGE C; CREATE FUNCTION plsh_validator(oid) RETURNS void AS '$libdir/plsh' LANGUAGE C; CREATE LANGUAGE plsh HANDLER plsh_handler VALIDATOR plsh_validator; COMMENT ON LANGUAGE plsh IS 'PL/sh procedural language'; plsh-1.20130823/test/expected/inline.out000066400000000000000000000003431220565033300176510ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test DO E'#!/bin/sh\necho inline > /tmp/plsh-test/inline.out; chmod a+r /tmp/plsh-test/inline.out' LANGUAGE plsh; \! cat /tmp/plsh-test/inline.out inline \! rm -r /tmp/plsh-test plsh-1.20130823/test/expected/psql.out000066400000000000000000000005011220565033300173460ustar00rootroot00000000000000CREATE TABLE pbar (a int, b text); INSERT INTO pbar VALUES (1, 'one'), (2, 'two'); CREATE FUNCTION query (x int) RETURNS text LANGUAGE plsh AS $$ #!/bin/sh if which psql >/dev/null; then psql -At -c "select b from pbar where a = $1" else echo 'no PATH?' 1>&2 fi $$; SELECT query(1); query ------- one (1 row) plsh-1.20130823/test/expected/psql_1.out000066400000000000000000000004731220565033300175760ustar00rootroot00000000000000CREATE TABLE pbar (a int, b text); INSERT INTO pbar VALUES (1, 'one'), (2, 'two'); CREATE FUNCTION query (x int) RETURNS text LANGUAGE plsh AS $$ #!/bin/sh if which psql >/dev/null; then psql -At -c "select b from pbar where a = $1" else echo 'no PATH?' 1>&2 fi $$; SELECT query(1); ERROR: query: no PATH? plsh-1.20130823/test/expected/trigger.out000066400000000000000000000042621220565033300200420ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test CREATE FUNCTION shtrigger() RETURNS trigger AS $$ #!/bin/sh ( echo "---" for arg do echo "Arg is '$arg'" done printenv | LC_ALL=C sort | grep '^PLSH_TG_' ) >> /tmp/plsh-test/foo chmod a+r /tmp/plsh-test/foo exit 0 $$ LANGUAGE plsh; CREATE TABLE pfoo (a int, b text); CREATE TRIGGER testtrigger AFTER INSERT ON pfoo FOR EACH ROW EXECUTE PROCEDURE shtrigger('dummy'); CREATE TRIGGER testtrigger2 BEFORE UPDATE ON pfoo FOR EACH ROW EXECUTE PROCEDURE shtrigger('dummy2'); CREATE TRIGGER testtrigger3 AFTER DELETE ON pfoo FOR EACH STATEMENT EXECUTE PROCEDURE shtrigger('dummy3'); CREATE TRIGGER testtrigger4 AFTER TRUNCATE ON pfoo FOR EACH STATEMENT EXECUTE PROCEDURE shtrigger('dummy4'); INSERT INTO pfoo VALUES (0, null); INSERT INTO pfoo VALUES (1, 'one'); INSERT INTO pfoo VALUES (2, 'two'); INSERT INTO pfoo VALUES (3, 'three'); UPDATE pfoo SET b = 'oneone' WHERE a = 1; DELETE FROM pfoo; TRUNCATE pfoo; \! cat /tmp/plsh-test/foo --- Arg is 'dummy' Arg is '0' Arg is '' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy' Arg is '1' Arg is 'one' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy' Arg is '2' Arg is 'two' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy' Arg is '3' Arg is 'three' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy2' Arg is '1' Arg is 'one' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger2 PLSH_TG_OP=UPDATE PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=BEFORE --- Arg is 'dummy3' PLSH_TG_LEVEL=STATEMENT PLSH_TG_NAME=testtrigger3 PLSH_TG_OP=DELETE PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy4' PLSH_TG_LEVEL=STATEMENT PLSH_TG_NAME=testtrigger4 PLSH_TG_OP=TRUNCATE PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER \! rm -r /tmp/plsh-test plsh-1.20130823/test/expected/trigger_1.out000066400000000000000000000042431220565033300202610ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test CREATE FUNCTION shtrigger() RETURNS trigger AS $$ #!/bin/sh ( echo "---" for arg do echo "Arg is '$arg'" done printenv | LC_ALL=C sort | grep '^PLSH_TG_' ) >> /tmp/plsh-test/foo chmod a+r /tmp/plsh-test/foo exit 0 $$ LANGUAGE plsh; CREATE TABLE pfoo (a int, b text); CREATE TRIGGER testtrigger AFTER INSERT ON pfoo FOR EACH ROW EXECUTE PROCEDURE shtrigger('dummy'); CREATE TRIGGER testtrigger2 BEFORE UPDATE ON pfoo FOR EACH ROW EXECUTE PROCEDURE shtrigger('dummy2'); CREATE TRIGGER testtrigger3 AFTER DELETE ON pfoo FOR EACH STATEMENT EXECUTE PROCEDURE shtrigger('dummy3'); CREATE TRIGGER testtrigger4 AFTER TRUNCATE ON pfoo FOR EACH STATEMENT EXECUTE PROCEDURE shtrigger('dummy4'); ERROR: syntax error at or near "TRUNCATE" LINE 1: CREATE TRIGGER testtrigger4 AFTER TRUNCATE ON pfoo ^ INSERT INTO pfoo VALUES (0, null); INSERT INTO pfoo VALUES (1, 'one'); INSERT INTO pfoo VALUES (2, 'two'); INSERT INTO pfoo VALUES (3, 'three'); UPDATE pfoo SET b = 'oneone' WHERE a = 1; DELETE FROM pfoo; TRUNCATE pfoo; \! cat /tmp/plsh-test/foo --- Arg is 'dummy' Arg is '0' Arg is '' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy' Arg is '1' Arg is 'one' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy' Arg is '2' Arg is 'two' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy' Arg is '3' Arg is 'three' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger PLSH_TG_OP=INSERT PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER --- Arg is 'dummy2' Arg is '1' Arg is 'one' PLSH_TG_LEVEL=ROW PLSH_TG_NAME=testtrigger2 PLSH_TG_OP=UPDATE PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=BEFORE --- Arg is 'dummy3' PLSH_TG_LEVEL=STATEMENT PLSH_TG_NAME=testtrigger3 PLSH_TG_OP=DELETE PLSH_TG_TABLE_NAME=pfoo PLSH_TG_TABLE_SCHEMA=public PLSH_TG_WHEN=AFTER \! rm -r /tmp/plsh-test plsh-1.20130823/test/sql/000077500000000000000000000000001220565033300146405ustar00rootroot00000000000000plsh-1.20130823/test/sql/crlf.sql000066400000000000000000000001761220565033300163130ustar00rootroot00000000000000-- CR/LF test CREATE FUNCTION crlf_test() RETURNS text LANGUAGE plsh AS E'\r\n#!/bin/sh\r\necho OK\r\n'; SELECT crlf_test(); plsh-1.20130823/test/sql/event_trigger.sql000066400000000000000000000010171220565033300202240ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test CREATE FUNCTION evttrigger() RETURNS event_trigger AS $$ #!/bin/sh ( echo "---" for arg do echo "Arg is '$arg'" done printenv | LC_ALL=C sort | grep '^PLSH_TG_' ) >> /tmp/plsh-test/bar chmod a+r /tmp/plsh-test/bar exit 0 $$ LANGUAGE plsh; CREATE EVENT TRIGGER testtrigger ON ddl_command_start EXECUTE PROCEDURE evttrigger(); CREATE TABLE test (a int, b text); DROP TABLE test; DROP EVENT TRIGGER testtrigger; \! cat /tmp/plsh-test/bar \! rm -r /tmp/plsh-test plsh-1.20130823/test/sql/function.sql000066400000000000000000000021531220565033300172070ustar00rootroot00000000000000CREATE FUNCTION valtest(text) RETURNS text AS 'foo' LANGUAGE plsh; CREATE FUNCTION shtest (text, text) RETURNS text AS ' #!/bin/sh echo "One: $1 Two: $2" if test "$1" = "$2"; then echo ''this is an error'' 1>&2 fi exit 0 ' LANGUAGE plsh; SELECT shtest('foo', 'bar'); SELECT shtest('xxx', 'xxx'); SELECT shtest('null', NULL); CREATE FUNCTION return_null() RETURNS text LANGUAGE plsh AS '#!/bin/sh'; SELECT return_null() IS NULL; CREATE FUNCTION self_exit(int) RETURNS void LANGUAGE plsh AS ' #!/bin/sh exit $1 '; SELECT self_exit(77); CREATE FUNCTION self_signal(int) RETURNS void LANGUAGE plsh AS ' #!/bin/sh kill -$1 $$ '; SELECT self_signal(15); CREATE FUNCTION shell_args() RETURNS text LANGUAGE plsh AS ' #!/bin/sh -e -x false true '; SELECT shell_args(); CREATE FUNCTION shell_args2() RETURNS text LANGUAGE plsh AS ' #!/bin/sh -e -x'; SELECT shell_args2(); CREATE FUNCTION shell_args3() RETURNS text LANGUAGE plsh AS ' #!/bin/sh '; SELECT shell_args3(); CREATE FUNCTION perl_concat(text, text) RETURNS text LANGUAGE plsh AS ' #!/usr/bin/perl print $ARGV[0] . $ARGV[1]; '; SELECT perl_concat('pe', 'rl'); plsh-1.20130823/test/sql/init.sql000066400000000000000000000000141220565033300163170ustar00rootroot00000000000000\i plsh.sql plsh-1.20130823/test/sql/inline.sql000066400000000000000000000003361220565033300166410ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test DO E'#!/bin/sh\necho inline > /tmp/plsh-test/inline.out; chmod a+r /tmp/plsh-test/inline.out' LANGUAGE plsh; \! cat /tmp/plsh-test/inline.out \! rm -r /tmp/plsh-test plsh-1.20130823/test/sql/psql.sql000066400000000000000000000004451220565033300163430ustar00rootroot00000000000000CREATE TABLE pbar (a int, b text); INSERT INTO pbar VALUES (1, 'one'), (2, 'two'); CREATE FUNCTION query (x int) RETURNS text LANGUAGE plsh AS $$ #!/bin/sh if which psql >/dev/null; then psql -At -c "select b from pbar where a = $1" else echo 'no PATH?' 1>&2 fi $$; SELECT query(1); plsh-1.20130823/test/sql/trigger.sql000066400000000000000000000020201220565033300170160ustar00rootroot00000000000000\! mkdir /tmp/plsh-test && chmod a+rwx /tmp/plsh-test CREATE FUNCTION shtrigger() RETURNS trigger AS $$ #!/bin/sh ( echo "---" for arg do echo "Arg is '$arg'" done printenv | LC_ALL=C sort | grep '^PLSH_TG_' ) >> /tmp/plsh-test/foo chmod a+r /tmp/plsh-test/foo exit 0 $$ LANGUAGE plsh; CREATE TABLE pfoo (a int, b text); CREATE TRIGGER testtrigger AFTER INSERT ON pfoo FOR EACH ROW EXECUTE PROCEDURE shtrigger('dummy'); CREATE TRIGGER testtrigger2 BEFORE UPDATE ON pfoo FOR EACH ROW EXECUTE PROCEDURE shtrigger('dummy2'); CREATE TRIGGER testtrigger3 AFTER DELETE ON pfoo FOR EACH STATEMENT EXECUTE PROCEDURE shtrigger('dummy3'); CREATE TRIGGER testtrigger4 AFTER TRUNCATE ON pfoo FOR EACH STATEMENT EXECUTE PROCEDURE shtrigger('dummy4'); INSERT INTO pfoo VALUES (0, null); INSERT INTO pfoo VALUES (1, 'one'); INSERT INTO pfoo VALUES (2, 'two'); INSERT INTO pfoo VALUES (3, 'three'); UPDATE pfoo SET b = 'oneone' WHERE a = 1; DELETE FROM pfoo; TRUNCATE pfoo; \! cat /tmp/plsh-test/foo \! rm -r /tmp/plsh-test