Version 23 of pltcl

Updated 2011-03-16 18:09:13 by Str

pltcl is a PostgreSQL module which embeds a safe Tcl interpreter inside the running database. You can write triggers, stored procedures, perform queries and so on.

There is a related pltclu unsafe module which allows access to the file system, sockets etc.

Here's an example:

  CREATE FUNCTION tcl_max(integer, integer) RETURNS integer AS $$
      if {$1 > $2} {
          return $1
      }
      return $2
  $$ LANGUAGE pltcl STRICT;

Everything between the $$ quotes is Tcl. Arguments are positional and are 1-indexed.

Documentation:


Exploring the postgres tcl environment

I would like to add some information that is missing from the Postgresql documentation.

So we start with a working postgres server, are working interactively in psql logged into database projectone, and write the first script: (terminal protocol)

projectone=# create function test1() returns text as $$
projectone$#  return "okay, it works."
projectone$# $$ language pltcl;
ERROR:  language "pltcl" does not exist
TIP:  Use CREATE LANGUAGE to load the language into the database.

The message says it all. See the original pg documentation. But it is very easy, if you have installed the extension ( postgresql-pltcl in openSuse):

projectone=# create language pltcl;
CREATE LANGUAGE
projectone=# create language pltclu;
CREATE LANGUAGE

The pltclu has OS access, for example sending mail, managing files etc, while all work inside the database can be done with pltcl.

projectone=# create function test1() returns text as $$
                return "okay, it works."
             $$ language pltcl;
CREATE FUNCTION
projectone=# select test1() as helloworld;
   helloworld
-----------------
 okay, it works.
(1 Zeile)

Fine. But where is my proc? I provoke a script error:

projectone=# create or replace function test1() returns text as $$
             whatisthis
             $$ language pltcl;
CREATE FUNCTION
projectone=# select test1() as helloworld;
ERROR:  invalid command name "whatisthis"
KONTEXT:  invalid command name "whatisthis"
    while executing
"whatisthis"
    (procedure "__PLTcl_proc_91665" line 3)
    invoked from within
"__PLTcl_proc_91665"
in PL/Tcl function "test1"
projectone=#

Just fine, it says (procedure "__PLTcl_proc_91665" line 3), so you see the procedures will get an internal name. Let's check the system tables:

projectone=# \x
Erweiterte Anzeige ist an.
projectone=# select * from pg_proc where oid=91665;
-[ RECORD 1 ]---+------------
proname         | test1
pronamespace    | 2200
proowner        | 16384
prolang         | 91662
<snip>
proargmodes     |
proargnames     |
proargdefaults  |
prosrc          |
                :  whatisthis
                :
probin          |
<snip>

There is your tcl proc, with the name in column proname, all the source in column prosrc.

How do I call my own proc?

I would never call the procedure directly, because it is set up to be called by a call handler. I do it like that - but first we have to fix the test1 proc:

projectone=# create or replace function test1() returns text as $$
           return "okay, just looking around."
           $$ language pltcl;
CREATE FUNCTION
projectone=# create or replace function test2(text) returns text as $$
             set parameter $1
             spi_exec "select test1() as hello"
             return "you said: $parameter, and test1 said $hello"
$$ language pltcl;
CREATE FUNCTION
projectone=# select test2('Does it work?');
                               test2
--------------------------------------------------------------------
 you said: Does it work?, and test1 said okay, just looking around.
(1 Zeile)

This example shows: how to use a parameter in your proc, how to execute sql, and how to use the result.

Now let's explore the environment: (do not get confused by the different script delimiter, read the original doc)

projectone=# create or replace function eval_me(text) returns text as $_$
      return "[eval $1]"
      $_$ language pltcl;
CREATE FUNCTION
projectone=# select eval_me('info proc') as procedurelist;
                      procedurelist
----------------------------------------------------------
 __PLTcl_proc_91665 __PLTcl_proc_91666 __PLTcl_proc_91667
(1 Zeile)

This is the tool to further explore the pg environment.

You see, your procedures are there in the interpreter. They are valid for this database connection, and are defined after the first sql call, so subsequent calls can use the compiled proc.

Which variables do we have? See:

projectone=# select eval_me('info vars') as variables;
 variables
-----------
 1 GD
(1 Zeile)

We have variables 1 (the first argument), and GD which is mentioned in the docs as a global variable where you store your prepared sql statements. Do not do it like this, see later.

Can we use global variables? Check it out using the namespace delimiter:

projectone=# select eval_me('set ::mysession "it is me, nessuno."') as oops;
        oops
--------------------
 it is me, nessuno.
(1 Zeile)

projectone=# select eval_me('info vars ::*') as variables;
                                              variables
-----------------------------------------------------------------------------------------------------
 ::tcl_interactive ::tcl_version ::errorCode ::mysession ::tcl_patchLevel ::errorInfo ::tcl_platform
(1 Zeile)

projectone=# select eval_me('set ::mysession') as variables;
     variables
--------------------
 it is me, nessuno.
(1 Zeile)

So there it is. And there are the error variables nobody could tell me about: ::errorCode and ::errorInfo reflect the sql state.

But mind you: one interpreter per db connection, an one for pltcl, another for pltclu.

How to signal errors to the calling program

The elog command is for this purpose (see pg docs). However the reaction to error conditions is configurable. Here is a little function to do the tests - we can raise any condition like WARNING or ERROR by function call:

projectone=# create or replace function testelog(text, text)  returns text as $_$
          elog $1 $2
         return "testing condition $1"
         $_$ language pltcl;
CREATE FUNCTION
projectone=# select testelog('ERROR', 'do not do that');
ERROR:  do not do that
KONTEXT:  do not do that
    while executing
"elog $1 $2"
    (procedure "__PLTcl_proc_91668" line 3)
    invoked from within
"__PLTcl_proc_91668 ERROR {do not do that}"
in PL/Tcl function "testelog"
projectone=#

More important than doing it like this is to test it with your client application to see how you get the messages.

Here an example in PHP running interactively:

php >  $res = $DBH->query("select testelog('ERROR','thats a serious error')");
php > if ($res) print_r($res->fetchAll()); else echo "Statement empty";
Statement empty
php > print_r($DBH->errorInfo());
Array
(
    [0] => XX000
    [1] => 7
    [2] => ERROR:  thats a serious error
CONTEXT:  thats a serious error
    while executing
"elog $lev $msg"
    (procedure "__PLTcl_proc_65287" line 5)
    invoked from within
"__PLTcl_proc_65287 ERROR {thats a serious error}"
in PL/Tcl function "testelog"
)

How to query your tables directly

See above "call my own proc" for an example of spi_exec without a loop, and without an array. I think the array option is useful, so we test it here:

First we need a table with entries:

projectone=# create table table1 (id integer, name text);                     
CREATE TABLE                                                                  
projectone=# insert into table1 values (1,'Peter'),(2,'Paul'),(null,'Evil');
INSERT 0 3

Here is a code only test procedure, that will print out the array variable ARR.

The loop code is adapted from parray found in every tcl distro. The rest is just spi_exec -array ARR "select ...":

create or replace function testquery1(text) returns text as $$
    set infostring ""
    spi_exec -array ARR $1 {
        append infostring "enter Loop: --------------------\n"
        set a "ARR"
        set maxl 0
        set names [lsort [array names ARR ]]
        foreach name $names {
            if {[string length $name] > $maxl} {
                set maxl [string length $name]
            }
        }
        set maxl [expr {$maxl + [string length $a] + 2}]
        foreach name $names {
            set nameString [format %s(%s) $a $name]
            append infostring "[format "%-*s = %s" $maxl $nameString $ARR($name)]\n"
        }
    }
    return $infostring
$$ language pltcl;

See what is defined when we execute this query - the .tupno saves you from managing counters:

projectone=# select testquery1('select * from table1');
            testquery1
----------------------------------
 enter Loop: --------------------
 ARR(.tupno) = 0
 ARR(id)     = 1
 ARR(name)   = Peter
 enter Loop: --------------------
 ARR(.tupno) = 1
 ARR(id)     = 2
 ARR(name)   = Paul
 enter Loop: --------------------
 ARR(.tupno) = 2
 ARR(name)   = Evil

(1 Zeile)

Maybe more important to note is what is not defined: ARR(id) in the third iteration is not defined!

Note: A null result from an sql query will lead to an undefined variable in pltcl!

Would be nice if this was a configurable behavior. It is not, so you have to:

  • be sure not to have null result values
  • or test every result var with info exists before using it

As expected the loop is not executed if there are no result tuples:

projectone=# select testquery1('select * from table1 where id>100');
 testquery1
------------

(1 Zeile)

How to return tables or sets from pltcl

str: Impossible. If you can do it please tell me (Name misterherr Mailserver freenet.de).

You could work around by using temp tables that your function fills, or return strings that can be split easily.

How to use your tcl utility procs

You must load your procs in the interpreter. Since there is no file system access in pltcl the source must be in the database. No problem.

As example we use the print array function and tickle it a bit to return the string instead of printing it: (code only)

create or replace function tclutil() returns text as $$

    proc rarray {a {pattern *}} {
        # RCS: @(#) $Id: parray.tcl,v 1.4 2005/06/03 10:02:23 dkf Exp $
        # tickled 20110316-1615-strobel
        upvar 1 $a array
        if {![array exists array]} {
        error "\"$a\" isn't an array"
        }
        set returnthis ""
        set maxl 0
        set names [lsort [array names array $pattern]]
        foreach name $names {
        if {[string length $name] > $maxl} {
            set maxl [string length $name]
        }
        }
        set maxl [expr {$maxl + [string length $a] + 2}]
        foreach name $names {
            set nameString [format %s(%s) $a $name]
            append returnthis "[format "%-*s = %s" $maxl $nameString $array($name)]\n"
        }
        return $returnthis
    }

    set ::tclutilflag true
    return "ok"
$$ language pltcl;

Next we change testquery1 (save as testquery2) to use the new proc:

create or replace function testquery2(text) returns text as $$
    set infostring ""
    if {![info exists ::tclutilflag]} {
        spi_exec "select tclutil() as temp"
        # testing:
        append infostring "Functions loaded $temp.\n"
    }
    spi_exec -array ARR $1 {
        append infostring "enter Loop: --------------------\n"
        append infostring [rarray ARR]
    }
    return $infostring
$$ language pltcl;

That's all. We test the tclutil flag to see if we have to load the functions, that can be done by one spi_exec.

projectone=# select testquery2('select * from table1');
            testquery2
----------------------------------
 Functions loaded ok.
 enter Loop: --------------------
 ARR(.tupno) = 0
 ARR(id)     = 1
 ARR(name)   = Peter
 enter Loop: ---------------
 <snip>

On the second run the functions are there already, just check it with eval_me('info var ::*') and eval_me('info proc')

Just add any helper proc you need to tclutil and be sure to execute it once per session.

See if pltclu is working

My favorite test tool can be used here, in the untrusted version, then immediately follows one test:

projectone=# create or replace function eval_meu(text) returns text as $_$
           return "[eval $1]"
           $_$ language pltclu;
CREATE FUNCTION
projectone=# select eval_meu('exec ls -l /usr/share');
                                                         eval_meu
---------------------------------------------------------------------------------------------
 total 556
 drwxr-xr-x   2 root root  4096 Feb 15 17:11 aclocal
 drwxr-xr-x   2 root root  4096 Jul 19  2010 aclocal-1.11
 drwxr-xr-x   2 root root  4096 Jul 12  2010 adduser
 drwxr-xr-x   6 root root  4096 Jul 12  2010 apache2
 drwxr-xr-x   2 root root  4096 Jul 12  2010 applications
 drwxr-xr-x   6 root root  4096 Jul 12  2010 apport
 <snip>

Okay, the whole system is open to SQL now. If this is what you want...

Note: When accessing the OS in pltclu you are user postgres.

Example of a recursive procedure call

We will use hierarchical data in a table, the names are inspired by a menu tree.

After the first tests above you should know how it works, so I only show the code parts.

First create the test data by pasting these statements into psql:

create table table2 (id integer primary key, name text, parent integer);
insert into table2  values (1,'View',null),(2,'Insert',null),(3,'Extras',null);
insert into table2  values (4,'Zoom',1),(5,'Changes',1),(6,'Select all',1);
insert into table2  values (7,'bigger',4),(8,'smaller',4);
insert into table2  values (9,'view',5),(10,'record',5),(11,'accept',5);
insert into table2  values (12,'picture',2);
insert into table2  values (13,'from file',12),(14,'scan',12);
insert into table2  values (15,'file select',13);
insert into table2  values (16,'choose source',14),(17,'request',14);

Now paste the following proc into your tclutil function:

    proc treeup {id ilist} {
        set separator " >> "
        if {[lsearch $ilist $id] > -1} {
            return " --LOOP!"
        }
        lappend ilist $id
        set preparemarker ""
        if {![info exists ::PREP(treeup)]} {
            set ::PREP(treeup) [spi_prepare "SELECT parent, name from table2 where id = \$1 " [ list integer] ]
            # testing:
            set preparemarker " !PREPARED! "
        }
        spi_execp $::PREP(treeup) [list $id] {
            if {[info exists parent]} {
                return "$preparemarker[treeup $parent $ilist]$separator$name"
            } else {
                return "$preparemarker$name"
            }
        }
    }

Code-walk: The proc gets an index and a list, if this index is found in the list we have a loop (entry already visited).

Now we add the index to the list. We prepare the sql statement if necessary, note the usage of the global variable ::PREP. Then we check if the current entry has a parent, if yes we visit the parent doing a recursive call, else we just return the name of the current entry.

Next we write the function we want to call by sql:

create or replace function treeup(integer) returns text as $_$
    # arg 1 ist id
    if {![info exists ::tclutilflag]} {
        spi_exec "select tclutil() as temp"
    }
    if {[argisnull 1]} {
        return ""
    } else {
        return [treeup $1 [list  ]]
    }
$_$ language pltcl;

Be sure to "create or replace" the tclutil function. Now since prepared statements, functions and procs are stored in your session, you should care about restarting psql. Failing to do so will make you test the old code.

We test the new function:

projectone=# select id, name, treeup(id) as path from table2 order by path;
 id |     name      |                     path
----+---------------+-----------------------------------------------
  3 | Extras        | Extras
  2 | Insert        | Insert
 12 | picture       | Insert >> picture
 13 | from file     | Insert >> picture >> from file
 15 | file select   | Insert >> picture >> from file >> file select
 14 | scan          | Insert >> picture >> scan
 16 | choose source | Insert >> picture >> scan >> choose source
 17 | request       | Insert >> picture >> scan >> request
  1 | View          |  !PREPARED! View
  5 | Changes       | View >> Changes
 11 | accept        | View >> Changes >> accept
 10 | record        | View >> Changes >> record
  9 | view          | View >> Changes >> view
  6 | Select all    | View >> Select all
  4 | Zoom          | View >> Zoom
  7 | bigger        | View >> Zoom >> bigger
  8 | smaller       | View >> Zoom >> smaller
(17 Zeilen)

Now the sql statement is prepared only once per session, automatically, and reused on every tcl procedure call. The parameter for the where-condition is inserted perfectly.

More tests: create a circular reference in the test data and see if the proc takes care.


See also

  • pgtcl for access to the database from a stand-alone script.