]> granicus.if.org Git - postgresql/commitdiff
From: Jan Wieck <jwieck@debis.com>
authorMarc G. Fournier <scrappy@hub.org>
Wed, 11 Feb 1998 14:08:01 +0000 (14:08 +0000)
committerMarc G. Fournier <scrappy@hub.org>
Wed, 11 Feb 1998 14:08:01 +0000 (14:08 +0000)
    A few minutes ago I sent down the PL/Tcl  directory  to  this
    list.  Look at it and reuse anything that might help to build
    PL/perl.  I really hope that PL/perl and PL/Tcl appear in the
    6.3 distribution. I'll do whatever I can to make this happen.

src/pl/tcl/INSTALL [new file with mode: 0644]
src/pl/tcl/Makefile [new file with mode: 0644]
src/pl/tcl/license.terms [new file with mode: 0644]
src/pl/tcl/mkMakefile.tcldefs [new file with mode: 0755]
src/pl/tcl/pltcl.c [new file with mode: 0644]
src/pl/tcl/pltcl_guide.nr [new file with mode: 0644]

diff --git a/src/pl/tcl/INSTALL b/src/pl/tcl/INSTALL
new file mode 100644 (file)
index 0000000..27d0df3
--- /dev/null
@@ -0,0 +1,43 @@
+Installation instructions for PL/Tcl
+
+1.  Build the pltcl shared library
+
+    The Makefile for the pltcl shared library assumes the sources
+    for PostgreSQL are in /usr/local/src/postgresql-6.2.1/src. Edit
+    if not.
+
+    The Makefile depends on the tclConfig.sh file that get's installed
+    with Tcl. This should either be in /usr/lib or in /usr/local/lib.
+    If it is in a different place, edit mkMakefile.tcldefs or make a
+    symbolic link to it here.
+
+    Type make and the shared library should get built.
+
+2.  Now create the PL/Tcl language in PostgreSQL
+
+    Since the pg_language system catalog is private to each database,
+    the new language can be created only for individual databases,
+    or in the template1 database. In the latter case, it is
+    automatically available in all newly created databases.
+
+    The commands to create the new language are:
+
+        create function pltcl_call_handler () returns opaque
+               as 'path-to-pltcl-shared-lib'
+               language 'C';
+
+       create trusted procedural language 'pltcl'
+               handler pltcl_call_handler
+               lancompiler 'PL/Tcl';
+
+    The trusted keyword on create procedural language tells PostgreSQL,
+    that all users (not only those with superuser privilege) are
+    permitted to create functions with LANGUAGE 'pltcl'. This is
+    absolutely safe, because there is nothing a normal user can do
+    with PL/Tcl, to get around access restrictions he/she has.
+
+3.  Use PL/Tcl
+
+    Read pltcl_guide.txt to learn how to write functions and
+    trigger procedures in PL/Tcl.
+
diff --git a/src/pl/tcl/Makefile b/src/pl/tcl/Makefile
new file mode 100644 (file)
index 0000000..df51635
--- /dev/null
@@ -0,0 +1,91 @@
+#-------------------------------------------------------------------------
+#
+# Makefile
+#    Makefile for the pltcl shared object
+#
+# IDENTIFICATION
+#    $Header: /cvsroot/pgsql/src/pl/tcl/Makefile,v 1.1 1998/02/11 14:07:55 scrappy Exp $
+#
+#-------------------------------------------------------------------------
+
+#
+# Tell make where the postgresql sources live
+#
+SRCDIR= ../../../src
+include $(SRCDIR)/Makefile.global
+
+
+#
+# Include definitions from the tclConfig.sh file
+#
+include Makefile.tcldefs
+
+
+#
+# Uncomment the following to force a specific version of the
+# Tcl shared library to be used.
+#
+#TCL_LIB_SPEC=-L/usr/lib -ltcl8.0
+
+
+#
+# Change following to how shared library that contain
+# correct references to libtcl must get built on your system.
+# Since these definitions come from the tclConfig.sh script,
+# they should work if the shared build of tcl was successful
+# on this system.
+#
+%$(TCL_SHLIB_SUFFIX):  %.o
+       $(TCL_SHLIB_LD) -o $@ $< $(TCL_SHLIB_LD_LIBS) $(TCL_LIB_SPEC) $(TCL_LIBS)
+
+
+#
+# Uncomment the following to enable the unknown command lookup
+# on the first of all calls to the call handler. See the doc
+# in the modules directory about details.
+#
+#CFLAGS+= -DPLTCL_UNKNOWN_SUPPORT
+
+
+CC = $(TCL_CC)
+CFLAGS+= -I$(LIBPQDIR) -I$(SRCDIR)/include $(TCL_SHLIB_CFLAGS)
+
+# For fmgr.h
+CFLAGS+= -I$(SRCDIR)/backend
+
+CFLAGS+= $(TCL_DEFS)
+
+LDADD+= -L$(LIBPQDIR) -lpq
+        
+#
+# DLOBJS is the dynamically-loaded object file.
+#
+DLOBJS= pltcl$(DLSUFFIX)
+
+INFILES= $(DLOBJS) 
+
+#
+# plus exports files
+#
+ifdef EXPSUFF
+INFILES+= $(DLOBJS:.o=$(EXPSUFF))
+endif
+
+#
+# Build the shared lib
+#
+all: $(INFILES)
+
+Makefile.tcldefs:
+       ./mkMakefile.tcldefs
+
+#
+# Clean 
+#
+clean:
+       rm -f $(INFILES)
+       rm -f Makefile.tcldefs
+
+install: all
+       $(INSTALL) $(INSTL_LIB_OPTS) $(DLOBJS) $(DESTDIR)$(LIBDIR)/$(DLOBJS)
+
diff --git a/src/pl/tcl/license.terms b/src/pl/tcl/license.terms
new file mode 100644 (file)
index 0000000..2a20135
--- /dev/null
@@ -0,0 +1,30 @@
+    This software is copyrighted by Jan Wieck - Hamburg.
+
+    The  following  terms  apply to all files associated with the
+    software unless explicitly disclaimed in individual files.
+
+    The author hereby grants permission  to  use,  copy,  modify,
+    distribute,  and  license this software and its documentation
+    for any purpose, provided that existing copyright notices are
+    retained  in  all  copies  and  that  this notice is included
+    verbatim in any distributions. No written agreement, license,
+    or  royalty  fee  is required for any of the authorized uses.
+    Modifications to this software may be  copyrighted  by  their
+    author  and  need  not  follow  the licensing terms described
+    here, provided that the new terms are  clearly  indicated  on
+    the first page of each file where they apply.
+
+    IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTORS BE LIABLE TO ANY
+    PARTY  FOR  DIRECT,   INDIRECT,   SPECIAL,   INCIDENTAL,   OR
+    CONSEQUENTIAL   DAMAGES  ARISING  OUT  OF  THE  USE  OF  THIS
+    SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN
+    IF  THE  AUTHOR  HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH
+    DAMAGE.
+
+    THE  AUTHOR  AND  DISTRIBUTORS  SPECIFICALLY   DISCLAIM   ANY
+    WARRANTIES,  INCLUDING,  BUT  NOT  LIMITED  TO,  THE  IMPLIED
+    WARRANTIES  OF  MERCHANTABILITY,  FITNESS  FOR  A  PARTICULAR
+    PURPOSE,  AND NON-INFRINGEMENT.  THIS SOFTWARE IS PROVIDED ON
+    AN "AS IS" BASIS, AND THE AUTHOR  AND  DISTRIBUTORS  HAVE  NO
+    OBLIGATION   TO   PROVIDE   MAINTENANCE,   SUPPORT,  UPDATES,
+    ENHANCEMENTS, OR MODIFICATIONS.
diff --git a/src/pl/tcl/mkMakefile.tcldefs b/src/pl/tcl/mkMakefile.tcldefs
new file mode 100755 (executable)
index 0000000..ba1a9c6
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh
+if [ -f ./tclConfig.sh ]; then
+    . ./tclConfig.sh
+else
+    if [ -f /usr/lib/tclConfig.sh ]; then
+       echo "using tclConfig.sh from /usr/lib"
+       . /usr/lib/tclConfig.sh
+    else
+       if [ -f /usr/local/lib/tclConfig.sh ]; then
+           echo "using tclConfig.sh from /usr/local/lib"
+           . /usr/local/lib/tclConfig.sh
+       else
+           echo "tclConfig.sh not found in /usr/lib or /usr/local/lib"
+           echo "I need this file! Please make a symbolic link to this file"
+           echo "and start make again."
+           exit 1
+        fi
+    fi
+fi
+
+set | grep '^TCL' >Makefile.tcldefs
+exit 0
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644 (file)
index 0000000..9c5d6a8
--- /dev/null
@@ -0,0 +1,2159 @@
+/**********************************************************************
+ * pltcl.c             - PostgreSQL support for Tcl as
+ *                       procedural language (PL)
+ *
+ * IDENTIFICATION
+ *    $Header: /cvsroot/pgsql/src/pl/tcl/pltcl.c,v 1.1 1998/02/11 14:07:59 scrappy Exp $
+ *
+ *    This software is copyrighted by Jan Wieck - Hamburg.
+ *
+ *    The author hereby grants permission  to  use,  copy,  modify,
+ *    distribute,  and  license this software and its documentation
+ *    for any purpose, provided that existing copyright notices are
+ *    retained  in  all  copies  and  that  this notice is included
+ *    verbatim in any distributions. No written agreement, license,
+ *    or  royalty  fee  is required for any of the authorized uses.
+ *    Modifications to this software may be  copyrighted  by  their
+ *    author  and  need  not  follow  the licensing terms described
+ *    here, provided that the new terms are  clearly  indicated  on
+ *    the first page of each file where they apply.
+ *
+ *    IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTORS BE LIABLE TO ANY
+ *    PARTY  FOR  DIRECT,   INDIRECT,   SPECIAL,   INCIDENTAL,   OR
+ *    CONSEQUENTIAL   DAMAGES  ARISING  OUT  OF  THE  USE  OF  THIS
+ *    SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN
+ *    IF  THE  AUTHOR  HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ *    DAMAGE.
+ *
+ *    THE  AUTHOR  AND  DISTRIBUTORS  SPECIFICALLY   DISCLAIM   ANY
+ *    WARRANTIES,  INCLUDING,  BUT  NOT  LIMITED  TO,  THE  IMPLIED
+ *    WARRANTIES  OF  MERCHANTABILITY,  FITNESS  FOR  A  PARTICULAR
+ *    PURPOSE,  AND NON-INFRINGEMENT.  THIS SOFTWARE IS PROVIDED ON
+ *    AN "AS IS" BASIS, AND THE AUTHOR  AND  DISTRIBUTORS  HAVE  NO
+ *    OBLIGATION   TO   PROVIDE   MAINTENANCE,   SUPPORT,  UPDATES,
+ *    ENHANCEMENTS, OR MODIFICATIONS.
+ *
+ **********************************************************************/
+
+#include <tcl.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <setjmp.h>
+
+#include "executor/spi.h"
+#include "commands/trigger.h"
+#include "utils/elog.h"
+#include "utils/builtins.h"
+#include "fmgr.h"
+#include "access/heapam.h"
+
+#include "utils/syscache.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+
+
+/**********************************************************************
+ * The information we cache about loaded procedures
+ **********************************************************************/
+typedef struct pltcl_proc_desc {
+    char       *proname;
+    FmgrInfo   result_in_func;
+    Oid                result_in_elem;
+    int                result_in_len;
+    int                nargs;
+    FmgrInfo   arg_out_func[MAXFMGRARGS];
+    Oid                arg_out_elem[MAXFMGRARGS];
+    int                arg_out_len[MAXFMGRARGS];
+    int                arg_is_rel[MAXFMGRARGS];
+} pltcl_proc_desc;
+
+
+/**********************************************************************
+ * The information we cache about prepared and saved plans
+ **********************************************************************/
+typedef struct pltcl_query_desc {
+    char       qname[20];
+    void       *plan;
+    int                nargs;
+    Oid                *argtypes;
+    FmgrInfo   *arginfuncs;
+    Oid                *argtypelems;
+    Datum      *argvalues;
+    int                *arglen;
+} pltcl_query_desc;
+
+
+/************************************************************
+ * Make Warn_restart from tcop/postgres.c visible for us.
+ * The longjmp() mechanism of the elog(ERROR,...) restart let's
+ * interpreter levels lay around. So we must tidy up in that
+ * case and thus, we have to catch the longjmp's sometimes to
+ * return though all the interpreter levels back.
+ *
+ * It's ugly - Jan
+ ************************************************************/
+#if defined(nextstep)
+#define        sigjmp_buf      jmp_buf
+#define sigsetjmp(x,y) setjmp(x)
+#define siglongjmp     longjmp
+#endif
+
+extern sigjmp_buf      Warn_restart;   /* in tcop/postgres.c */
+
+/**********************************************************************
+ * Global data
+ **********************************************************************/
+static int             pltcl_firstcall         = 1;
+static int             pltcl_call_level        = 0;
+static int             pltcl_restart_in_progress = 0;
+static Tcl_Interp      *pltcl_hold_interp      = NULL;
+static Tcl_Interp      *pltcl_safe_interp      = NULL;
+static Tcl_HashTable   *pltcl_proc_hash        = NULL;
+static Tcl_HashTable   *pltcl_query_hash       = NULL;
+
+/**********************************************************************
+ * Forward declarations
+ **********************************************************************/
+static void pltcl_init_all(void);
+static void pltcl_init_safe_interp(void);
+
+#ifdef PLTCL_UNKNOWN_SUPPORT
+static void pltcl_init_load_unknown(void);
+#endif /* PLTCL_UNKNOWN_SUPPORT */
+
+Datum pltcl_call_handler(FmgrInfo *proinfo,
+       FmgrValues *proargs, bool *isNull);
+
+static Datum pltcl_func_handler(FmgrInfo *proinfo,
+       FmgrValues *proargs, bool *isNull);
+
+static HeapTuple pltcl_trigger_handler(FmgrInfo *proinfo);
+
+static int pltcl_elog(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[]);
+static int pltcl_quote(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[]);
+
+static int pltcl_SPI_exec(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[]);
+static int pltcl_SPI_prepare(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[]);
+static int pltcl_SPI_execp(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[]);
+
+static void pltcl_set_tuple_values(Tcl_Interp *interp, char *arrayname,
+       int tupno, HeapTuple tuple, TupleDesc tupdesc);
+static void pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc,
+       Tcl_DString *retval);
+       
+/**********************************************************************
+ * pltcl_init_all()            - Initialize all
+ **********************************************************************/
+static void
+pltcl_init_all(void)
+{
+    Tcl_HashEntry      *hashent;
+    Tcl_HashSearch     hashsearch;
+    pltcl_proc_desc    *prodesc;
+    pltcl_query_desc   *querydesc;
+
+    /************************************************************
+     * Do initialization only once
+     ************************************************************/
+    if (!pltcl_firstcall) return;
+
+    /************************************************************
+     * Create the dummy hold interpreter to prevent close of
+     * stdout and stderr on DeleteInterp
+     ************************************************************/
+    if (pltcl_hold_interp == NULL) {
+        if ((pltcl_hold_interp = Tcl_CreateInterp()) == NULL) {
+           elog(ERROR, "pltcl: internal error - cannot create 'hold' "
+                       "interpreter");
+       }
+    }
+
+    /************************************************************
+     * Destroy the existing safe interpreter
+     ************************************************************/
+    if (pltcl_safe_interp != NULL) {
+        Tcl_DeleteInterp(pltcl_safe_interp);
+       pltcl_safe_interp = NULL;
+    }
+
+    /************************************************************
+     * Free the proc hash table
+     ************************************************************/
+    if (pltcl_proc_hash != NULL) {
+        hashent = Tcl_FirstHashEntry(pltcl_proc_hash, &hashsearch);
+       while (hashent != NULL) {
+           prodesc = (pltcl_proc_desc *)Tcl_GetHashValue(hashent);
+           free(prodesc->proname);
+           free(prodesc);
+           hashent = Tcl_NextHashEntry(&hashsearch);
+       }
+       Tcl_DeleteHashTable(pltcl_proc_hash);
+       free(pltcl_proc_hash);
+       pltcl_proc_hash = NULL;
+    }
+
+    /************************************************************
+     * Free the prepared query hash table
+     ************************************************************/
+    if (pltcl_query_hash != NULL) {
+        hashent = Tcl_FirstHashEntry(pltcl_query_hash, &hashsearch);
+       while (hashent != NULL) {
+           querydesc = (pltcl_query_desc *)Tcl_GetHashValue(hashent);
+           free(querydesc->argtypes);
+           free(querydesc);
+           hashent = Tcl_NextHashEntry(&hashsearch);
+       }
+       Tcl_DeleteHashTable(pltcl_query_hash);
+       free(pltcl_query_hash);
+       pltcl_query_hash = NULL;
+    }
+
+    /************************************************************
+     * Now recreate a new safe interpreter
+     ************************************************************/
+    pltcl_init_safe_interp();
+
+    pltcl_firstcall = 0;
+    return;
+}
+
+
+/**********************************************************************
+ * pltcl_init_safe_interp()    - Create the safe Tcl interpreter
+ **********************************************************************/
+static void
+pltcl_init_safe_interp(void)
+{
+    /************************************************************
+     * Create the interpreter as a safe slave of the hold interp.
+     ************************************************************/
+    if ((pltcl_safe_interp = 
+               Tcl_CreateSlave(pltcl_hold_interp, "safe", 1)) == NULL) {
+        elog(ERROR, 
+           "pltcl: internal error - cannot create 'safe' interpreter");
+    }
+
+    /************************************************************
+     * Enable debugging output from the Tcl bytecode compiler
+     * To see the trace, the interpreter must be created unsafe
+     * USE ONLY FOR DEBUGGING!!!
+     ************************************************************/
+    /*
+    Tcl_SetVar(pltcl_safe_interp, "tcl_traceCompile", "1", 0);
+    */
+
+    /************************************************************
+     * Initialize the proc and query hash tables
+     ************************************************************/
+    pltcl_proc_hash  = (Tcl_HashTable *)malloc(sizeof(Tcl_HashTable));
+    pltcl_query_hash = (Tcl_HashTable *)malloc(sizeof(Tcl_HashTable));
+    Tcl_InitHashTable(pltcl_proc_hash,  TCL_STRING_KEYS);
+    Tcl_InitHashTable(pltcl_query_hash, TCL_STRING_KEYS);
+
+    /************************************************************
+     * Install the commands for SPI support in the safe interpreter
+     ************************************************************/
+    Tcl_CreateCommand(pltcl_safe_interp, "elog",
+       pltcl_elog, NULL, NULL);
+    Tcl_CreateCommand(pltcl_safe_interp, "quote",
+       pltcl_quote, NULL, NULL);
+
+    Tcl_CreateCommand(pltcl_safe_interp, "spi_exec",
+       pltcl_SPI_exec, NULL, NULL);
+    Tcl_CreateCommand(pltcl_safe_interp, "spi_prepare",
+       pltcl_SPI_prepare, NULL, NULL);
+    Tcl_CreateCommand(pltcl_safe_interp, "spi_execp",
+       pltcl_SPI_execp, NULL, NULL);
+
+#ifdef PLTCL_UNKNOWN_SUPPORT
+    /************************************************************
+     * Try to load the unknown procedure from pltcl_modules
+     ************************************************************/
+    if (SPI_connect() != SPI_OK_CONNECT) {
+        elog(ERROR, "pltcl_init_safe_interp(): SPI_connect failed");
+    }
+    pltcl_init_load_unknown();
+    if (SPI_finish() != SPI_OK_FINISH) {
+        elog(ERROR, "pltcl_init_safe_interp(): SPI_finish failed");
+    }
+#endif /* PLTCL_UNKNOWN_SUPPORT */
+}
+
+
+#ifdef PLTCL_UNKNOWN_SUPPORT
+
+/**********************************************************************
+ * pltcl_init_load_unknown()   - Load the unknown procedure from
+ *                               table pltcl_modules (if it exists)
+ **********************************************************************/
+static void
+pltcl_init_load_unknown(void)
+{
+    int        spi_rc;
+    int                tcl_rc;
+    Tcl_DString        unknown_src;
+    char       *part;
+    int                i;
+    int                fno;
+
+    /************************************************************
+     * Check if table pltcl_modules exists
+     ************************************************************/
+    spi_rc = SPI_exec("select 1 from pg_class "
+                       "where relname = 'pltcl_modules'", 1);
+    if (spi_rc != SPI_OK_SELECT) {
+        elog(ERROR, "pltcl_init_load_unknown(): select from pg_class failed");
+    }
+    if (SPI_processed == 0) {
+       return;
+    }
+
+    /************************************************************
+     * Read all the row's from it where modname = 'unknown' in
+     * the order of modseq
+     ************************************************************/
+    Tcl_DStringInit(&unknown_src);
+
+    spi_rc = SPI_exec("select modseq, modsrc from pltcl_modules "
+                       "where modname = 'unknown' "
+                       "order by modseq", 0);
+    if (spi_rc != SPI_OK_SELECT) {
+        elog(ERROR, "pltcl_init_load_unknown(): select from pltcl_modules "
+               "failed");
+    }
+
+    /************************************************************
+     * If there's nothing, module unknown doesn't exist
+     ************************************************************/
+    if (SPI_processed == 0) {
+        Tcl_DStringFree(&unknown_src);
+       elog(NOTICE, "pltcl: Module unknown not found in pltcl_modules");
+       return;
+    }
+
+    /************************************************************
+     * There is a module named unknown. Resemble the
+     * source from the modsrc attributes and evaluate
+     * it in the safe interpreter
+     ************************************************************/
+    fno = SPI_fnumber(SPI_tuptable->tupdesc, "modsrc");
+
+    for (i = 0; i < SPI_processed; i++) {
+        part = SPI_getvalue(SPI_tuptable->vals[i],
+               SPI_tuptable->tupdesc, fno);
+        if (part != NULL) {
+           Tcl_DStringAppend(&unknown_src, part, -1);
+           pfree(part);
+       }
+    }
+    tcl_rc = Tcl_GlobalEval(pltcl_safe_interp, Tcl_DStringValue(&unknown_src));
+    Tcl_DStringFree(&unknown_src);
+}
+
+#endif /* PLTCL_UNKNOWN_SUPPORT */
+
+
+/**********************************************************************
+ * pltcl_call_handler          - This is the only visible function
+ *                               of the PL interpreter. The PostgreSQL
+ *                               function manager and trigger manager
+ *                               call this function for execution of
+ *                               PL/Tcl procedures.
+ **********************************************************************/
+Datum
+pltcl_call_handler(FmgrInfo    *proinfo,
+               FmgrValues      *proargs,
+               bool            *isNull)
+{
+    Datum      retval;
+
+    /************************************************************
+     * Initialize interpreters on first call
+     ************************************************************/
+    if (pltcl_firstcall) {
+        pltcl_init_all();
+    }
+
+    /************************************************************
+     * Connect to SPI manager
+     ************************************************************/
+    if (SPI_connect() != SPI_OK_CONNECT) {
+        elog(ERROR, "pltcl: cannot connect to SPI manager");
+    }
+    /************************************************************
+     * Keep track about the nesting of Tcl-SPI-Tcl-... calls
+     ************************************************************/
+    pltcl_call_level++;
+
+    /************************************************************
+     * Determine if called as function or trigger and
+     * call appropriate subhandler
+     ************************************************************/
+    if (CurrentTriggerData == NULL) {
+       retval =  pltcl_func_handler(proinfo, proargs, isNull);
+    } else {
+       retval =  (Datum)pltcl_trigger_handler(proinfo);
+    }
+
+    pltcl_call_level--;
+
+    /************************************************************
+     * Disconnect from SPI manager
+     ************************************************************/
+    if (SPI_finish() != SPI_OK_FINISH) {
+        elog(ERROR, "pltcl: SPI_finish() failed");
+    }
+
+    return retval;
+}
+
+
+/**********************************************************************
+ * pltcl_func_handler()                - Handler for regular function calls
+ **********************************************************************/
+static Datum
+pltcl_func_handler(FmgrInfo    *proinfo,
+               FmgrValues      *proargs,
+               bool            *isNull)
+{
+    int                        i;
+    char               internal_proname[512];
+    char               *stroid;
+    Tcl_HashEntry      *hashent;
+    int                        hashnew;
+    pltcl_proc_desc    *prodesc;
+    Tcl_DString                tcl_cmd;
+    Tcl_DString                list_tmp;
+    int                        tcl_rc;
+    Datum              retval;
+    sigjmp_buf         save_restart;
+
+    /************************************************************
+     * Build our internal proc name from the functions Oid
+     ************************************************************/
+    stroid = oidout(proinfo->fn_oid);
+    strcpy(internal_proname, "__PLTcl_proc_");
+    strcat(internal_proname, stroid);
+    pfree(stroid);
+
+    /************************************************************
+     * Lookup the internal proc name in the hashtable
+     ************************************************************/
+    hashent = Tcl_FindHashEntry(pltcl_proc_hash, internal_proname);
+    if (hashent == NULL) {
+       /************************************************************
+        * If we haven't found it in the hashtable, we analyze
+        * the functions arguments and returntype and store
+        * the in-/out-functions in the prodesc block and create
+        * a new hashtable entry for it.
+        *
+        * Then we load the procedure into the safe interpreter.
+        ************************************************************/
+       HeapTuple       procTup;
+       HeapTuple       typeTup;
+       Form_pg_proc    procStruct;
+       TypeTupleForm   typeStruct;
+       Tcl_DString     proc_internal_def;
+       Tcl_DString     proc_internal_body;
+       char            proc_internal_args[4096];
+       char            *proc_source;
+       char            buf[512];
+
+       /************************************************************
+        * Allocate a new procedure description block
+        ************************************************************/
+       prodesc = (pltcl_proc_desc *)malloc(sizeof(pltcl_proc_desc));
+       prodesc->proname = malloc(strlen(internal_proname) + 1);
+       strcpy(prodesc->proname, internal_proname);
+
+       /************************************************************
+        * Lookup the pg_proc tuple by Oid
+        ************************************************************/
+        procTup = SearchSysCacheTuple(PROOID,
+               ObjectIdGetDatum(proinfo->fn_oid),
+               0, 0, 0);
+       if (!HeapTupleIsValid(procTup)) {
+           free(prodesc->proname);
+           free(prodesc);
+           elog(ERROR, "pltcl: cache lookup from pg_proc failed");
+       }
+       procStruct = (Form_pg_proc) GETSTRUCT(procTup);
+
+       /************************************************************
+        * Get the required information for input conversion of the
+        * return value.
+        ************************************************************/
+       typeTup = SearchSysCacheTuple(TYPOID,
+               ObjectIdGetDatum(procStruct->prorettype),
+               0, 0, 0);
+       if (!HeapTupleIsValid(typeTup)) {
+           free(prodesc->proname);
+           free(prodesc);
+           elog(ERROR, "pltcl: cache lookup for return type failed");
+       }
+       typeStruct = (TypeTupleForm) GETSTRUCT(typeTup);
+       
+       if (typeStruct->typrelid != InvalidOid) {
+           free(prodesc->proname);
+           free(prodesc);
+           elog(ERROR, "pltcl: return types of tuples not supported yet");
+       }
+
+       fmgr_info(typeStruct->typinput, &(prodesc->result_in_func));
+       prodesc->result_in_elem = (Oid) (typeStruct->typelem);
+       prodesc->result_in_len  = typeStruct->typlen;
+
+       /************************************************************
+        * Get the required information for output conversion
+        * of all procedure arguments
+        ************************************************************/
+       prodesc->nargs = proinfo->fn_nargs;
+       proc_internal_args[0] = '\0';
+       for (i = 0; i < proinfo->fn_nargs; i++) {
+           typeTup = SearchSysCacheTuple(TYPOID,
+               ObjectIdGetDatum(procStruct->proargtypes[i]),
+               0, 0, 0);
+           if (!HeapTupleIsValid(typeTup)) {
+               free(prodesc->proname);
+               free(prodesc);
+               elog(ERROR, "pltcl: cache lookup for argument type failed");
+           }
+           typeStruct = (TypeTupleForm) GETSTRUCT(typeTup);
+
+           if (typeStruct->typrelid != InvalidOid) {
+               prodesc->arg_is_rel[i] = 1;
+               if (i > 0) {
+                   strcat(proc_internal_args, " ");
+               }
+               sprintf(buf, "__PLTcl_Tup_%d", i + 1);
+               strcat(proc_internal_args, buf);
+               continue;
+           } else {
+               prodesc->arg_is_rel[i] = 0;
+           }
+
+           fmgr_info(typeStruct->typoutput, &(prodesc->arg_out_func[i]));
+           prodesc->arg_out_elem[i] = (Oid) (typeStruct->typelem);
+           prodesc->arg_out_len[i]  = typeStruct->typlen;
+
+           if (i > 0) {
+               strcat(proc_internal_args, " ");
+           }
+           sprintf(buf, "%d", i + 1);
+           strcat(proc_internal_args, buf);
+       }
+
+       /************************************************************
+        * Create the tcl command to define the internal
+        * procedure
+        ************************************************************/
+       Tcl_DStringInit(&proc_internal_def);
+       Tcl_DStringInit(&proc_internal_body);
+       Tcl_DStringAppendElement(&proc_internal_def, "proc");
+       Tcl_DStringAppendElement(&proc_internal_def, internal_proname);
+       Tcl_DStringAppendElement(&proc_internal_def, proc_internal_args);
+
+       /************************************************************
+        * prefix procedure body with 
+        * upvar #0 <internal_procname> GD
+        * and with appropriate upvars for tuple arguments
+        ************************************************************/
+       Tcl_DStringAppend(&proc_internal_body, "upvar #0 ", -1);
+       Tcl_DStringAppend(&proc_internal_body, internal_proname, -1);
+       Tcl_DStringAppend(&proc_internal_body, " GD\n", -1);
+       for (i = 0; i < proinfo->fn_nargs; i++) {
+           if (!prodesc->arg_is_rel[i]) continue;
+           sprintf(buf, "array set %d $__PLTcl_Tup_%d\n", i + 1, i + 1);
+           Tcl_DStringAppend(&proc_internal_body, buf, -1);
+       }
+       proc_source = textout(&(procStruct->prosrc));
+       Tcl_DStringAppend(&proc_internal_body, proc_source, -1);
+       pfree(proc_source);
+       Tcl_DStringAppendElement(&proc_internal_def, 
+               Tcl_DStringValue(&proc_internal_body));
+       Tcl_DStringFree(&proc_internal_body);
+
+       /************************************************************
+        * Create the procedure in the safe interpreter
+        ************************************************************/
+       tcl_rc = Tcl_GlobalEval(pltcl_safe_interp, 
+               Tcl_DStringValue(&proc_internal_def));
+       Tcl_DStringFree(&proc_internal_def);
+       if (tcl_rc != TCL_OK) {
+           free(prodesc->proname);
+           free(prodesc);
+           elog(ERROR, "pltcl: cannot create internal procedure %s - %s",
+               internal_proname, pltcl_safe_interp->result);
+       }
+
+       /************************************************************
+        * Add the proc description block to the hashtable
+        ************************************************************/
+       hashent = Tcl_CreateHashEntry(pltcl_proc_hash,
+               prodesc->proname, &hashnew);
+       Tcl_SetHashValue(hashent, (ClientData)prodesc);
+    } else {
+       /************************************************************
+        * Found the proc description block in the hashtable
+        ************************************************************/
+        prodesc = (pltcl_proc_desc *)Tcl_GetHashValue(hashent);
+    }
+
+    /************************************************************
+     * Create the tcl command to call the internal
+     * proc in the safe interpreter
+     ************************************************************/
+    Tcl_DStringInit(&tcl_cmd);
+    Tcl_DStringInit(&list_tmp);
+    Tcl_DStringAppendElement(&tcl_cmd, internal_proname);
+
+    /************************************************************
+     * Catch elog(ERROR) during build of the Tcl command
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       Tcl_DStringFree(&tcl_cmd);
+       Tcl_DStringFree(&list_tmp);
+       pltcl_restart_in_progress = 1;
+       if (--pltcl_call_level == 0) {
+           pltcl_restart_in_progress = 0;
+       }
+       siglongjmp(Warn_restart, 1);
+    }
+
+    /************************************************************
+     * Add all call arguments to the command
+     ************************************************************/
+    for (i = 0; i < prodesc->nargs; i++) {
+       if (prodesc->arg_is_rel[i]) {
+           /**************************************************
+            * For tuple values, add a list for 'array set ...'
+            **************************************************/
+           Tcl_DStringInit(&list_tmp);
+           pltcl_build_tuple_argument(
+                   ((TupleTableSlot *)(proargs->data[i]))->val,
+                   ((TupleTableSlot *)(proargs->data[i]))->ttc_tupleDescriptor,
+                   &list_tmp);
+           Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&list_tmp));
+           Tcl_DStringFree(&list_tmp);
+           Tcl_DStringInit(&list_tmp);
+       } else {
+           /**************************************************
+            * Single values are added as string element
+            * of their external representation
+            **************************************************/
+           char *tmp;
+
+           tmp = (*fmgr_faddr(&(prodesc->arg_out_func[i])))
+                              (proargs->data[i],
+                               prodesc->arg_out_elem[i],
+                               prodesc->arg_out_len[i]);
+           Tcl_DStringAppendElement(&tcl_cmd, tmp);
+           pfree(tmp);
+       }
+    }
+    Tcl_DStringFree(&list_tmp);
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+
+    /************************************************************
+     * Call the Tcl function
+     ************************************************************/
+    tcl_rc = Tcl_GlobalEval(pltcl_safe_interp, Tcl_DStringValue(&tcl_cmd));
+    Tcl_DStringFree(&tcl_cmd);
+
+    /************************************************************
+     * Check the return code from Tcl and handle
+     * our special restart mechanism to get rid
+     * of all nested call levels on transaction
+     * abort.
+     ************************************************************/
+    if (tcl_rc != TCL_OK || pltcl_restart_in_progress) {
+       if (!pltcl_restart_in_progress) {
+           pltcl_restart_in_progress = 1;
+           if (--pltcl_call_level == 0) {
+               pltcl_restart_in_progress = 0;
+           }
+           elog(ERROR, "pltcl: %s", pltcl_safe_interp->result);
+       }
+       if (--pltcl_call_level == 0) {
+           pltcl_restart_in_progress = 0;
+       }
+       siglongjmp(Warn_restart, 1);
+    }
+
+    /************************************************************
+     * Convert the result value from the safe interpreter
+     * into it's PostgreSQL data format and return it.
+     * Again, the call to fmgr() could fire an elog and we
+     * have to count for the current interpreter level we are
+     * on. The save_restart from above is still good.
+     ************************************************************/
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       pltcl_restart_in_progress = 1;
+       if (--pltcl_call_level == 0) {
+           pltcl_restart_in_progress = 0;
+       }
+       siglongjmp(Warn_restart, 1);
+    }
+
+    retval = (Datum)(*fmgr_faddr(&prodesc->result_in_func))
+                      (pltcl_safe_interp->result,
+                       prodesc->result_in_elem,
+                       prodesc->result_in_len);
+
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+    return retval;
+}
+
+
+/**********************************************************************
+ * pltcl_trigger_handler()     - Handler for trigger calls
+ **********************************************************************/
+static HeapTuple
+pltcl_trigger_handler(FmgrInfo *proinfo)
+{
+    TriggerData                *trigdata;
+    char               internal_proname[512];
+    char               *stroid;
+    Tcl_HashEntry      *hashent;
+    int                        hashnew;
+    pltcl_proc_desc    *prodesc;
+    TupleDesc          tupdesc;
+    HeapTuple          rettup;
+    Tcl_DString                tcl_cmd;
+    Tcl_DString                tcl_trigtup;
+    Tcl_DString                tcl_newtup;
+    int                        tcl_rc;
+    int                        i;
+
+    int                        *modattrs;
+    Datum              *modvalues;
+    char               *modnulls;
+
+    int                        ret_numvals;
+    char               **ret_values;
+
+    sigjmp_buf         save_restart;
+
+    /************************************************************
+     * Save the current trigger data local
+     ************************************************************/
+    trigdata = CurrentTriggerData;
+    CurrentTriggerData = NULL;
+
+    /************************************************************
+     * Build our internal proc name from the functions Oid
+     ************************************************************/
+    stroid = oidout(proinfo->fn_oid);
+    strcpy(internal_proname, "__PLTcl_proc_");
+    strcat(internal_proname, stroid);
+    pfree(stroid);
+
+    /************************************************************
+     * Lookup the internal proc name in the hashtable
+     ************************************************************/
+    hashent = Tcl_FindHashEntry(pltcl_proc_hash, internal_proname);
+    if (hashent == NULL) {
+       /************************************************************
+        * If we haven't found it in the hashtable,
+        * we load the procedure into the safe interpreter.
+        ************************************************************/
+       Tcl_DString     proc_internal_def;
+       Tcl_DString     proc_internal_body;
+       HeapTuple       procTup;
+       Form_pg_proc    procStruct;
+       char            *proc_source;
+
+       /************************************************************
+        * Allocate a new procedure description block
+        ************************************************************/
+       prodesc = (pltcl_proc_desc *)malloc(sizeof(pltcl_proc_desc));
+       memset(prodesc, 0, sizeof(pltcl_proc_desc));
+       prodesc->proname = malloc(strlen(internal_proname) + 1);
+       strcpy(prodesc->proname, internal_proname);
+
+       /************************************************************
+        * Lookup the pg_proc tuple by Oid
+        ************************************************************/
+        procTup = SearchSysCacheTuple(PROOID,
+               ObjectIdGetDatum(proinfo->fn_oid),
+               0, 0, 0);
+       if (!HeapTupleIsValid(procTup)) {
+           free(prodesc->proname);
+           free(prodesc);
+           elog(ERROR, "pltcl: cache lookup from pg_proc failed");
+       }
+       procStruct = (Form_pg_proc) GETSTRUCT(procTup);
+
+       /************************************************************
+        * Create the tcl command to define the internal
+        * procedure
+        ************************************************************/
+       Tcl_DStringInit(&proc_internal_def);
+       Tcl_DStringInit(&proc_internal_body);
+       Tcl_DStringAppendElement(&proc_internal_def, "proc");
+       Tcl_DStringAppendElement(&proc_internal_def, internal_proname);
+       Tcl_DStringAppendElement(&proc_internal_def, 
+               "TG_name TG_relid TG_relatts TG_when TG_level TG_op __PLTcl_Tup_NEW __PLTcl_Tup_OLD args");
+
+       /************************************************************
+        * prefix procedure body with 
+        * upvar #0 <internal_procname> GD
+        * and with appropriate setting of NEW, OLD,
+        * and the arguments as numerical variables.
+        ************************************************************/
+       Tcl_DStringAppend(&proc_internal_body, "upvar #0 ", -1);
+       Tcl_DStringAppend(&proc_internal_body, internal_proname, -1);
+       Tcl_DStringAppend(&proc_internal_body, " GD\n", -1);
+
+       Tcl_DStringAppend(&proc_internal_body,
+               "array set NEW $__PLTcl_Tup_NEW\n", -1);
+       Tcl_DStringAppend(&proc_internal_body,
+               "array set OLD $__PLTcl_Tup_OLD\n", -1);
+
+       Tcl_DStringAppend(&proc_internal_body,
+               "set i 0\n"
+               "set v 0\n"
+               "foreach v $args {\n"
+               "  incr i\n"
+               "  set $i $v\n"
+               "}\n"
+               "unset i v\n\n", -1);
+
+       proc_source = textout(&(procStruct->prosrc));
+       Tcl_DStringAppend(&proc_internal_body, proc_source, -1);
+       pfree(proc_source);
+       Tcl_DStringAppendElement(&proc_internal_def, 
+               Tcl_DStringValue(&proc_internal_body));
+       Tcl_DStringFree(&proc_internal_body);
+
+       /************************************************************
+        * Create the procedure in the safe interpreter
+        ************************************************************/
+       tcl_rc = Tcl_GlobalEval(pltcl_safe_interp, 
+               Tcl_DStringValue(&proc_internal_def));
+       Tcl_DStringFree(&proc_internal_def);
+       if (tcl_rc != TCL_OK) {
+           free(prodesc->proname);
+           free(prodesc);
+           elog(ERROR, "pltcl: cannot create internal procedure %s - %s",
+               internal_proname, pltcl_safe_interp->result);
+       }
+
+       /************************************************************
+        * Add the proc description block to the hashtable
+        ************************************************************/
+       hashent = Tcl_CreateHashEntry(pltcl_proc_hash,
+               prodesc->proname, &hashnew);
+       Tcl_SetHashValue(hashent, (ClientData)prodesc);
+    } else {
+       /************************************************************
+        * Found the proc description block in the hashtable
+        ************************************************************/
+        prodesc = (pltcl_proc_desc *)Tcl_GetHashValue(hashent);
+    }
+
+    tupdesc = trigdata->tg_relation->rd_att;
+
+    /************************************************************
+     * Create the tcl command to call the internal
+     * proc in the safe interpreter
+     ************************************************************/
+    Tcl_DStringInit(&tcl_cmd);
+    Tcl_DStringInit(&tcl_trigtup);
+    Tcl_DStringInit(&tcl_newtup);
+
+    /************************************************************
+     * We call external functions below - care for elog(ERROR)
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       Tcl_DStringFree(&tcl_cmd);
+       Tcl_DStringFree(&tcl_trigtup);
+       Tcl_DStringFree(&tcl_newtup);
+       pltcl_restart_in_progress = 1;
+       if (--pltcl_call_level == 0) {
+           pltcl_restart_in_progress = 0;
+       }
+       siglongjmp(Warn_restart, 1);
+    }
+
+    /* The procedure name */
+    Tcl_DStringAppendElement(&tcl_cmd, internal_proname);
+
+    /* The trigger name for argument TG_name */
+    Tcl_DStringAppendElement(&tcl_cmd, trigdata->tg_trigger->tgname);
+
+    /* The oid of the trigger relation for argument TG_relid */
+    stroid = oidout(trigdata->tg_relation->rd_id);
+    Tcl_DStringAppendElement(&tcl_cmd, stroid);
+    pfree(stroid);
+
+    /* A list of attribute names for argument TG_relatts */
+    Tcl_DStringAppendElement(&tcl_trigtup, "");
+    for (i = 0; i < tupdesc->natts; i++) {
+        Tcl_DStringAppendElement(&tcl_trigtup, tupdesc->attrs[i]->attname.data);
+    }
+    Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
+    Tcl_DStringFree(&tcl_trigtup);
+    Tcl_DStringInit(&tcl_trigtup);
+
+    /* The when part of the event for TG_when */
+    if (TRIGGER_FIRED_BEFORE(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
+    } 
+    else if (TRIGGER_FIRED_AFTER(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
+    }
+    else {
+        Tcl_DStringAppendElement(&tcl_cmd, "UNKNOWN");
+    }
+
+    /* The level part of the event for TG_level */
+    if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "ROW");
+    } 
+    else if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "STATEMENT");
+    }
+    else {
+        Tcl_DStringAppendElement(&tcl_cmd, "UNKNOWN");
+    }
+
+    /* Build the data list for the trigtuple */
+    pltcl_build_tuple_argument(trigdata->tg_trigtuple,
+               tupdesc, &tcl_trigtup);
+
+    /* Now the command part of the event for TG_op and data for NEW and OLD */
+    if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "INSERT");
+
+       Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
+       Tcl_DStringAppendElement(&tcl_cmd, "");
+       
+       rettup = trigdata->tg_trigtuple;
+    } 
+    else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "DELETE");
+
+       Tcl_DStringAppendElement(&tcl_cmd, "");
+       Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
+
+       rettup = trigdata->tg_trigtuple;
+    }
+    else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) {
+        Tcl_DStringAppendElement(&tcl_cmd, "UPDATE");
+
+       pltcl_build_tuple_argument(trigdata->tg_newtuple,
+                   tupdesc, &tcl_newtup);
+
+       Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_newtup));
+       Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
+
+       rettup = trigdata->tg_newtuple;
+    }
+    else {
+        Tcl_DStringAppendElement(&tcl_cmd, "UNKNOWN");
+
+       Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
+       Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
+
+       rettup = trigdata->tg_trigtuple;
+    }
+
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+    Tcl_DStringFree(&tcl_trigtup);
+    Tcl_DStringFree(&tcl_newtup);
+
+    /************************************************************
+     * Finally append the arguments from CREATE TRIGGER
+     ************************************************************/
+    for (i = 0; i < trigdata->tg_trigger->tgnargs; i++) {
+        Tcl_DStringAppendElement(&tcl_cmd, trigdata->tg_trigger->tgargs[i]);
+    }
+
+    /************************************************************
+     * Call the Tcl function
+     ************************************************************/
+    tcl_rc = Tcl_GlobalEval(pltcl_safe_interp, Tcl_DStringValue(&tcl_cmd));
+    Tcl_DStringFree(&tcl_cmd);
+
+    /************************************************************
+     * Check the return code from Tcl and handle
+     * our special restart mechanism to get rid
+     * of all nested call levels on transaction
+     * abort.
+     ************************************************************/
+    if (tcl_rc == TCL_ERROR || pltcl_restart_in_progress) {
+       if (!pltcl_restart_in_progress) {
+           pltcl_restart_in_progress = 1;
+           if (--pltcl_call_level == 0) {
+               pltcl_restart_in_progress = 0;
+           }
+           elog(ERROR, "pltcl: %s", pltcl_safe_interp->result);
+       }
+       if (--pltcl_call_level == 0) {
+           pltcl_restart_in_progress = 0;
+       }
+       siglongjmp(Warn_restart, 1);
+    }
+
+    switch (tcl_rc) {
+        case TCL_OK:
+               break;
+        
+       default:
+               elog(ERROR, "pltcl: unsupported TCL return code %d", tcl_rc);
+    }
+
+    /************************************************************
+     * The return value from the procedure might be one of
+     * the magic strings OK or SKIP or a list from array get
+     ************************************************************/
+    if (strcmp(pltcl_safe_interp->result, "OK") == 0) {
+        return rettup;
+    }
+    if (strcmp(pltcl_safe_interp->result, "SKIP") == 0) {
+        return (HeapTuple)NULL;;
+    }
+
+    /************************************************************
+     * Convert the result value from the safe interpreter
+     * and setup structures for SPI_modifytuple();
+     ************************************************************/
+    if (Tcl_SplitList(pltcl_safe_interp, pltcl_safe_interp->result,
+               &ret_numvals, &ret_values) != TCL_OK) {
+       elog(NOTICE, "pltcl: cannot split return value from trigger");
+       elog(ERROR, "pltcl: %s", pltcl_safe_interp->result);
+    }
+
+    if (ret_numvals % 2 != 0) {
+       ckfree(ret_values);
+        elog(ERROR, "pltcl: invalid return list from trigger - must have even # of elements");
+    }
+
+    modattrs  = (int *)palloc(tupdesc->natts * sizeof(int));
+    modvalues = (Datum *)palloc(tupdesc->natts * sizeof(Datum));
+    for (i = 0; i < tupdesc->natts; i++) {
+        modattrs[i]  = i + 1;
+       modvalues[i] = (Datum)NULL;
+    }
+
+    modnulls  = palloc(tupdesc->natts + 1);
+    memset(modnulls, 'n', tupdesc->natts);
+    modnulls[tupdesc->natts] = '\0';
+
+    /************************************************************
+     * Care for possible elog(ERROR)'s below
+     ************************************************************/
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       ckfree(ret_values);
+       pltcl_restart_in_progress = 1;
+       if (--pltcl_call_level == 0) {
+           pltcl_restart_in_progress = 0;
+       }
+       siglongjmp(Warn_restart, 1);
+    }
+
+    i = 0;
+    while(i < ret_numvals) {
+        int            attnum;
+       HeapTuple       typeTup;
+       Oid             typinput;
+       Oid             typelem;
+       FmgrInfo        finfo;
+
+       /************************************************************
+        * Ignore pseudo elements with a dot name
+        ************************************************************/
+       if (*(ret_values[i]) == '.') {
+           i += 2;
+           continue;
+       }
+
+       /************************************************************
+        * Get the attribute number
+        ************************************************************/
+       attnum = SPI_fnumber(tupdesc, ret_values[i++]);
+       if (attnum == SPI_ERROR_NOATTRIBUTE) {
+           elog(ERROR, "pltcl: invalid attribute '%s'", ret_values[--i]);
+       }
+
+       /************************************************************
+        * Lookup the attribute type in the syscache
+        * for the input function
+        ************************************************************/
+       typeTup = SearchSysCacheTuple(TYPOID,
+               ObjectIdGetDatum(tupdesc->attrs[attnum - 1]->atttypid),
+               0, 0, 0);
+       if (!HeapTupleIsValid(typeTup)) {
+           elog(ERROR, "pltcl: Cache lookup for attribute '%s' type %ld failed",
+               ret_values[--i],
+               ObjectIdGetDatum(tupdesc->attrs[attnum - 1]->atttypid));
+       }
+       typinput = (Oid) (((TypeTupleForm)GETSTRUCT(typeTup))->typinput);
+       typelem  = (Oid) (((TypeTupleForm)GETSTRUCT(typeTup))->typelem);
+
+       /************************************************************
+        * Set the attribute to NOT NULL and convert the contents
+        ************************************************************/
+       modnulls[attnum - 1] = ' ';
+       fmgr_info(typinput, &finfo);
+       modvalues[attnum - 1] = (Datum)(*fmgr_faddr(&finfo))
+               (ret_values[i++],
+               typelem, 
+               (!VARLENA_FIXED_SIZE(tupdesc->attrs[attnum - 1]))
+                   ? tupdesc->attrs[attnum - 1]->attlen
+                   : tupdesc->attrs[attnum - 1]->atttypmod
+       );
+    }
+
+
+    rettup = SPI_modifytuple(trigdata->tg_relation, rettup, tupdesc->natts,
+               modattrs, modvalues, modnulls);
+
+    pfree(modattrs);
+    pfree(modvalues);
+    pfree(modnulls);
+
+    if (rettup == NULL) {
+        elog(ERROR, "pltcl: SPI_modifytuple() failed - RC = %d\n", SPI_result);
+    }
+
+    ckfree(ret_values);
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+
+    return rettup;
+}
+
+
+/**********************************************************************
+ * pltcl_elog()                - elog() support for PLTcl
+ **********************************************************************/
+static int pltcl_elog(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[])
+{
+    int level;
+    sigjmp_buf save_restart;
+
+    /************************************************************
+     * Suppress messages during the restart process
+     ************************************************************/
+    if (pltcl_restart_in_progress)
+        return TCL_ERROR;
+
+    /************************************************************
+     * Catch the restart longjmp and begin a controlled
+     * return though all interpreter levels if it happens
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       pltcl_restart_in_progress = 1;
+       return TCL_ERROR;
+    }
+
+    if (argc != 3) {
+        Tcl_SetResult(interp, "syntax error - 'elog level msg'",
+               TCL_VOLATILE);
+        return TCL_ERROR;
+    }
+
+    if (strcmp(argv[1], "NOTICE") == 0) {
+        level = NOTICE;
+    } else
+    if (strcmp(argv[1], "WARN") == 0) {
+        level = ERROR;
+    } else
+    if (strcmp(argv[1], "ERROR") == 0) {
+        level = ERROR;
+    } else
+    if (strcmp(argv[1], "FATAL") == 0) {
+        level = FATAL;
+    } else
+    if (strcmp(argv[1], "DEBUG") == 0) {
+        level = DEBUG;
+    } else
+    if (strcmp(argv[1], "NOIND") == 0) {
+        level = NOIND;
+    } else {
+        Tcl_AppendResult(interp, "Unknown elog level '", argv[1],
+                       "'", NULL);
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+        return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Call elog(), restore the original restart address
+     * and return to the caller (if not catched)
+     ************************************************************/
+    elog(level, argv[2]);
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+    return TCL_OK;
+}
+
+
+/**********************************************************************
+ * pltcl_quote()       - quote literal strings that are to
+ *                       be used in SPI_exec query strings
+ **********************************************************************/
+static int pltcl_quote(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[])
+{
+    char       *tmp;
+    char       *cp1;
+    char       *cp2;
+
+    /************************************************************
+     * Check call syntax
+     ************************************************************/
+    if (argc != 2) {
+        Tcl_SetResult(interp, "syntax error - 'quote string'", TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Allocate space for the maximum the string can
+     * grow to and initialize pointers
+     ************************************************************/
+    tmp = palloc(strlen(argv[1]) * 2 + 1);
+    cp1 = argv[1];
+    cp2 = tmp;
+
+    /************************************************************
+     * Walk through string and double every quote and backslash
+     ************************************************************/
+    while (*cp1) {
+        if (*cp1 == '\'') {
+           *cp2++ = '\'';
+       } else {
+           if (*cp1 == '\\') {
+               *cp2++ = '\\';
+           }
+       }
+       *cp2++ = *cp1++;
+    }
+
+    /************************************************************
+     * Terminate the string and set it as result
+     ************************************************************/
+    *cp2 = '\0';
+    Tcl_SetResult(interp, tmp, TCL_VOLATILE);
+    pfree(tmp);
+    return TCL_OK;
+}
+
+
+/**********************************************************************
+ * pltcl_SPI_exec()            - The builtin SPI_exec command
+ *                               for the safe interpreter
+ **********************************************************************/
+static int pltcl_SPI_exec(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[])
+{
+    int                spi_rc;
+    char       buf[64];
+    int                count = 0;
+    char       *arrayname = NULL;
+    int                query_idx;
+    int                i;
+    int                loop_rc;
+    int                ntuples;
+    HeapTuple  *tuples;
+    TupleDesc  tupdesc;
+    sigjmp_buf save_restart;
+
+    char       *usage = "syntax error - 'SPI_exec "
+                            "?-count n? "
+                            "?-array name? query ?loop body?";
+
+    /************************************************************
+     * Don't do anything if we are already in restart mode
+     ************************************************************/
+    if (pltcl_restart_in_progress)
+        return TCL_ERROR;
+
+    /************************************************************
+     * Check the call syntax and get the count option
+     ************************************************************/
+    if (argc < 2) {
+        Tcl_SetResult(interp, usage, TCL_VOLATILE);
+        return TCL_ERROR;
+    }
+
+    i = 1;
+    while (i < argc) {
+        if (strcmp(argv[i], "-array") == 0) {
+           if (++i >= argc) {
+               Tcl_SetResult(interp, usage, TCL_VOLATILE);
+               return TCL_ERROR;
+           }
+           arrayname = argv[i++];
+           continue;
+       }
+
+        if (strcmp(argv[i], "-count") == 0) {
+           if (++i >= argc) {
+               Tcl_SetResult(interp, usage, TCL_VOLATILE);
+               return TCL_ERROR;
+           }
+           if (Tcl_GetInt(interp, argv[i++], &count) != TCL_OK) {
+               return TCL_ERROR;
+           }
+           continue;
+       }
+
+       break;
+    }
+
+    query_idx = i;
+    if (query_idx >= argc) {
+       Tcl_SetResult(interp, usage, TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Prepare to start a controlled return through all
+     * interpreter levels on transaction abort
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       pltcl_restart_in_progress = 1;
+       Tcl_SetResult(interp, "Transaction abort", TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Execute the query and handle return codes
+     ************************************************************/
+    spi_rc = SPI_exec(argv[query_idx], count);
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+
+    switch (spi_rc) {
+        case SPI_OK_UTILITY:
+           Tcl_SetResult(interp, "0", TCL_VOLATILE);
+           return TCL_OK;
+
+        case SPI_OK_SELINTO:
+        case SPI_OK_INSERT:
+        case SPI_OK_DELETE:
+        case SPI_OK_UPDATE:
+           sprintf(buf, "%d", SPI_processed);
+           Tcl_SetResult(interp, buf, TCL_VOLATILE);
+           return TCL_OK;
+
+        case SPI_OK_SELECT:
+           break;
+
+        case SPI_ERROR_ARGUMENT:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_ARGUMENT", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_UNCONNECTED:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_UNCONNECTED", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_COPY:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_COPY", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_CURSOR:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_CURSOR", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_TRANSACTION:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_TRANSACTION", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_OPUNKNOWN:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_OPUNKNOWN", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        default:
+           sprintf(buf, "%d", spi_rc);
+           Tcl_AppendResult(interp, "pltcl: SPI_exec() failed - ",
+               "unknown RC ", buf, NULL);
+            return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Only SELECT queries fall through to here - remember the
+     * tuples we got
+     ************************************************************/
+
+    ntuples = SPI_processed;
+    if (ntuples > 0) {
+       tuples  = SPI_tuptable->vals;
+       tupdesc = SPI_tuptable->tupdesc;
+    }
+
+    /************************************************************
+     * Again prepare for elog(ERROR)
+     ************************************************************/
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       pltcl_restart_in_progress = 1;
+       Tcl_SetResult(interp, "Transaction abort", TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * If there is no loop body given, just set the variables
+     * from the first tuple (if any) and return the number of
+     * tuples selected
+     ************************************************************/
+    if (argc == query_idx + 1) {
+       if (ntuples > 0) {
+           pltcl_set_tuple_values(interp, arrayname, 0, tuples[0], tupdesc);
+       }
+        sprintf(buf, "%d", ntuples);
+       Tcl_SetResult(interp, buf, TCL_VOLATILE);
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       return TCL_OK;
+    }
+
+    /************************************************************
+     * There is a loop body - process all tuples and evaluate
+     * the body on each
+     ************************************************************/
+    query_idx++;
+    for (i = 0; i < ntuples; i++) {
+        pltcl_set_tuple_values(interp, arrayname, i, tuples[i], tupdesc);
+
+       loop_rc = Tcl_Eval(interp, argv[query_idx]);
+
+        if (loop_rc == TCL_OK) continue;
+       if (loop_rc == TCL_CONTINUE) continue;
+       if (loop_rc == TCL_RETURN) {
+           memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+           return TCL_RETURN;
+       }
+       if (loop_rc == TCL_BREAK) break;
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Finally return the number of tuples
+     ************************************************************/
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+    sprintf(buf, "%d", ntuples);
+    Tcl_SetResult(interp, buf, TCL_VOLATILE);
+    return TCL_OK;
+}
+
+
+/**********************************************************************
+ * pltcl_SPI_prepare()         - Builtin support for prepared plans
+ *                               The Tcl command SPI_prepare
+ *                               allways saves the plan using
+ *                               SPI_saveplan and returns a key for
+ *                               access. There is no chance to prepare
+ *                               and not save the plan currently.
+ **********************************************************************/
+static int pltcl_SPI_prepare(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[])
+{
+    int                        nargs;
+    char               **args;
+    pltcl_query_desc   *qdesc;
+    void               *plan;
+    int                        i;
+    HeapTuple          typeTup;
+    Tcl_HashEntry      *hashent;
+    int                        hashnew;
+    sigjmp_buf         save_restart;
+
+    /************************************************************
+     * Don't do anything if we are already in restart mode
+     ************************************************************/
+    if (pltcl_restart_in_progress)
+       return TCL_ERROR;
+
+    /************************************************************
+     * Check the call syntax
+     ************************************************************/
+    if (argc != 3) {
+        Tcl_SetResult(interp, "syntax error - 'SPI_prepare query argtypes'",
+               TCL_VOLATILE);
+        return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Split the argument type list
+     ************************************************************/
+    if (Tcl_SplitList(interp, argv[2], &nargs, &args) != TCL_OK) {
+        return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Allocate the new querydesc structure
+     ************************************************************/
+    qdesc = (pltcl_query_desc *)malloc(sizeof(pltcl_query_desc));
+    sprintf(qdesc->qname, "%lx", (long)qdesc);
+    qdesc->nargs       = nargs;
+    qdesc->argtypes    = (Oid *)malloc(nargs * sizeof(Oid));
+    qdesc->arginfuncs  = (FmgrInfo *)malloc(nargs * sizeof(FmgrInfo));
+    qdesc->argtypelems = (Oid *)malloc(nargs * sizeof(Oid));
+    qdesc->argvalues   = (Datum *)malloc(nargs * sizeof(Datum));
+    qdesc->arglen      = (int *)malloc(nargs * sizeof(int));
+
+    /************************************************************
+     * Prepare to start a controlled return through all
+     * interpreter levels on transaction abort
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       pltcl_restart_in_progress = 1;
+       free(qdesc->argtypes);
+       free(qdesc->arginfuncs);
+       free(qdesc->argtypelems);
+       free(qdesc->argvalues);
+       free(qdesc->arglen);
+       free(qdesc);
+       ckfree(args);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Lookup the argument types by name in the system cache
+     * and remember the required information for input conversion
+     ************************************************************/
+    for (i = 0; i < nargs; i++) {
+        typeTup = SearchSysCacheTuple(TYPNAME,
+               PointerGetDatum(args[i]),
+               0, 0, 0);
+        if (!HeapTupleIsValid(typeTup)) {
+           elog(ERROR, "pltcl: Cache lookup of type %s failed", args[i]);
+       }
+       qdesc->argtypes[i] = typeTup->t_oid;
+       fmgr_info(((TypeTupleForm) GETSTRUCT(typeTup))->typinput,
+               &(qdesc->arginfuncs[i]));
+       qdesc->argtypelems[i] = ((TypeTupleForm) GETSTRUCT(typeTup))->typelem;
+       qdesc->argvalues[i] = (Datum)NULL;
+       qdesc->arglen[i] = (int)(((TypeTupleForm) GETSTRUCT(typeTup))->typlen);
+    }
+
+    /************************************************************
+     * Prepare the plan and check for errors
+     ************************************************************/
+    plan = SPI_prepare(argv[1], nargs, qdesc->argtypes);
+
+    if (plan == NULL) {
+       char buf[128];
+       char *reason;
+
+       memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+
+       switch(SPI_result) {
+           case SPI_ERROR_ARGUMENT:
+               reason = "SPI_ERROR_ARGUMENT";
+               break;
+
+           case SPI_ERROR_UNCONNECTED:
+               reason = "SPI_ERROR_UNCONNECTED";
+               break;
+
+           case SPI_ERROR_COPY:
+               reason = "SPI_ERROR_COPY";
+               break;
+
+           case SPI_ERROR_CURSOR:
+               reason = "SPI_ERROR_CURSOR";
+               break;
+
+           case SPI_ERROR_TRANSACTION:
+               reason = "SPI_ERROR_TRANSACTION";
+               break;
+
+           case SPI_ERROR_OPUNKNOWN:
+               reason = "SPI_ERROR_OPUNKNOWN";
+               break;
+
+           default:
+               sprintf(buf, "unknown RC %d", SPI_result);
+               reason = buf;
+               break;
+               
+       }
+
+       elog(ERROR, "pltcl: SPI_prepare() failed - %s", reason);
+    }
+
+    /************************************************************
+     * Save the plan
+     ************************************************************/
+    qdesc->plan = SPI_saveplan(plan);
+    if (qdesc->plan == NULL) {
+       char buf[128];
+       char *reason;
+
+       memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+
+       switch(SPI_result) {
+           case SPI_ERROR_ARGUMENT:
+               reason = "SPI_ERROR_ARGUMENT";
+               break;
+
+           case SPI_ERROR_UNCONNECTED:
+               reason = "SPI_ERROR_UNCONNECTED";
+               break;
+
+           default:
+               sprintf(buf, "unknown RC %d", SPI_result);
+               reason = buf;
+               break;
+               
+       }
+
+       elog(ERROR, "pltcl: SPI_saveplan() failed - %s", reason);
+    }
+
+    /************************************************************
+     * Insert a hashtable entry for the plan and return
+     * the key to the caller
+     ************************************************************/
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+    hashent = Tcl_CreateHashEntry(pltcl_query_hash, qdesc->qname, &hashnew);
+    Tcl_SetHashValue(hashent, (ClientData)qdesc);
+
+    Tcl_SetResult(interp, qdesc->qname, TCL_VOLATILE);
+    return TCL_OK;
+}
+
+
+/**********************************************************************
+ * pltcl_SPI_execp()           - Execute a prepared plan
+ **********************************************************************/
+static int pltcl_SPI_execp(ClientData cdata, Tcl_Interp *interp,
+       int argc, char *argv[])
+{
+    int                        spi_rc;
+    char               buf[64];
+    int                        i, j;
+    int                        loop_body;
+    Tcl_HashEntry      *hashent;
+    pltcl_query_desc   *qdesc;
+    char               *nulls = NULL;
+    char               *arrayname = NULL;
+    int                        count = 0;
+    int                        callnargs;
+    static char                **callargs = NULL;
+    int                        loop_rc;
+    int                        ntuples;
+    HeapTuple          *tuples = NULL;
+    TupleDesc          tupdesc = NULL;
+    sigjmp_buf         save_restart;
+
+    char               *usage = "syntax error - 'SPI_execp "
+                            "?-nulls string? ?-count n? "
+                            "?-array name? query ?args? ?loop body?";
+
+    /************************************************************
+     * Tidy up from an earlier abort
+     ************************************************************/
+    if (callargs != NULL) {
+        ckfree(callargs);
+       callargs = NULL;
+    }
+
+    /************************************************************
+     * Don't do anything if we are already in restart mode
+     ************************************************************/
+    if (pltcl_restart_in_progress)
+        return TCL_ERROR;
+
+    /************************************************************
+     * Get the options and check syntax
+     ************************************************************/
+    i = 1;
+    while (i < argc) {
+        if (strcmp(argv[i], "-array") == 0) {
+           if (++i >= argc) {
+               Tcl_SetResult(interp, usage, TCL_VOLATILE);
+               return TCL_ERROR;
+           }
+           arrayname = argv[i++];
+           continue;
+       }
+        if (strcmp(argv[i], "-nulls") == 0) {
+           if (++i >= argc) {
+               Tcl_SetResult(interp, usage, TCL_VOLATILE);
+               return TCL_ERROR;
+           }
+           nulls = argv[i++];
+           continue;
+       }
+        if (strcmp(argv[i], "-count") == 0) {
+           if (++i >= argc) {
+               Tcl_SetResult(interp, usage, TCL_VOLATILE);
+               return TCL_ERROR;
+           }
+           if (Tcl_GetInt(interp, argv[i++], &count) != TCL_OK) {
+               return TCL_ERROR;
+           }
+           continue;
+       }
+
+       break;
+    }
+
+    /************************************************************
+     * Check minimum call arguments
+     ************************************************************/
+    if (i >= argc) {
+        Tcl_SetResult(interp, usage, TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Get the prepared plan descriptor by it's key
+     ************************************************************/
+    hashent = Tcl_FindHashEntry(pltcl_query_hash, argv[i++]);
+    if (hashent == NULL) {
+        Tcl_AppendResult(interp, "invalid queryid '", argv[--i], "'", NULL);
+       return TCL_ERROR;
+    }
+    qdesc = (pltcl_query_desc *)Tcl_GetHashValue(hashent);
+
+    /************************************************************
+     * If a nulls string is given, check for correct length
+     ************************************************************/
+    if (nulls != NULL) {
+       if (strlen(nulls) != qdesc->nargs) {
+           Tcl_SetResult(interp, 
+               "length of nulls string doesn't match # of arguments",
+               TCL_VOLATILE);
+           return TCL_ERROR;
+       }
+    }
+
+    /************************************************************
+     * If there was a argtype list on preparation, we need
+     * an argument value list now
+     ************************************************************/
+    if (qdesc->nargs > 0) {
+        if (i >= argc) {
+           Tcl_SetResult(interp, "missing argument list", TCL_VOLATILE);
+           return TCL_ERROR;
+       }
+
+       /************************************************************
+        * Split the argument values
+        ************************************************************/
+       if (Tcl_SplitList(interp, argv[i++], &callnargs, &callargs) != TCL_OK) {
+           return TCL_ERROR;
+       }
+
+       /************************************************************
+        * Check that the # of arguments matches
+        ************************************************************/
+       if (callnargs != qdesc->nargs) {
+           Tcl_SetResult(interp,
+              "argument list length doesn't match # of arguments for query",
+              TCL_VOLATILE);
+           if (callargs != NULL) {
+               ckfree(callargs);
+               callargs = NULL;
+           }
+           return TCL_ERROR;
+       }
+
+       /************************************************************
+        * Prepare to start a controlled return through all
+        * interpreter levels on transaction abort during the
+        * parse of the arguments
+        ************************************************************/
+       memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+       if (sigsetjmp(Warn_restart, 1) != 0) {
+           memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+           for (j = 0; j < callnargs; j++) {
+               if (qdesc->arglen[j] < 0 && 
+                       qdesc->argvalues[j] != (Datum)NULL) {
+                   pfree((char *)(qdesc->argvalues[j]));
+                   qdesc->argvalues[j] = (Datum)NULL;
+               }
+           }
+           ckfree(callargs);
+           callargs = NULL;
+           pltcl_restart_in_progress = 1;
+           Tcl_SetResult(interp, "Transaction abort", TCL_VOLATILE);
+           return TCL_ERROR;
+       }
+
+       /************************************************************
+        * Setup the value array for the SPI_execp() using
+        * the type specific input functions
+        ************************************************************/
+       for (j = 0; j < callnargs; j++) {
+           qdesc->argvalues[j] = (Datum)(*fmgr_faddr(&qdesc->arginfuncs[j]))
+                                               (callargs[j],
+                                                qdesc->argtypelems[j],
+                                                qdesc->arglen[j]);
+       }
+
+       /************************************************************
+        * Free the splitted argument value list
+        ************************************************************/
+       memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       ckfree(callargs);
+       callargs = NULL;
+    } else {
+        callnargs = 0;
+    }
+
+    /************************************************************
+     * Remember the index of the last processed call
+     * argument - a loop body for SELECT might follow
+     ************************************************************/
+    loop_body = i;
+
+    /************************************************************
+     * Prepare to start a controlled return through all
+     * interpreter levels on transaction abort
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       for (j = 0; j < callnargs; j++) {
+           if (qdesc->arglen[j] < 0 && qdesc->argvalues[j] != (Datum)NULL) {
+               pfree((char *)(qdesc->argvalues[j]));
+               qdesc->argvalues[j] = (Datum)NULL;
+           }
+       }
+       pltcl_restart_in_progress = 1;
+       Tcl_SetResult(interp, "Transaction abort", TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Execute the plan
+     ************************************************************/
+    spi_rc = SPI_execp(qdesc->plan, qdesc->argvalues, nulls, count);
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+
+    /************************************************************
+     * For varlena data types, free the argument values
+     ************************************************************/
+    for (j = 0; j < callnargs; j++) {
+        if (qdesc->arglen[j] < 0 && qdesc->argvalues[j] != (Datum)NULL) {
+           pfree((char *)(qdesc->argvalues[j]));
+           qdesc->argvalues[j] = (Datum)NULL;
+       }
+    }
+
+    /************************************************************
+     * Check the return code from SPI_execp()
+     ************************************************************/
+    switch (spi_rc) {
+        case SPI_OK_UTILITY:
+           Tcl_SetResult(interp, "0", TCL_VOLATILE);
+           return TCL_OK;
+
+        case SPI_OK_SELINTO:
+        case SPI_OK_INSERT:
+        case SPI_OK_DELETE:
+        case SPI_OK_UPDATE:
+           sprintf(buf, "%d", SPI_processed);
+           Tcl_SetResult(interp, buf, TCL_VOLATILE);
+           return TCL_OK;
+
+        case SPI_OK_SELECT:
+           break;
+
+        case SPI_ERROR_ARGUMENT:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_ARGUMENT", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_UNCONNECTED:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_UNCONNECTED", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_COPY:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_COPY", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_CURSOR:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_CURSOR", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_TRANSACTION:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_TRANSACTION", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        case SPI_ERROR_OPUNKNOWN:
+           Tcl_SetResult(interp, 
+               "pltcl: SPI_exec() failed - SPI_ERROR_OPUNKNOWN", 
+               TCL_VOLATILE);
+           return TCL_ERROR;
+
+        default:
+           sprintf(buf, "%d", spi_rc);
+           Tcl_AppendResult(interp, "pltcl: SPI_exec() failed - ",
+               "unknown RC ", buf, NULL);
+            return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Only SELECT queries fall through to here - remember the
+     * tuples we got
+     ************************************************************/
+
+    ntuples = SPI_processed;
+    if (ntuples > 0) {
+       tuples  = SPI_tuptable->vals;
+       tupdesc = SPI_tuptable->tupdesc;
+    }
+
+    /************************************************************
+     * Prepare to start a controlled return through all
+     * interpreter levels on transaction abort during
+     * the ouput conversions of the results
+     ************************************************************/
+    memcpy(&save_restart, &Warn_restart, sizeof(save_restart));
+    if (sigsetjmp(Warn_restart, 1) != 0) {
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       pltcl_restart_in_progress = 1;
+       Tcl_SetResult(interp, "Transaction abort", TCL_VOLATILE);
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * If there is no loop body given, just set the variables
+     * from the first tuple (if any) and return the number of
+     * tuples selected
+     ************************************************************/
+    if (loop_body >= argc) {
+       if (ntuples > 0) {
+           pltcl_set_tuple_values(interp, arrayname, 0, tuples[0], tupdesc);
+       }
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+        sprintf(buf, "%d", ntuples);
+       Tcl_SetResult(interp, buf, TCL_VOLATILE);
+       return TCL_OK;
+    }
+
+    /************************************************************
+     * There is a loop body - process all tuples and evaluate
+     * the body on each
+     ************************************************************/
+    for (i = 0; i < ntuples; i++) {
+        pltcl_set_tuple_values(interp, arrayname, i, tuples[i], tupdesc);
+
+       loop_rc = Tcl_Eval(interp, argv[loop_body]);
+
+        if (loop_rc == TCL_OK) continue;
+       if (loop_rc == TCL_CONTINUE) continue;
+       if (loop_rc == TCL_RETURN) {
+           memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+           return TCL_RETURN;
+       }
+       if (loop_rc == TCL_BREAK) break;
+        memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+       return TCL_ERROR;
+    }
+
+    /************************************************************
+     * Finally return the number of tuples
+     ************************************************************/
+    memcpy(&Warn_restart, &save_restart, sizeof(Warn_restart));
+    sprintf(buf, "%d", ntuples);
+    Tcl_SetResult(interp, buf, TCL_VOLATILE);
+    return TCL_OK;
+}
+
+
+/**********************************************************************
+ * pltcl_set_tuple_values()    - Set variables for all attributes
+ *                               of a given tuple
+ **********************************************************************/
+static void pltcl_set_tuple_values(Tcl_Interp *interp, char *arrayname,
+       int tupno, HeapTuple tuple, TupleDesc tupdesc)
+{
+    int                i;
+    char       *outputstr;
+    char       buf[64];
+    Datum      attr;
+    bool       isnull;
+
+    char       *attname;
+    HeapTuple  typeTup;
+    Oid                typoutput;
+    Oid                typelem;
+
+    char       **arrptr;
+    char       **nameptr;
+    char       *nullname = NULL;
+
+    /************************************************************
+     * Prepare pointers for Tcl_SetVar2() below and in array
+     * mode set the .tupno element
+     ************************************************************/
+    if (arrayname == NULL) {
+        arrptr = &attname;
+       nameptr = &nullname;
+    } else {
+        arrptr = &arrayname;
+       nameptr = &attname;
+       sprintf(buf, "%d", tupno);
+       Tcl_SetVar2(interp, arrayname, ".tupno", buf, 0);
+    }
+
+    for (i = 0; i < tupdesc->natts; i++) {
+       /************************************************************
+        * Get the attribute name
+        ************************************************************/
+        attname = tupdesc->attrs[i]->attname.data;
+
+       /************************************************************
+        * Get the attributes value
+        ************************************************************/
+       attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);
+       
+       /************************************************************
+        * Lookup the attribute type in the syscache
+        * for the output function
+        ************************************************************/
+       typeTup = SearchSysCacheTuple(TYPOID,
+               ObjectIdGetDatum(tupdesc->attrs[i]->atttypid),
+               0, 0, 0);
+       if (!HeapTupleIsValid(typeTup)) {
+           elog(ERROR, "pltcl: Cache lookup for attribute '%s' type %ld failed",
+               attname, ObjectIdGetDatum(tupdesc->attrs[i]->atttypid));
+       }
+
+       typoutput = (Oid) (((TypeTupleForm)GETSTRUCT(typeTup))->typoutput);
+       typelem   = (Oid) (((TypeTupleForm)GETSTRUCT(typeTup))->typelem);
+
+       /************************************************************
+        * If there is a value, set the variable
+        * If not, unset it
+        *
+        * Hmmm - Null attributes will cause functions to
+        *        crash if they don't expect them - need something
+        *        smarter here.
+        ************************************************************/
+       if (!isnull && OidIsValid(typoutput)) {
+           FmgrInfo    finfo;
+
+           fmgr_info(typoutput, &finfo);
+
+           outputstr = (*fmgr_faddr(&finfo))
+                       (attr, typelem,
+                       tupdesc->attrs[i]->attlen);
+
+           Tcl_SetVar2(interp, *arrptr, *nameptr, outputstr, 0);
+           pfree(outputstr);
+       } else {
+           Tcl_UnsetVar2(interp, *arrptr, *nameptr, 0);
+       }
+    }
+}
+
+
+/**********************************************************************
+ * pltcl_build_tuple_argument()        - Build a string usable for 'array set'
+ *                               from all attributes of a given tuple
+ **********************************************************************/
+static void pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc,
+       Tcl_DString *retval)
+{
+    int                i;
+    char       *outputstr;
+    Datum      attr;
+    bool       isnull;
+
+    char       *attname;
+    HeapTuple  typeTup;
+    Oid                typoutput;
+    Oid                typelem;
+
+    for (i = 0; i < tupdesc->natts; i++) {
+       /************************************************************
+        * Get the attribute name
+        ************************************************************/
+        attname = tupdesc->attrs[i]->attname.data;
+
+       /************************************************************
+        * Get the attributes value
+        ************************************************************/
+       attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);
+       
+       /************************************************************
+        * Lookup the attribute type in the syscache
+        * for the output function
+        ************************************************************/
+       typeTup = SearchSysCacheTuple(TYPOID,
+               ObjectIdGetDatum(tupdesc->attrs[i]->atttypid),
+               0, 0, 0);
+       if (!HeapTupleIsValid(typeTup)) {
+           elog(ERROR, "pltcl: Cache lookup for attribute '%s' type %ld failed",
+               attname, ObjectIdGetDatum(tupdesc->attrs[i]->atttypid));
+       }
+
+       typoutput = (Oid) (((TypeTupleForm)GETSTRUCT(typeTup))->typoutput);
+       typelem   = (Oid) (((TypeTupleForm)GETSTRUCT(typeTup))->typelem);
+
+       /************************************************************
+        * If there is a value, append the attribute name and the
+        * value to the list
+        *
+        * Hmmm - Null attributes will cause functions to
+        *        crash if they don't expect them - need something
+        *        smarter here.
+        ************************************************************/
+       if (!isnull && OidIsValid(typoutput)) {
+           FmgrInfo    finfo;
+
+           fmgr_info(typoutput, &finfo);
+
+           outputstr = (*fmgr_faddr(&finfo))
+                       (attr, typelem,
+                       tupdesc->attrs[i]->attlen);
+
+           Tcl_DStringAppendElement(retval, attname);
+           Tcl_DStringAppendElement(retval, outputstr);
+           pfree(outputstr);
+       }
+    }
+}
+
+
diff --git a/src/pl/tcl/pltcl_guide.nr b/src/pl/tcl/pltcl_guide.nr
new file mode 100644 (file)
index 0000000..87fcc71
--- /dev/null
@@ -0,0 +1,410 @@
+.pl 27.0c
+.ll 17.0c
+.po 2.0c
+.nf
+.nh
+.de HD
+.sp 2m
+..
+.de FT
+.sp 2m
+.tl _PL/Tcl_A PostgreSQL PL_Page %
+..
+.wh 0 HD
+.wh -3 FT
+.sp 5m
+.ce 1000
+PL/Tcl
+A procedural language for the
+
+PostgreSQL
+database system
+.ce 0
+.sp 5m
+.fi
+.in +4
+PL/Tcl is a dynamic loadable extension for the PostgreSQL database system
+that enables the Tcl language to be used to create functions and
+trigger-procedures. It offers most of the capabilities a function
+writer has in the C language, except for some restrictions.
+
+The good restriction is, that everything is executed in a safe
+Tcl-interpreter. In addition to the limited command set of safe Tcl, only
+a few commands are available to access the database over SPI and to raise
+messages via elog(). There is no way to access internals of the
+database backend or gaining OS-level access under the permissions of the
+PostgreSQL user ID like in C. Thus, any unprivileged user may be
+permitted to use this language.
+
+The other, internal given, restriction is, that Tcl procedures cannot
+be used to create input-/output-functions for new data types.
+.bp
+.ti -4
+Data type conversions
+
+PostgreSQL has a rich set of builtin data types. And new data types can
+be defined. The trick is, that PostgreSQL doesn't really know much about
+the internals of a data type. It just offers a container for storing the
+values and knows some functions to call to convert between the external
+string representation and the internal container format. In addition, it
+knows which functions to call to compare containers or to do some 
+arithmetics on them for sorting, indexing and calculations.
+
+Tcl on the other hand stores all values as strings.
+
+These two different concepts meet perfectly for what we need. A PostgreSQL
+function has a return value and up to 9 arguments. The data types appear
+in the pg_type system catalog, where we find their type specific regproc's
+responsible for input-/output-conversion from/to strings.
+
+A special case are set values, which can appear as arguments to a
+function. A set value is like a structure containing all the fields
+of a table as it's elements. 
+
+C functions cannot have sets as return values. So we cannot do this in
+Tcl either.
+
+
+.ti -4
+PostgreSQL functions and Tcl procedure names
+
+In PostgreSQL, one and the same function name can be used for
+different functions as long as the number of arguments or their types
+differ. This would collide with Tcl procedure names. To offer the same
+flexibility in PL/Tcl, the internal Tcl procedure names contain the object
+ID of the procedures pg_proc row as part of their name. Thus, different
+argtype versions of the same PostgreSQL function are different for Tcl too.
+.bp
+.ti -4
+Defining PostgreSQL functions in PL/Tcl
+
+The following assumes, that the PL/Tcl language is created by the
+administrator of the database with the language name 'pltcl'. See the
+installation instructions to do that.
+
+To create a function in the PL/Tcl language, use the known syntax:
+
+.nf
+    CREATE FUNCTION funcname ([typename [...]])
+.in +4
+    RETURNS typename AS '
+.in +4
+    PL/Tcl procedure body
+.in -4
+    ' LANGUAGE 'pltcl';
+.in -4
+.fi
+
+When calling this function in a query, the arguments are given as
+variables $1 ... $n to the procedure body. So a little max function
+returning the higher of two int4 values would be created as:
+
+.nf
+    create function max (int4, int4)
+.in +4
+    returns int4 as '
+.in +4
+    if {$1 > $2} {return $1}
+    return $2
+.in -4
+    ' language 'pltcl';
+.in -4
+.fi
+
+Set arguments are given to the procedure as Tcl arrays. The element names
+in the array are the field names of the set. If a field in the actual set
+has the NULL value, it will not appear in the array! The overpaid_2 sample
+from the CREATE FUNCTION section of the manual would be defined in Tcl as
+
+.nf
+    create function overpaid_2 (EMP)
+.in +4
+    returns bool as '
+.in +4
+    if {200000.0 < $EMP(salary)} {
+.in +4
+    return 't'
+.in -4
+    }
+    if {$EMP(age) < 30 && 100000.0 < $EMP(salary)} {
+.in +4
+    return 't'
+.in -4
+    }
+    return 'f'
+.in -4
+    ' language 'pltcl';
+.in -4
+.fi
+
+Sometimes (especially when using the SPI functions described later) it
+is useful to have some global status data that is held between two
+calls to a procedure. To protect PL/Tcl procedures from side effects,
+an array is made available to each procedure via the upvar
+command. The global name of this variable is the procedures internal
+name and the local name is GD.
+.bp
+.ti -4
+Defining trigger procedures in PL/Tcl
+
+Trigger procedures are defined in PostgreSQL as functions without
+arguments and a return type of opaque. And so are they in the PL/Tcl
+language.
+
+The informations from the trigger manager are given to the procedure body
+in the following variables:
+
+.in +4
+.ti -4
+$TG_name
+.br
+The name of the trigger from the CREATE TRIGGER statement
+
+.ti -4
+$TG_relid
+.br
+The Object ID of the table that caused the trigger procedure to be
+called.
+
+.ti -4
+$TG_relatts
+.br
+A Tcl list of the tables field names prefixed with an empty list element.
+So looking up an element name in the list with the lsearch Tcl command
+returns the same positive number starting from 1 as the fields are numbered
+in the pg_attribute system catalog.
+
+.ti -4
+$TG_when
+.br
+The string BEFORE or AFTER, depending on the event of the trigger call.
+
+.ti -4
+$TG_level
+.br
+The string ROW or STATEMENT, depending on the event of the trigger call.
+
+.ti -4
+$TG_op
+.br
+The string INSERT, UPDATE or DELETE, depending on the event of the trigger 
+call.
+
+.ti -4
+$NEW
+.br
+An array containing the values of the new table row on INSERT/UPDATE
+actions, or empty on DELETE.
+
+.ti -4
+$OLD
+.br
+An array containing the values of the old table row on UPDATE/DELETE
+actions, or empty on INSERT.
+
+.ti -4
+$GD
+.br
+The global status data array as described in the functions section of this
+document.
+
+.ti -4
+$args
+.br
+A Tcl list of the arguments to the procedure as given in the
+CREATE TRIGGER statement. The arguments are also accessible as $1 ... $n
+in the procedure body.
+.bp
+.in -4
+The return value from a trigger procedure is one of the strings OK or SKIP,
+or a list as returned by the 'array get' Tcl command. If the return value
+is OK, the normal operation (INSERT/UPDATE/DELETE) that fired this trigger
+will take place. Obviously, SKIP tells the trigger manager to silently
+suppress the operation. The list from 'array get' tells PL/Tcl
+to return a modified row to the trigger manager that will be inserted instead
+of the one given in $NEW (INSERT/UPDATE only). Needless to say that all
+this is only meaningful when the trigger is BEFORE and FOR EACH ROW.
+
+Here's a little example trigger procedure that forces an integer value
+in a table to keep track of the # of updates that are performed on the
+row. For new row's inserted, the value is initialized to 0 and then
+incremented on every update operation:
+
+.nf
+.in +4
+create function trigfunc_modcount() returns opaque as '
+    switch $TG_op {
+        INSERT {
+            set NEW($1) 0
+        }
+        UPDATE {
+            set NEW($1) $OLD($1)
+            incr NEW($1)
+        }
+        default {
+            return OK
+        }
+    }
+    return [array get NEW]
+.ti -1
+ ' language 'pltcl';
+
+create table T1 (key int4, modcnt int4, desc text);
+
+create trigger trig_T1_modcount before insert or update
+    on T1 for each row execute procedure
+    trigfunc_modcount('modcnt');
+.in -4
+.fi
+.bp
+.ti -4
+PostgreSQL database access from PL/Tcl
+
+The following commands are available to access the database from
+the body of a PL/Tcl procedure:
+
+.in +4
+.ti -4
+elog level msg
+.br
+Fire a log message. Possible levels are NOTICE, WARN, ERROR, 
+FATAL, DEBUG and NOIND
+like for the elog() C function.
+
+.ti -4
+quote string
+.br
+Duplicates all occurences of single quote and backslash characters.
+It should be used when variables are used in the query string given
+to spi_exec or spi_prepare (not for the value list on spi_execp). 
+Think about a query string like
+
+.ti +4
+select '$val' as ret
+
+where the Tcl variable actually contains "doesn't". This would result
+in the final query string 
+
+.ti +4
+select 'doesn't' as ret
+
+what's wrong. It should contain
+
+.ti +4
+select 'doesn''t'
+
+and should be written as
+
+.ti +4
+select '[quote $val]' as ret
+
+to work.
+
+.ti -4
+spi_exec ?-count n? ?-array name? query ?loop-body?
+.br
+Call parser/planner/optimizer/executor for query. 
+The optional -count value tells spi_exec the maximum number of rows
+to be processed by the query.
+
+If the query is
+a SELECT statement and the optional loop-body (a body of Tcl commands
+like in a foreach statement) is given, it is evaluated for each 
+row selected and behaves like expected on continue/break. The values
+of selected fields are put into variables named as the column names. So a
+
+.ti +2
+spi_exec "select count(*) as cnt from pg_proc"
+
+will set the variable $cnt to the number of rows in the pg_proc system
+catalog. If the option -array is given, the column values are stored
+in the associative array named 'name' indexed by the column name
+instead of individual variables.
+
+.in +2
+.nf
+spi_exec -array C "select * from pg_class" {
+    elog DEBUG "have table $C(relname)"
+}
+.fi
+.in -2
+
+will print a DEBUG log message for every row of pg_class. The return value
+of spi_exec is the number of rows affected by query as found in
+the global variable SPI_processed.
+
+.ti -4
+spi_prepare query typelist
+.br
+Prepares AND SAVES a query plan for later execution. It is a bit different
+from the C level SPI_prepare in that the plan is automatically copied to the
+toplevel memory context. Thus, there is currently no way of preparing a
+plan without saving it.
+
+If the query references arguments, the type names must be given as a Tcl
+list. The return value from spi_prepare is a query ID to be used in
+subsequent calls to spi_execp. See spi_execp for a sample.
+
+.ti -4
+spi_execp ?-count n? ?-array name? ?-nulls str? queryid ?values? ?loop-body?
+
+Execute a prepared plan from spi_prepare with variable substitution. 
+The optional -count value tells spi_execp the maximum number of rows
+to be processed by the query.
+
+The optional value for -nulls is a string of spaces and 'n' characters
+telling spi_execp which of the values are NULL's. If given, it must
+have exactly the length of the number of values.
+
+The queryid is the ID returned by the spi_prepare call.
+
+If there was a typelist given to spi_prepare, a Tcl list of values of
+exactly the same length must be given to spi_execp after the query. If
+the type list on spi_prepare was empty, this argument must be omitted.
+
+If the query is a SELECT statement, the same as described for spi_exec
+happens for the loop-body and the variables for the fields selected.
+
+Here's an example for a PL/Tcl function using a prepared plan:
+
+.in +4
+.nf
+create table T1 (key int4, val text);
+
+create function T1_count(int4) returns int4 as '
+    if {![info exists GD]} {
+        # prepare the plan on the first call
+        set GD(plan) [spi_prepare \\\\
+          "select count(*) as cnt from T1 where key = \\\\$1" \\\\
+          int4]
+    }
+    spi_execp -count 1 $GD(plan) [list $1]
+    return $cnt
+.ti -1
+ ' language 'pltcl';
+.fi
+.in -4
+
+Note that each backslash that Tcl should see must be doubled in
+the query creating the function, since the PostgreSQL parser processes
+backslashes too.
+.bp
+.ti -4
+Modules and the unknown command
+
+PL/Tcl has a special support for things often used. It recognizes two
+magic tables, pltcl_modules and pltcl_modfuncs.
+If these exist, the module 'unknown' is loaded into the interpreter
+right after creation. Whenever an unknown Tcl procedure is called,
+the unknown proc is called to check if the procedure is defined in one
+of the modules. If this is true, the module is loaded on demand.
+
+See the documentation in the modules subdirectory for detailed
+information.
+
+
+
+.in -4
+Now enjoy PL/Tcl.
+
+jwieck@debis.com (Jan Wieck)