Use PL/Tcl
Overview
PL/Tcl is a loadable procedural language for the ADPG/PostgreSQL database system that allows you to use the Tcl language to write functions and procedures.
PL/Tcl offers most of the capabilities that the C language provides with few limitations and allows you to use the powerful string processing libraries that are available for Tcl.
One useful restriction is that the code runs in the security context of the Tcl interpreter. In addition to the limited set of secure Tcl commands, only a few commands are available to access the database using SPI and to issue messages via elog. PL/Tcl does not provide the ability to interact with the internal mechanism of the database server or access the OS with the rights of the PostgreSQL server process, which is possible in C functions. So, unprivileged database users can be trusted to use this language — it does not give them unlimited authority.
Another major limitation is that Tcl functions cannot be used to create input/output functions for new data types.
Sometimes it is necessary to write a Tcl function that goes beyond safe Tcl. For example, you may need a function that sends emails. To handle these cases, there is a variation of PL/Tcl called PL/TclU (untrusted Tcl). This is exactly the same language except that a full Tcl interpreter is used. PL/TclU must be installed as an untrusted procedural language so that only database superusers can create functions in it. The author of a PL/TclU function must ensure that the function cannot be used to do anything unexpected, since it can perform any operation that a user with database administrator rights can do.
To install PL/Tcl and/or PL/TclU in a particular database, use the CREATE EXTENSION command:
CREATE EXTENSION pltcl;
CREATE EXTENSION pltclu;
TIP
If the pltcl or pltclu extension is created in the template1 database used as the default template, all subsequently created databases will have this extension installed.
|
PL/Tcl functions and arguments
To create a function in the PL/Tcl language, use the standard CREATE FUNCTION command:
CREATE FUNCTION <funcname> (<argument-types>) RETURNS <return-type> AS $$
# PL/Tcl function body
$$ LANGUAGE pltcl [STRICT];
The body of the function is a part of a Tcl script. When the function is called, the argument values are passed to the Tcl script as variables named 1 … n
. The result is returned from the Tcl code with the return
statement.
The PL/TclU function has the same syntax, except that the language must be specified as pltclu
.
For example, the following function returns the greater of two integer values:
CREATE FUNCTION calculate_max(integer, integer) RETURNS integer AS $$
if {$1 > $2} {return $1}
return $2
$$ LANGUAGE pltcl STRICT;
With the STRICT
clause, if a null
value is passed, the function is not called but returns a null
result.
If STRICT
is not specified and an argument value is null
, the corresponding $n
variable will be set to an empty string.
Use the argisnull
function to determine whether a particular argument is null
. To return a null
value from a PL/Tcl function, execute return_null
. This can be done whether the function contains STRICT
or not.
It is possible to rewrite the calculate_max
function with argisnull
and return_null
to return the non-null argument if one of the arguments is null
:
CREATE FUNCTION calculate_max(integer, integer) RETURNS integer AS $$
if {[argisnull 1]} {
if {[argisnull 2]} { return_null }
return $2
}
if {[argisnull 2]} { return $1 }
if {$1 > $2} {return $1}
return $2
$$ LANGUAGE pltcl;
Composite-type arguments are passed to the function as Tcl arrays. The element names of the array are the attribute names of the composite type. If an attribute in a row has the null
value, it does not appear in the array. The following example uses an array:
CREATE TABLE book (
book_id SERIAL PRIMARY KEY,
title VARCHAR(50) NOT NULL,
author_id INT NOT NULL,
in_stock INTEGER DEFAULT 0);
INSERT INTO book(title, author_id, in_stock) VALUES
('Mrs. Dalloway',1,25),
('To the Lighthouse',1,13),
('To Kill a Mockingbird',2,0),
('The Great Gatsby',3,3),
('The Lord of the Rings',4,45);
CREATE FUNCTION is_in_stock(book) RETURNS boolean AS $$
if {$1(in_stock)> 0} {
return "t"
}
return "f"
$$ LANGUAGE pltcl;
SELECT book_id, title, is_in_stock(b.*) FROM book b;
Result:
book_id | title | is_in_stock ---------+-----------------------+------------- 1 | Mrs. Dalloway | t 2 | To the Lighthouse | t 3 | To Kill a Mockingbird | f 4 | The Great Gatsby | t 5 | The Lord of the Rings | t
PL/Tcl functions can also return composite-type results. To do this, the Tcl code must return a list of column name/value pairs matching the expected result type. Any column names omitted from the list return null
values, and an error is raised if there are unexpected column names. Example:
CREATE FUNCTION sum_prod(x int, y int, out sum int, out prod int) AS $$
return [list sum [expr {$1 + $2}] prod [expr {$1 * $2}]]
$$ LANGUAGE pltcl;
SELECT sum_prod(2,6);
Result:
sum_prod ---------- (8,12)
You can return output arguments of procedures in the same way, for example:
CREATE PROCEDURE calculate_cube(INOUT x integer, INOUT y integer) AS $$
return [list x [expr {$1 * $1 * $1}] y [expr {$2 * $2 * $2} ]]
$$ LANGUAGE pltcl;
CALL calculate_cube(2, 15);
Result:
x | y ---+------ 8 | 3375
You can use the array get
Tcl command to create the result list from an array representation:
CREATE FUNCTION arrival(book, amount int) RETURNS book AS $$
set 1(in_stock) [expr {$1(in_stock) + $2}]
return [array get 1]
$$ LANGUAGE pltcl;
SELECT arrival(book::book, 6) FROM book;
Result:
arrival ---------------------------------- (1,"Mrs. Dalloway",1,31) (2,"To the Lighthouse",1,19) (3,"To Kill a Mockingbird",2,6) (4,"The Great Gatsby",3,9) (5,"The Lord of the Rings",4,51)
PL/Tcl functions can return sets of results. To do this, the Tcl code should call return_next
once per row to return an appropriate value for a scalar type, or a list of column name/value pairs for a composite type. The following function returns a set of a scalar type:
CREATE FUNCTION incr_sequence(start_seq int, end_seq int) RETURNS SETOF int AS $$
for {set i $1} {$i <= $2} {incr i} {
return_next $i
}
$$ LANGUAGE pltcl;
SELECT incr_sequence(8, 14);
Result:
incr_sequence --------------- 8 9 10 11 12 13 14
The example below returns a set of a composite type:
CREATE FUNCTION cube_sequence(start_seq int, end_seq int) RETURNS TABLE ( x int, x_cube int) AS $$
for {set i $1} {$i <= $2} {incr i} {
return_next [list x $i x_cube [expr {$i * $i * $i}]]
}
$$ LANGUAGE pltcl;
SELECT cube_sequence(8, 14);
Result:
cube_sequence --------------- (8,512) (9,729) (10,1000) (11,1331) (12,1728) (13,2197) (14,2744)
NOTE
The argument values passed to the PL/Tcl function are the input arguments converted to text. The return and return_next commands accept any string that matches the format of the declared function result type or the specified column in the result of a composite type.
|
Global data
Sometimes it is necessary to have some global data that is stored between two function calls or shared between different functions. This is easy to do in PL/Tcl, but there are some limitations.
For security reasons, PL/Tcl runs functions called by a specific SQL role in a separate Tcl interpreter for that role. This prevents one user from accidentally or maliciously interfering with the behavior of another user’s PL/Tcl functions. Each such interpreter has its own values for any Tcl global variables. So, two PL/Tcl functions use the same global variables only if they are executed by the same SQL role. In an application that runs code under multiple SQL roles in the same session (using SECURITY DEFINER
or SET ROLE
, etc.), it may be necessary to perform additional steps to ensure that PL/Tcl functions can share data. To do this, ensure that the functions that should communicate are owned by the same user and set the SECURITY DEFINER
parameter for them. See CREATE FUNCTION.
All PL/TclU functions used in a session are executed in the same Tcl interpreter, which is different from the interpreters used for PL/Tcl functions. In this way, global data is automatically distributed among the PL/TclU functions. This is not considered a security risk because all PL/TclU functions are performed at the same trust level — the database superuser level.
To protect PL/Tcl functions from unintentionally affecting each other, each function is provided with a global array variable using the upvar
command. The global name of this variable is the internal name of the function, and the local name is GD
. It is recommended to use GD
for persistent private function data. Use regular Tcl global variables only for values that will be shared among multiple functions. Note that GD
arrays are only global within a particular interpreter, so they do not bypass the security restrictions mentioned above.
You can find an example of using GD
in the description of the spi_execp function.
Database access from PL/Tcl
Below are the commands used for accessing a database from the body of a PL/Tcl function. In this section, according to the Tcl convention, question marks (?
) replace the brackets ([]
) typically used to indicate optional elements in a syntax description.
spi_exec
The spi_exec
command executes an SQL command given as a string. An error in the command causes an error to be raised. The return value of spi_exec
is the number of rows processed (selected, inserted, updated, or deleted) by the command, or zero if the command is a utility statement. Additionally, if the command is a SELECT
statement, the values of the selected columns are placed in Tcl variables, as described below.
spi_exec ?-count <n>? ?-array <name>? <command> ?<loop-body>?
Where:
-
command
— SQL command. -
count <n>
— an optional parameter indicating that execution ofspi_exec
should stop immediately aftern
rows are retrieved, as if the query included aLIMIT
clause. Ifn
is zero, the query is executed until completion, the same as if-count
is not specified. -
array <name>
— an optional parameter, used if the SQL command is aSELECT
statement. When specified, the column values are stored in a named associative array, and the column names are used as array indexes. The current row number in the result (starting from zero) is stored in an array element namedtupno
, unless that name is used as a column name. Ifarray
is not specified, the values of the result columns are placed in Tcl variables named after the column names. -
loop-body
— an optional parameter that specifies a Tcl script fragment that is executed once for each row of the query result. If the command is aSELECT
statement and noloop-body
is specified, only the first row of results is stored in Tcl variables or array, the remaining rows are ignored. Storing does not occur if the query returns no rows. If the SQL command is notSELECT
,loop-body
is ignored.
The following function sets the value of the count_books
variable to the number of rows in the books
table:
CREATE OR REPLACE FUNCTION book_count() RETURNS int AS $$
spi_exec "SELECT count(*) AS count_books FROM book"
return $count_books
$$ LANGUAGE pltcl;
SELECT book_count();
Result:
book_count ------------ 5
The function below returns a string containing books of a specific author. The column values of the current row are stored in the res
array before each iteration:
CREATE OR REPLACE FUNCTION books_by_author() RETURNS text AS $$
set book_result " Virginia Woolf: "
set counter 1
spi_exec -array res "SELECT * FROM book WHERE author_id=1" {
if {$counter > 1} { append book_result ", " }
append book_result $res(title)
incr counter
}
return $book_result
$$ LANGUAGE pltcl;
SELECT books_by_author();
Result:
books_by_author --------------------------------------------------- Virginia Woolf: Mrs. Dalloway, To the Lighthouse
spi_prepare
The spi_prepare
function prepares and saves a query plan for later execution. The saved plan is available during the current session.
spi_prepare <query> <typelist>
Where:
-
query
— SQL query; -
typelist
— list of query parameter types.
The query string processed by spi_prepare
supports parameters. You can use numbers $1 … $n
to refer to parameters in the query string. The parameter types must be given as a Tcl list. For example, [ list int4 int4 ]
. Add an empty list as typelist
if no parameters are used.
The spi_prepare
function returns a query ID to be used in subsequent calls of the spi_execp
function described below.
spi_execp
The spi_execp
function executes a query previously prepared with spi_prepare.
spi_execp ?-count <n>? ?-array <name>? ?-nulls <string>? <queryid> ?<value-list>? ?<loop-body>?
Where:
-
queryid
— ID returned byspi_prepare
. -
count <n>
— an optional parameter indicating that execution ofspi_execp
should stop immediately aftern
rows are retrieved. Ifn
is zero, the query is executed until completion, the same as if-count
is not specified. -
array <name>
— an optional parameter, used if the SQL command is aSELECT
statement. When specified, the column values are stored in a named associative array, and the column names are used as array indexes. The current row number in the result (starting from zero) is stored in an array element namedtupno
, unless that name is used as a column name. Ifarray
is not specified, the values of the result columns are placed in Tcl variables named after the column names. -
nulls <string>
— an optional parameter indicating which parameters have null values.string
is a sequence of characters that includes spaces andn
characters for parameters that have null values. The string must have the same length as thevalue-list
. Ifnulls
is not specified, all the parameter values are assumed to be non-null. -
value-list
— a Tcl list of actual values for query parameters.value-list
must be the same length as the parameter type list previously given tospi_prepare
. Omitvalue-list
if the query has no parameters. -
loop-body
— an optional parameter that specifies a Tcl script fragment that is executed once for each row of the query result. If the command is aSELECT
statement and noloop-body
is specified, only the first row of results is stored in Tcl variables or array, the remaining rows are ignored. Storing does not occur if the query returns no rows. If the SQL command is notSELECT
,loop-body
is ignored.
Except for the way in which the query and its parameters are specified, spi_execp
works like spi_exec
.
The following function calculates the number of rows where the aid
column value is greater than or equal to the first parameter of the rows_count
function but less than or equal to the second parameter. The example operates with the pgbench_accounts
table created using pgbench:
CREATE FUNCTION rows_count(integer, integer) RETURNS integer AS $$
if {![ info exists GD(plan) ]} {
# prepare a plan on the first call
set GD(plan) [ spi_prepare "SELECT count(*) AS row_number FROM pgbench_accounts WHERE aid >= \$1 AND aid <= \$2" [ list int4 int4 ] ]
}
spi_execp -count 1 $GD(plan) [ list $1 $2 ]
return $row_number
$$ LANGUAGE pltcl;
SELECT rows_count(28,3678);
Result:
rows_count ------------ 3651
Note that backslashes inside the query string given to spi_prepare
ensure that the $n
markers will be passed as is and are not replaced by the Tcl variable substitution.
quote
The quote function doubles all occurrences of a single quote and backslash characters in the given string. This can be used to protect strings that will be inserted into SQL commands passed to spi_exec
or spi_prepare
.
quote <string>
Example:
"SELECT '[ quote $val ]' AS ret"
As a result, for example, SELECT 'It doesn’t true' AS ret
will be transformed into SELECT 'It doesn''t true' AS ret
.
subtransaction
The subtransaction
function executes the given command within an SQL subtransaction.
subtransaction <command>
Where command
is a Tcl script.
If the script returns an error, the entire subtransaction is rolled back before returning the error out to the surrounding code.
Example:
CREATE TABLE accounts(
acc_number integer PRIMARY KEY,
balance numeric
);
CREATE TABLE transfer_logs(
log_id SERIAL PRIMARY KEY,
result text
);
INSERT INTO accounts VALUES
(1234,1000.00),
(4321,2000.00);
CREATE FUNCTION account_transfers() RETURNS void AS $$
if [catch {
spi_exec "UPDATE accounts SET balance = balance - 1000 WHERE acc_number = '1234'"
spi_exec "UPDATE accounts SET balance = balance + 1000 WHERE acc_number = '4321'"
} errormsg] {
set result [format "The transfer was canceled: %s" $errormsg]
} else {
set result "The transfer is successful"
}
spi_exec "INSERT INTO transfer_logs (result) VALUES ('[quote $result]')"
$$ LANGUAGE pltcl;
SELECT account_transfers();
SELECT * FROM accounts;
Result:
acc_number | balance ------------+--------- 1234 | 0.00 4321 | 3000.00
For more information, see Explicit Subtransactions in PL/Tcl.
elog
The elog
function emits a log or error message.
elog <level> <msg>
Where:
-
level
— message priority level. Possible levels areDEBUG
,LOG
,INFO
,NOTICE
,WARNING
,ERROR
, andFATAL
. -
msg
— message text.
Example:
elog DEBUG "The database contains table $table_name"
The ERROR
level raises an error condition. If this is not trapped by the Tcl code, the error propagates out to the calling query, causing the current transaction or subtransaction to be aborted. This is the same as the Tcl error command. FATAL
aborts the transaction and causes the current session to shut down. There is probably no good reason to use this error level in PL/Tcl functions, but it is provided for completeness. The other levels only generate messages of different priority levels.
Whether messages of a particular priority are reported to the client, written to the server log, or both is controlled by the log_min_messages and client_min_messages configuration parameters. You can specify these settings on the Primary configuration tab of the ADPG service. Expand the ADPG configurations node, open the postgresql.conf field, find and change the log_min_messages
parameter. To set client_min_messages
, add this parameter to the postgresql.conf field (see Configuration parameters). For more information on error handling, refer to Error handling in PL/Tcl.