42.3. PL/Tcl
PL/Tcl 是一个可装载的 Postgres 数据库系统过程语言,它使 Tcl 语言可以用来创建函数和触发器过程。
这个软件包最初由 Jan Wieck 编写。
42.3.1. 概述
PL/Tcl 提供了函数编写者在 C 语言中所具备的大部分能力,但有一些限制。
好的限制是所有内容都在一个安全的 Tcl 解释器中执行。除了安全 Tcl 的有限命令集之外,只有少数命令可用于经 SPI 访问数据库以及通过 elog() 发出消息。没有办法像 C 语言那样访问数据库后端的内部,也无法获得 Postgres 用户 ID 权限下的 OS 级访问。因此,可以允许任何非特权数据库用户使用这一语言。
另一项内在的限制是,Tcl 过程不能用来为新数据类型创建输入/输出函数。
如果在安装过程的配置步骤中指定了 Tcl/Tk 支持,PL/Tcl 调用处理器的共享对象就会自动构建并安装到 Postgres 库目录中。
42.3.2. 描述
42.3.2.1. Postgres 函数与 Tcl 过程名
在 Postgres 中,只要参数个数或类型不同,同一个函数名就可以用于不同的函数。这与 Tcl 过程名会冲突。为了让 PL/Tcl 提供同样的灵活性,内部 Tcl 过程名中把过程 pg_proc 行的对象 ID 作为名称的一部分。因此,同一个 Postgres 函数的不同参数类型版本对 Tcl 来说也是不同的。
42.3.2.2. 在 PL/Tcl 中定义函数
要用 PL/Tcl 语言创建函数,使用已知的语法
CREATE FUNCTIONfuncname(argument-types) RETURNSreturntypeAS ' # PL/Tcl function body ' LANGUAGE 'pltcl';
在查询中调用这一函数时,参数会以变量 $1 ... $n 的形式传给 Tcl 过程体。因此一个返回两个 int4 值中较大者的小小 max 函数可以这样创建:
CREATE FUNCTION tcl_max (int4, int4) RETURNS int4 AS '
if {$1 > $2} {return $1}
return $2
' LANGUAGE 'pltcl';
复合类型的参数会以 Tcl 数组的形式传给过程。数组中的元素名是复合类型的属性名。如果实际行中的某个属性为 NULL 值,它不会出现在数组中!下面是一个用 PL/Tcl 定义 overpaid_2 函数(见较早的 Postgres 文档)的例子
CREATE FUNCTION overpaid_2 (EMP) RETURNS bool AS '
if {200000.0 < $1(salary)} {
return "t"
}
if {$1(age) < 30 && 100000.0 < $1(salary)} {
return "t"
}
return "f"
' LANGUAGE 'pltcl';
42.3.2.3. PL/Tcl 中的全局数据
有时(尤其是使用后文描述的 SPI 函数时),需要在对过程的两次调用之间保存一些全局状态数据。同一后端中执行的所有 PL/Tcl 过程共享同一个安全 Tcl 解释器。为帮助保护 PL/Tcl 过程免受副作用影响,每个过程都可以通过 upvar 命令访问一个数组。这个变量的全局名是过程的内部名,局部名是 GD。
42.3.2.4. PL/Tcl 中的触发器过程
在 Postgres 中,触发器过程定义为没有参数、返回类型为 opaque 的函数。在 PL/Tcl 语言中也是如此。
来自触发器管理器的信息会通过下列变量传给过程体:
$TG_name来自 CREATE TRIGGER 语句的触发器名称。
$TG_relid导致触发器过程被调用的表的对象 ID。
$TG_relatts表字段名组成的 Tcl 列表,前面带有一个空列表元素。因此用 lsearch Tcl 命令在列表中查找元素名时,返回的正数编号与字段在 pg_attribute 系统目录中的编号相同(都从 1 开始)。
$TG_when按触发器调用的事件为 BEFORE 或 AFTER 的字符串。
$TG_level按触发器调用的事件为 ROW 或 STATEMENT 的字符串。
$TG_op按触发器调用的事件为 INSERT、UPDATE 或 DELETE 的字符串。
$NEW在 INSERT/UPDATE 操作中包含新表行值的数组,DELETE 时为空。
$OLD在 UPDATE/DELETE 操作中包含旧表行值的数组,INSERT 时为空。
$GD上面描述的全局状态数据数组。
$args按CREATE TRIGGER 语句给出的传给过程的参数组成的 Tcl 列表。这些参数在过程体中也可以用 $1 ... $n 访问。
触发器过程的返回值是字符串 OK 或 SKIP 之一,或者是 'array get' Tcl 命令返回的列表。如果返回值是 OK,触发本次触发的正常操作(INSERT/UPDATE/DELETE)将会进行。显然,SKIP 告诉触发器管理器悄悄地抑制该操作。'array get' 的列表告诉 PL/Tcl 向触发器管理器返回一个修改过的行,用它代替 $NEW 中给出的行进行插入(仅限 INSERT/UPDATE)。不用说,所有这些只有在触发器为 BEFORE 且 FOR EACH ROW 时才有意义。
下面是一个小示例触发器过程,它强制表中的一个整数值跟踪对该行执行的更新次数。对于新插入的行,该值初始化为 0,然后每次更新操作时递增:
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]
' LANGUAGE 'pltcl';
CREATE TABLE mytab (num int4, modcnt int4, desc text);
CREATE TRIGGER trig_mytab_modcount BEFORE INSERT OR UPDATE ON mytab
FOR EACH ROW EXECUTE PROCEDURE trigfunc_modcount('modcnt');
42.3.2.5. 从 PL/Tcl 访问数据库
在 PL/Tcl 过程体中可以使用下列命令来访问数据库:
- elog
levelmsg 发出一条日志消息。可能的级别有 NOTICE、WARN、ERROR、FATAL、DEBUG 和 NOIND,与 elog() C 函数的相同。
- quote
string 复制所有单引号和反斜杠字符。在给 spi_exec 或 spi_prepare 的查询字符串中使用变量时应该使用它(不用于 spi_execp 的值列表)。想一想这样的查询字符串:
"SELECT '$val' AS ret"其中 Tcl 变量 val 实际包含 "doesn't"。这会得到最终的查询字符串
"SELECT 'doesn't' AS ret"它会在 spi_exec 或 spi_prepare 期间导致解析错误。它应该包含
"SELECT 'doesn''t' AS ret"并且必须写成
"SELECT '[ quote $val ]' AS ret"- spi_exec ?-count
n? ?-arrayname?query?loop-body? 为查询调用解析器/规划器/优化器/执行器。可选的 -count 值告诉 spi_exec 查询要处理的最大行数。
如果查询是 SELECT 语句并且给出了可选的循环体(一组 Tcl 命令,就像在 foreach 语句中那样),则对选出的每一行求值,并且在 continue/break 上的行为与预期一致。选出字段的值被放入以列名命名的变量中。因此
spi_exec "SELECT count(*) AS cnt FROM pg_proc"会把变量 $cnt 设置为 pg_proc 系统目录中的行数。如果给出了 -array 选项,列值将存入名为 'name' 的关联数组并以列名为索引,而不是各个单独的变量。
spi_exec -array C "SELECT * FROM pg_class" { elog DEBUG "have table $C(relname)" }会对 pg_class 的每一行打印一条 DEBUG 日志消息。spi_exec 的返回值是查询影响的行数,与全局变量 SPI_processed 中的值相同。
- spi_prepare
querytypelist 预备并保存一个查询计划供以后执行。它与 C 层的 SPI_prepare 稍有不同:计划会被自动复制到顶层内存上下文。因此,目前没有办法预备计划而不保存它。
如果查询引用了参数,类型名必须以 Tcl 列表的形式给出。spi_prepare 的返回值是一个查询 ID,用于后续对 spi_execp 的调用。示例见 spi_execp。
- spi_exec ?-count
n? ?-arrayname? ?-nullsstr?query?valuelist? ?loop-body? 执行 spi_prepare 预备的计划并做变量替换。可选的 -count 值告诉 spi_execp 查询要处理的最大行数。
-nulls 的可选值是由空格和 'n' 字符组成的字符串,告诉 spi_execp 哪些值是 NULL。如果给出,它的长度必须恰好等于值的个数。
queryid 是 spi_prepare 调用返回的 ID。
如果 spi_prepare 给出了类型列表,则必须在查询之后给 spi_execp 一个长度恰好相同的 Tcl 值列表。如果 spi_prepare 的类型列表为空,则必须省略这个参数。
如果查询是 SELECT 语句,循环体和选出字段的变量的处理方式与 spi_exec 一节所述相同。
下面是一个使用预备计划的 PL/Tcl 函数示例:
CREATE FUNCTION t1_count(int4, int4) RETURNS int4 AS ' if {![ info exists GD(plan) ]} { # prepare the saved plan on the first call set GD(plan) [ spi_prepare \\ "SELECT count(*) AS cnt FROM t1 WHERE num >= \\$1 AND num <= \\$2" \\ int4 ] } spi_execp -count 1 $GD(plan) [ list $1 $2 ] return $cnt ' LANGUAGE 'pltcl';注意,Tcl 应该看到的每个反斜杠在创建函数的查询中都必须加倍,因为主解析器在 CREATE FUNCTION 时也会处理反斜杠。在给 spi_prepare 的查询字符串内部应该使用美元符号标记参数位置,以免 $1 被第一次函数调用时给出的值替换。
- 模块和 unknown 命令
PL/Tcl 对常用的东西提供了特殊支持。它识别两个魔法表 pltcl_modules 和 pltcl_modfuncs。如果它们存在,'unknown' 模块会在解释器创建后立即装载。每当调用一个未知的 Tcl 过程时,会请求 unknown 过程检查该过程是否定义在某个模块中。如果为真,就按需装载该模块。要启用这一行为,PL/Tcl 调用处理器必须在编译时设置 -DPLTCL_UNKNOWN_SUPPORT。
在 PL/Tcl 源码的 modules 子目录中有维护这些表的支持脚本,其中包括必须最先安装的 unknown 模块的源码。