24.2. 描述 #
24.2.1. PL/Tcl 函数和参数
要用 PL/Tcl 语言创建函数,可使用标准语法
CREATE FUNCTIONfuncname(argument-types) RETURNSreturn-typeAS ' # PL/Tcl function body ' LANGUAGE 'pltcl';
PL/TclU 的写法相同,只是语言应指定为
'pltclu'。
函数体就是一段 Tcl 脚本。调用函数时,参数值以变量
$1 ... $n 传入
Tcl 脚本。结果由 Tcl 代码按通常方式用
return 语句返回。例如,一个返回两个整数值中较大值的函数可以定义为:
CREATE FUNCTION tcl_max (integer, integer) RETURNS integer AS '
if {$1 > $2} {return $1}
return $2
' LANGUAGE 'pltcl' WITH (isStrict);
注意子句 WITH (isStrict),它让我们不必考虑
NULL 输入:如果传入的是 NULL,函数根本不会被调用,而是会自动返回 NULL 结果。
在非严格函数中,如果某个参数的实际值为 NULL,对应的
$n 变量会被设置为空串。要检测某个特定参数是否为空值,可使用函数
argisnull。例如,假设我们希望 tcl_max
在一个参数为 null、另一个非 null 时返回非 null 的参数,而不是返回 NULL:
CREATE FUNCTION tcl_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';
如上所示,要从 PL/Tcl 函数返回空值,请执行return_null。无论函数是否为严格函数,都可以这样做。
复合类型参数会作为 Tcl 数组传递给过程。数组元素名就是该复合类型的属性名。如果传入行中的某个属性为 NULL,它就不会出现在数组中!下面是一个在 PL/Tcl 中定义 overpaid_2 函数(见旧版 PostgreSQL 文档)的例子:
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';
目前尚不支持返回复合类型的结果值。
24.2.2. PL/Tcl 中的数据值
提供给 PL/Tcl 函数脚本的参数值,只是将输入参数转换成文本形式(就像用 SELECT 语句将它们显示出来一样)。反过来,return命令会接受任何字符串,只要它是该函数声明返回类型的可接受输入格式。因此,在 PL/Tcl 函数内部,所有值都只是文本字符串。
24.2.3. PL/Tcl 中的全局数据
有时需要在两次过程调用之间保留某些全局状态数据,或者在不同过程之间共享。这很容易做到,因为同一后端中执行的所有 PL/Tcl 过程共享同一个安全的 Tcl 解释器。因此,任何全局 Tcl 变量都可以被所有 PL/Tcl 过程调用访问,并在 SQL 客户端连接的整个期间持续存在。(注意 PL/TclU 函数同样共享全局数据,但它们位于另一个不同的 Tcl 解释器中,无法与 PL/Tcl 函数通信。)
为帮助防止 PL/Tcl 过程无意间彼此干扰,每个过程都可通过upvar命令访问一个全局数组。该变量的全局名称是过程的内部名称,局部名称是GD。建议将 GD 用于保存过程的私有状态数据。只有当你明确希望某些值在多个过程之间共享时,才应使用常规的 Tcl 全局变量。
下面spi_execp的示例展示了GD的用法。
24.2.4. 从 PL/Tcl 访问数据库
在 PL/Tcl 过程体中可以使用下列命令来访问数据库:
spi_exec?-countn? ?-arrayname?query?loop-body?执行以字符串形式给出的 SQL 查询。查询出错时会引发错误。否则,该命令的返回值是查询处理的行数(选出、插入、更新或删除的行),如果查询是工具语句则返回零。此外,如果查询是 SELECT 语句,则所选列的值会按下文所述放入 Tcl 变量中。
可选的
-count值告诉spi_exec此查询最多处理多少行,其效果类似于将查询设为游标后执行FETCH n。如果查询是 SELECT 语句,则该语句结果列的值会放入以列名命名的 Tcl 变量中。如果给定了
-array选项,则列值会存储在指定的关联数组中,SELECT 列名用作数组索引。如果查询是 SELECT 语句且未给出
loop-body脚本,则只会把结果的第一行存入 Tcl 变量中;其余行如果存在,会被忽略。如果查询没有返回任何行,则不会进行任何存储(这种情况可以通过检查spi_exec的结果来发现)。例如:spi_exec "SELECT count(*) AS cnt FROM pg_proc"
会把 Tcl 变量
$cnt设置为 pg_proc 系统目录中的行数。如果给出了可选的
loop-body参数,它就是一段 Tcl 脚本,对 SELECT 结果中的每一行执行一次(注意:如果给定的查询不是 SELECT,则忽略loop-body)。在每次迭代之前,当前行各字段的值会被存入 Tcl 变量。例如:spi_exec -array C "SELECT * FROM pg_class" { elog DEBUG "have table $C(relname)" }会为 pg_class 的每一行打印一条 DEBUG 日志消息。这一特性的工作方式类似于其他 Tcl 循环构造;特别是
continue和break在循环体中按通常方式工作。如果 SELECT 结果的某个字段为 NULL,则其目标变量会被 “unset”,而不是被设置。
spi_preparequerytypelist准备并保存一个查询计划以供后续执行。保存的计划会在当前后端的整个生命周期内保留。
查询可以使用参数,也就是在实际执行该计划时要提供的值的占位符。在查询字符串中,用符号
$1...$n引用参数。如果查询使用了参数,则必须以 Tcl 列表的形式给出各参数类型的名称。(如果未使用参数,则为typelist写一个空列表。)目前,参数类型必须用 pg_type 中所示的内部类型名标识;例如用int4而不是integer。spi_prepare的返回值是一个查询 ID,供后续调用spi_execp时使用。示例参见spi_execp。spi_execp?-countn? ?-arrayname? ?-nullsstring?queryid?value-list? ?loop-body?执行先前用
spi_prepare准备好的查询。queryid是spi_prepare返回的 ID。如果查询引用了参数,则必须提供value-list。这是参数实际值构成的 Tcl 列表,其长度必须与先前提供给spi_prepare的参数类型列表相同。如果查询没有参数,则省略value-list。可选的
-nulls值是由空格和'n'字符组成的字符串,用来告诉spi_execp哪些参数是空值。如果给出,它的长度必须与value-list完全相同。如果未给出,则所有参数值都视为非空值。除了指定查询及其参数的方式之外,
spi_execp的工作方式与spi_exec完全相同。-count、-array和loop-body选项的含义相同,结果值也相同。下面是使用已准备计划的 PL/Tcl 函数示例:
CREATE FUNCTION t1_count(integer, integer) RETURNS integer 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" \\ [ list int4 int4 ] ] } spi_execp -count 1 $GD(plan) [ list $1 $2 ] return $cnt ' LANGUAGE 'pltcl';注意,在输入函数时,Tcl 应看到的每个反斜杠都必须写成双份,因为主解析器在 CREATE FUNCTION 中也会处理反斜杠。我们需要在传给
spi_prepare的查询字符串中加入反斜杠,以确保$n标记会原样传递给spi_prepare,而不会被 Tcl 执行变量替换。spi_lastoid返回由最后一条被
spi_exec或被spi_execp执行的查询所插入行的 OID(当该查询是单行 INSERT 时;否则得到零)。quotestring将给定字符串中的所有单引号和反斜杠字符都加倍。这可用于安全地为那些要插入到传给
spi_exec或spi_prepare的 SQL 查询中的字符串加引号。例如,设想类似下面这样的查询字符串:"SELECT '$val' AS ret"
其中 Tcl 变量 val 的实际内容是
doesn't。这会得到最终查询字符串:SELECT 'doesn't' AS ret
这会在
spi_exec或spi_prepare期间导致解析错误。提交的查询应当包含:SELECT 'doesn''t' AS ret
它在 PL/Tcl 中可以构成为:
"SELECT '[ quote $val ]' AS ret"
spi_execp的一个优点是,你不必像这样为参数值加引号,因为参数永远不会被当作 SQL 查询字符串的一部分来解析。eloglevelmsg发出日志或错误消息。可用级别包括
DEBUG、NOTICE、ERROR和FATAL。DEBUG和NOTICE只是像 elog 后端 C 函数那样把给定的消息发到 postmaster 日志中(NOTICE还会发送给客户端)。ERROR会引发错误条件:放弃函数的进一步执行,并中止当前事务。FATAL会中止事务并导致当前后端关闭。(在 PL/Tcl 函数中使用这个错误级别可能并没有什么充分理由,但为了完整性仍提供它)。
24.2.5. PL/Tcl 中的触发器过程
触发器过程可以用 PL/Tcl 编写。按照
PostgreSQL 的惯例,要作为触发器调用的过程必须声明为一个无参数、返回类型为 opaque
的函数。
来自触发器管理器的信息通过下列变量传入过程体:
$TG_nameCREATE TRIGGER 语句中触发器的名称。
$TG_relid导致触发器过程被调用的表的对象 ID。
$TG_relatts表字段名构成的 Tcl 列表,其前面带有一个空列表元素。因此,使用 Tcl 的
lsearch命令在该列表中查找元素名时,返回的元素编号会从 1 开始表示第一列,这与 PostgreSQL 中字段的惯常编号方式一致。$TG_when字符串
BEFORE或AFTER,取决于触发调用的类型。$TG_level字符串
ROW或STATEMENT,取决于触发调用的类型。$TG_op字符串
INSERT、UPDATE或DELETE,取决于触发调用的类型。$NEW一个包含新表行值的关联数组,用于 INSERT/UPDATE 动作;对于 DELETE 则为空。该数组以字段名为索引。值为 NULL 的字段不会出现在数组中!
$OLD一个包含旧表行值的关联数组,用于 UPDATE/DELETE 动作;对于 INSERT 则为空。该数组以字段名为索引。值为 NULL 的字段不会出现在数组中!
$args一个 Tcl 列表,包含 CREATE TRIGGER 语句中给出的过程参数。这些参数也可以在过程体中以
$1...$n的形式访问。
触发器过程的返回值可以是字符串 OK 或
SKIP,也可以是 array get Tcl 命令返回的列表。如果返回值为 OK,触发该触发的操作(INSERT/UPDATE/DELETE)将正常进行。SKIP 告诉触发器管理器静默地取消针对该行的操作。如果返回的是列表,则告诉 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 integer, description text, modcnt integer);
CREATE TRIGGER trig_mytab_modcount BEFORE INSERT OR UPDATE ON mytab
FOR EACH ROW EXECUTE PROCEDURE trigfunc_modcount('modcnt');
注意,触发器过程本身并不知道列名;列名由触发器参数提供。这使得该触发器过程可以在不同的表中复用。
24.2.6. 模块与 unknown 命令
PL/Tcl 支持在使用时自动装载(auto-loading)Tcl 代码。它会识别一张特殊的表
pltcl_modules,该表被假定包含若干 Tcl 代码模块。如果这张表存在,模块 unknown 会从该表取出,并在创建解释器之后立即装载到 Tcl 解释器中。
虽然 unknown 模块实际上可以包含你需要的任何初始化脚本,但它通常会定义一个 Tcl
“unknown” 过程,每当
Tcl 无法识别被调用的过程名时,就会调用该过程。PL/Tcl 的这一过程的标准版本会尝试在 pltcl_modules 中找到一个能够定义所需过程的模块。如果找到了,就把它装载到解释器中,然后允许继续执行最初尝试的过程调用。辅助表pltcl_modfuncs提供了哪个模块定义了哪些函数的索引,使查找相当快速。
PostgreSQL发行版中包含用于维护这些表的支持脚本:pltcl_loadmod、pltcl_listmod、pltcl_delmod,以及标准 unknown 模块的源代码,位于share/unknown.pltcl。要支持自动装载机制,最初必须把这个模块装载到每个数据库中。
表 pltcl_modules 和 pltcl_modfuncs
必须对所有用户可读,但最好只让数据库管理员拥有并可以写入它们。
24.2.7. Tcl 过程名称
在PostgreSQL中,只要参数个数或参数类型不同,就可以复用同一个函数名。不过,Tcl 要求所有过程名都必须不同。PL/Tcl 处理这一问题的方式是:在内部 Tcl 过程名中包含系统表 pg_proc 中该过程所在行的对象 ID 作为其名称的一部分。因此,名称相同但参数类型不同的PostgreSQL函数,也会对应不同的 Tcl 过程。这通常不是 PL/Tcl 程序员需要关心的事情,但在调试时可能会看见。