20.2. 描述

20.2.1. PL/Tcl 函数和参数

要用PL/Tcl语言创建一个函数,使用已知的语法

CREATE FUNCTION funcname (argument-types) RETURNS return-type AS '
    # 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 变量将被设置为一个空字串. 要检测一个特定的参数是否为 NULL,可以使用函数 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 函数中返回一个 NULL 数值, 可以执行 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';
     

目前没有返回一个复合类型结果值的支持.

20.2.2. PL/Tcl 里的数据值

提供给 PL/Tcl 函数脚本的参数值都只是转换成文本形式 的输入参数(就象它们用 SELECT 语句显示出来的那样). 相反,return 可以用任何可以为函数所声明的返回类型 接受的输入格式的字串.因此,PL/Tcl 程序员可以把数据值当做文本操作.

20.2.3. PL/Tcl 里的全局量

有时候 在两个过程之间保存一些状态数据和非常有用的. 因为所有在一个后端运行的PL/Tcl过程共享同一个安全 Tcl 解释器. 所以实现这个目标相当容易. 因此,任何全局 Tcl 变量都是可以被所有PL/Tcl过程调用访问的, 并且将在该次 SQL 客户端联接过程中保持一致.(请注意PL/TclU函数 也类似地共享全局数据,但是它们在一个不同的 Tcl 解释器里并且无法和 PL/Tcl函数通讯.

为了保护 PL/Tcl 过程相互之间不至于互相干扰, 每个过程可以通过upvar命令访问一个全局数组. 此变量的全局名称是过程的内部名称,其局部名称是GD. 我们建议使用GD作为存储过程的私有状态数据的数组. 而把普通的 Tcl 全局变量只用于那些你想在多个过程之间共享的变量.

一个使用 GD 的例子在下面的 spi_execp 例子里显示.

20.2.4. 在 PL/Tcl 里面访问数据库

在 PL/Tcl 过程体里有下面的命令可以用于从访问数据库∶

spi_exec ?-count n? ?-array name? query ?loop-body?

执行一个以字串形式给出的 SQL 查询.查询中的错误会导致抛出一个 错误.否则,该命令的返回值是查询处理的行数(选出,插入,更新,或者删除), 如果该查询是一个功能性语句则返回零.另外,如果查询是一条SELECT 语句,那么选出的字段按照下面描述的方法放在 Tcl 变量中.

可选的 -count 值告诉 spi_exec 在该查询中处理的最大的行数.其效果和把查询设置为一个游标, 然后说 FETCH n 是一样的.

如果查询是一个 SELECT 语句,那么 SELECT 的结果列的数值将放在 按照各字段命名的 Tcl 变量中.如果给出了 -array 选项, 那么字段值将放到这个命名的相关数组中,而 SELECT 的字段名可以用做 数组索引.

如果查询是 SELECT 语句并且没有给出 loop-body 脚本, 那么只有结果的头几行会存储到 Tcl 变量中;如果还有其他行的话, 将会被忽略.如果 SELECT 没有返回任何行,那么不会发生存储的现象 (这个情况可以通过检查 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 循环构造的运做方式累死;特别是 continuebreak 在循环体中的作用和平常是一样的.

如果一个 SELECT 结果的一个字段是 NULL,那么其目标变量就是 "unset" 而不是设置上什么东西.

spi_prepare query typelist

为后面的执行准备并保存一个查询规划.保存的规划的生命期就是 当前后端的生命期.

查询可以使用 arguments,这些参数是规划实际 执行的时候提供的数值的占位符. 在查询字串里用符号 $1 ... $n 引用各个参数.如果查询使用了参数,那么参数类型名必需以一个 Tcl 列表 的形式给出.(如果没有使用参数,那么给 typelist 写一个空列表.)目前,参数类型必需和 pg_type 里显示的内部类型名一样; 比如是int4 而不是 integer

spi_prepare 的返回值是一个可以在随后的 spi_execp 调用中使用的查询 ID.参阅 spi_execp 获取例子.

spi_execp ?-count n? ?-array name? ?-nulls string? queryid ?value-list? ?loop-body?

执行一个前面用 spi_prepare 准备的查询. queryidspi_prepare 返回的 ID.如果该查询引用了参数,那么我们必需提供一个 value-list∶这是一个 Tcl 列表,里面包含 那些参数的实际数值.这个列表的长度必需和前面给 spi_prepare 提供的参数类型列表的长度一样长.如果查询 没有参数,那么省略 value-list

-nulls 可选的数值是一个空白字串和字符 'n', 告诉 spi_execp 哪些参数是 NULL.如果给出, 那么它必需和 value-list 的长度相同. 如果没有给出,那么所有参数值都是非 NULL.

除了查询及其参数声明的方式之外,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

如果该查询是单行 INSERT, 返回最后的 spi_exec 或者 spi_execp 查询插入的行的 OID.(如果不是,你收到零.)

quote string

在给出的字串里将所由单引号和反斜扛字符复制成双份. 它可以用于安全地处理那些要输入到 spi_exec 或者 spi_prepare 中的 SQL 查询中的 引起字串.比如,假如一个查询看起来象这样

"SELECT '$val' AS ret"

这里的 Tcl 变量 val 实际上包含 doesn't. 这样最后的查询字串会是这样

SELECT 'doesn't' AS ret

而这个字串在 spi_execspi_prepare 的时候会导致一个分析错误. 提交的查询应该包含

SELECT 'doesn''t' AS ret

我们在 PL/Tcl 中可以这样构造

"SELECT '[ quote $val ]' AS ret"

spi_execp 的一个优点是你不需要象这样 引起参数值,因为参数决不会当做 SQL 查询字串的一部分分析.

elog level msg

发出一个日志或者错误消息.可能的级别是 DEBUGLOGINFONOTICEWARNINGERROR,和 FATAL. 大多数只是简单地发出指定消息, 就象后端的 C 函数 elogERROR 抛出一个错误条件∶该行数进一步的执行将中止, 同时退出当前事务.FATAL 退出当前事务并且导致当前后端 关闭(可能在 PL/Tcl 函数里没有什么理由使用这个错误级别,提供它主要 是为了完整).

20.2.5. PL/Tcl 里的触发器过程

触发器过程可以用 PL/Tcl 写.和 PostgreSQL 的传统一样,要当做触发器调用的过程必需声明为没有参数并且返回 类型为 trigger 的函数.

触发器管理器传递给过程体的信息是通过下面变量传递的:

$TG_name

CREATE TRIGGER 语句里的触发器名称.

$TG_relid

导致触发器被调用的表的对象标识.

$TG_relatts

以一个空表元素为前导的表中字段名称的 Tcl 列表. 所以用Tcl命令 lsearch在列表里查找元素名称时, 返回的从 1 开始计数的正整数,与PostgreSQL 里字段编号的传统一样.

$TG_when

由触发器调用事件决定的字符串BEFOREAFTER

$TG_level

由触发器调用事件决定的字符串ROWSTATEMENT

$TG_op

由触发器调用事件决定的字符串INSERTUPDATEDELETE

$NEW

一个关联数组,包含 INSERT/UPDATE 动作的新表行,如果是 DELETE 则为空. 该数组是用字段名做索引的.那些为 NULL 的字段不会在数组中出现!

$OLD

一个关联数组,包含 UPDATE/DELETE 动作的新表行,如果是 INSERT 则为空. 该数组是用字段名做索引的.那些为 NULL 的字段不会在数组中出现!

$args

如同在 CREATE TRIGGER 语句里给出的参数一样的Tcl 参数表. 这些参数在过程体里可以通过$1...$n来访问.

触发器过程返回的值是字符串OKSKIP之一, 或者一个象array get Tcl 命令返回的数组那样的东西. 如果返回值是OK,触发触发器的操作 (INSERT/UPDATE/DELETE)将会正常进行. SKIP告诉触发器管理器不声不响地忽略该行的操作. 如果返回一个数组,那么它告诉 PL/Tcl 返回一个修改后的 行给触发器管理器, 该行将代替在 $NEW (只在 INSERT/UPDATE 中起作用)中给出的行. 当然,这些只有在触发器是 BEFORE 和 FOR EACH ROW 时才有意义; 否则返回值将被忽略.

下面是一个小的触发器过程的例子, 它强制表内的一个整数值对行的更新次数进行跟踪. 对插入的新行,该值初始化为 0 并且在每次更新操作中加一:

CREATE FUNCTION trigfunc_modcount() RETURNS TRIGGER 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');
     

请注意触发器过程本身并不知道字段名字;那些是从触发器参数中 提供的.这样就让我们可以将触发器过程复用于不同的表.

20.2.6. 模块和unknown(未知)的命令

PL/Tcl 使用时支持自动装载 Tcl 代码. 它识别一个特殊的表,pltcl_modules,该表被认为 包含 Tcl 代码的模块.如果存在这个表,则在创建完解释器后马上从该表中抓取 unknown 模块并装载到 Tcl 解释器中.

因为 unknown 模块实际上可以包含任何你需要的初始化脚本, 它通常是定义为一个 Tcl "unknown" 过程,在 Tcl 不能识别一个 调用的过程名的时候就调用它.PL/Tcl这个过程的标准版本试图在 pltcl_modules 里找到一个定义所需要过程的模块. 如果找到一个,那么把它装载入解释器,然后允许继续按照原来的过程调用 处理.另外还定义了一个表 pltcl_modfuncs,它提供了 哪个函数由哪个模块定义的索引,因此查找过程相当快.

PostgreSQL 包括维护这些表的 支持脚本∶ pltcl_loadmodpltcl_listmodpltcl_delmod,以及标准未知模块 share/unknown.pltcl 的源代码.这个模块可以一开始就 装载入每个数据库以便支持自动装载机制.

pltcl_modulespltcl_modfuncs 必需 可以为所有人读取,但是把它做成只有数据库管理员可写并拥有是聪明的做法.

20.2.7. Tcl 过程名字

PostgreSQL 里,同一个函数名字 可以用于不同的函数,只要参数个数或者它们的类型不同. 不过,Tcl 要求所有的过程名字都是唯一的. PL/Tcl 通过把内部 Tcl 过程名字包含该过程的pg_proc行的对象 ID 来处理这些问题.因此同名不同参数类型的 PostgreSQL 行数也将会有不同的 Tcl 过程名. 这个问题通常对 PL/Tcl 程序员而言不算啥,但是在调试的时候可能会看到.