pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
要用 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';
目前尚不支持返回复合类型的结果值。
提供给 PL/Tcl 函数脚本的参数值,只是将输入参数转换成文本形式(就像用 SELECT 语句将它们显示出来一样)。反过来,return命令会接受任何字符串,只要它是该函数声明返回类型的可接受输入格式。因此,在 PL/Tcl 函数内部,所有值都只是文本字符串。
有时需要在两次过程调用之间保留某些全局状态数据,或者在不同过程之间共享。这很容易做到,因为同一后端中执行的所有 PL/Tcl 过程共享同一个安全的 Tcl 解释器。因此,任何全局 Tcl 变量都可以被所有 PL/Tcl 过程调用访问,并在 SQL 客户端连接的整个期间持续存在。(注意 PL/TclU 函数同样共享全局数据,但它们位于另一个不同的 Tcl 解释器中,无法与 PL/Tcl 函数通信。)
为帮助防止 PL/Tcl 过程无意间彼此干扰,每个过程都可通过upvar命令访问一个全局数组。该变量的全局名称是过程的内部名称,局部名称是GD。建议将 GD 用于保存过程的私有状态数据。只有当你明确希望某些值在多个过程之间共享时,才应使用常规的 Tcl 全局变量。
下面spi_execp的示例展示了GD的用法。
在 PL/Tcl 过程体中可以使用下列命令来访问数据库:
spi_exec ?-count n? ?-array name? 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_prepare query typelist准备并保存一个查询计划以供后续执行。保存的计划会在当前后端 的整个生命周期内保留。
查询可以使用参数,也就是在实际执行该计划时要提供的值的占位 符。在查询字符串中,用符号 $1 ... $n 引用参数。如果查询使用了参数,则必须以 Tcl 列表的形式给出 各参数类型的名称。(如果未使用参数,则为 typelist 写一个空列表。)目前,参数类型必须用 pg_type 中所示的内部类型名标识;例如用 int4 而不是 integer。
spi_prepare 的返回值是一个查询 ID,供 后续调用 spi_execp 时使用。示例参见 spi_execp。
spi_execp ?-count n? ?-array name? ?-nulls string? 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 时;否则得到零)。
quote string将给定字符串中的所有单引号和反斜杠字符都加倍。这可用于安全 地为那些要插入到传给 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 查询字符串的一部分 来解析。
elog level msg发出日志或错误消息。可用级别包括 DEBUG、 NOTICE、ERROR 和 FATAL。 DEBUG 和 NOTICE 只是像 elog 后端 C 函数那样把给定的消息发到 postmaster 日志中(NOTICE 还会发送给客户端)。 ERROR 会引发错误条件:放弃函数的进一步执行,并中止当前事务。 FATAL 会中止事务并导致当前后端关闭。 (在 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');
注意,触发器过程本身并不知道列名;列名由触发器参数提供。这使 得该触发器过程可以在不同的表中复用。
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 必须对所有用户可读,但最好只让数据库管理员拥有并可以写入它们。
在PostgreSQL中,只要参数个数或参数类型不同,就可以复用同一个函数名。不过,Tcl 要求所有过程名都必须不同。PL/Tcl 处理这一问题的方式是:在内部 Tcl 过程名中包含系统表 pg_proc 中该过程所在行的对象 ID 作为其名称的一部分。因此,名称相同但参数类型不同的PostgreSQL函数,也会对应不同的 Tcl 过程。这通常不是 PL/Tcl 程序员需要关心的事情,但在调试时可能会看见。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。