十二月 23, 2023
摘要:plpgsql_check
插件是一个 linter 工具,用于对 PostgreSQL PL/pgSQL 语言源代码进行静态分析,该语言是 PostgreSQL 存储过程的原生语言。
目录
介绍
plpgsql_check
插件是 PostgreSQL PL/pgSQL 的完整 linter。它仅利用内部的 PostgreSQL 解析器/评估器,因此您可以准确地看到运行时会发生的错误。此外,它还会解析函数/存储过程中的 SQL,并查找在“CREATE PROCEDURE/FUNCTION”命令中通常不会发现的错误。 您可以控制许多警告和提示的消息级别。最后,您可以添加 PRAGMA 类型的标记,来关闭/打开许多方面的检查,从而隐藏您已经知道的消息,或提醒您稍后再回来进行更深入的清理。
特性
- 检查内部 SQL 语句中引用的数据库对象字段和类型
- 验证函数参数是否使用了正确的类型
- 识别未使用到的变量、函数参数,以及未修改的 OUT 参数
- 尝试检测存在的死代码(RETURN 命令后的代码)
- 检测函数中 RETURN 命令的缺失(常见于异常处理之后,逻辑复杂的时候)
- 尝试识别不需要的隐式转换,这可能是一个性能问题,例如未使用到索引
- 能够收集函数使用到的关系和函数
- 能够检查 EXECUTE 语句是否存在 SQL 注入漏洞
验证器会检查 PL/pgSQL 函数中的 SQL 语句,报告存在的语义错误。可以通过调用plpgsql_check_function
找到这些错误:
主动模式
postgres=# CREATE EXTENSION plpgsql_check;
LOAD
postgres=# CREATE TABLE t1(a int, b int);
CREATE TABLE
postgres=#
CREATE OR REPLACE FUNCTION public.f1()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE r record;
BEGIN
FOR r IN SELECT * FROM t1
LOOP
RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column
END LOOP;
END;
$function$;
CREATE FUNCTION
postgres=# select f1(); -- execution doesn't find a bug due to empty table t1
f1
-----
(1 row)
postgres=# \x
Expanded display is on.
postgres=# select * from plpgsql_check_function_tb('f1()');
-[ RECORD 1 ]---------------------------
functionid | f1
lineno | 6
statement | RAISE
sqlstate | 42703
message | record "r" has no field "c"
detail | [null]
hint | [null]
level | error
position | 0
query | [null]
postgres=# \sf+ f1
CREATE OR REPLACE FUNCTION public.f1()
RETURNS void
LANGUAGE plpgsql
1 AS $function$
2 DECLARE r record;
3 BEGIN
4 FOR r IN SELECT * FROM t1
5 LOOP
6 RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column
7 END LOOP;
8 END;
9 $function$
函数plpgsql_check_function()
有三种可能的输出格式:text、json 或 xml
select * from plpgsql_check_function('f1()', fatal_errors := false);
plpgsql_check_function
------------------------------------------------------------------------
error:42703:4:SQL statement:column "c" of relation "t1" does not exist
Query: update t1 set c = 30
-- ^
error:42P01:7:RAISE:missing FROM-clause entry for table "r"
Query: SELECT r.c
-- ^
error:42601:7:RAISE:too few parameters specified for RAISE
(7 rows)
postgres=# select * from plpgsql_check_function('fx()', format:='xml');
plpgsql_check_function
----------------------------------------------------------------
<Function oid="16400">
<Issue>
<Level>error</level>
<Sqlstate>42P01</Sqlstate>
<Message>relation "foo111" does not exist</Message>
<Stmt lineno="3">RETURN</Stmt>
<Query position="23">SELECT (select a from foo111)</Query>
</Issue>
</Function>
(1 row)
参数
您可以通过函数参数设置警告级别:
强制性参数
funcoid oid
- 函数名称或函数签名 - 函数需要有函数规范。 PostgreSQL 中的任何函数都可以由 Oid、名称或签名指定。当您知道 oid 或完整的函数签名时,可以使用 regprocedure 类型参数,如'fx()'::regprocedure
或16799::regprocedure
。可能的替代方法是,当函数的名称是唯一的时候,可以仅使用名称 - 例如'fx'
。但是,当名称不唯一或函数不存在时,这样会引发错误。
可选参数
-
relid DEFAULT 0
- 触发器函数关联的关系的 OID。有必要检查任何触发器函数。通过该参数可传入触发器操作的表。 -
fatal_errors boolean DEFAULT true
- 在发现第一个错误时停止(防止报告大量错误信息) -
other_warnings boolean DEFAULT true
- 显示警告信息,例如在赋值表达式两侧使用了不同的属性编号、变量名覆盖了函数的参数、未使用到的变量、不需要的强制转换等。 -
extra_warnings boolean DEFAULT true
- 显示警告信息,如RETURN
语句缺失、影子变量、死代码、从未读取(未使用到)的函数参数、未修改的变量、修改过的自动变量等。 -
performance_warnings boolean DEFAULT false
- 性能相关的警告信息,例如带有类型修饰符的声明类型、强制转换、where 子句中的隐式转换(这可能是未使用到索引的原因)等。 -
security_warnings boolean DEFAULT false
- 安全相关的检查,如 SQL 注入漏洞检测。 -
compatibility_warnings boolean DEFAULT false
- 兼容性相关的检查,例如在 refcursor 或 cursor 变量中,显式设置过时的内部游标名称。 -
anyelementtype regtype DEFAULT 'int'
- 测试 anyelement 类型时要使用的实际类型。 -
anyenumtype regtype DEFAULT '-'
- 测试 anyenum 类型时要使用的实际类型。 -
anyrangetype regtype DEFAULT 'int4range'
- 测试 anyrange 类型时要使用的实际类型。 -
anycompatibletype DEFAULT 'int'
- 测试 anycompatible 类型时要使用的实际类型。 -
anycompatiblerangetype DEFAULT 'int4range'
- 测试 anycompatible range 类型时要使用的实际 range 类型。 -
without_warnings DEFAULT false
- 禁用所有警告(忽略所有 xxx_warning 参数,这是一种快速覆盖的方式) -
all_warnings DEFAULT false
- 启用所有警告(忽略其他 xxx_warning 参数,这是一种快速开启的方式) -
newtable DEFAULT NULL
,oldtable DEFAULT NULL
- NEW 或 OLD 转换表的名称。在触发器函数中使用转换表时,这些参数是必需的。 -
use_incomment_options DEFAULT true
- 如果为 true,则注释内选项会处于启用状态。 -
incomment_options_usage_warning DEFAULT false
- 如果为 true,则在使用注释内选项时会输出警告。 -
constant_tracing boolean DEFAULT true
- 当它为 true 时,那些存放常量内容的变量,可以像常量一样使用 (这只在一些简单的情况下起作用,且在这些情况下,变量的内容不应该模棱两可)。
触发器
当您想检查任何触发器时,您必须输入一个将与触发器函数一起使用的关系
CREATE TABLE bar(a int, b int);
postgres=# \sf+ foo_trg
CREATE OR REPLACE FUNCTION public.foo_trg()
RETURNS trigger
LANGUAGE plpgsql
1 AS $function$
2 BEGIN
3 NEW.c := NEW.a + NEW.b;
4 RETURN NEW;
5 END;
6 $function$
缺少关系检查时
postgres=# select * from plpgsql_check_function('foo_trg()');
ERROR: missing trigger relation
HINT: Trigger relation oid must be valid
正确的触发器函数检查(带有指定的关系)
postgres=# select * from plpgsql_check_function('foo_trg()', 'bar');
plpgsql_check_function
--------------------------------------------------------
error:42703:3:assignment:record "new" has no field "c"
(1 row)
对于带有传递表的触发器,可以设置oldtable
和newtable
参数:
create or replace function footab_trig_func()
returns trigger as $$
declare x int;
begin
if false then
-- should be ok;
select count(*) from newtab into x;
-- should fail;
select count(*) from newtab where d = 10 into x;
end if;
return null;
end;
$$ language plpgsql;
select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab');
注释内选项
plpgsql_check
允许在注释中保存检查的设置。它在检查之前会从函数的源代码获取这些选项。语法为:
@plpgsql_check_option: optioname [=] value [, optname [=] value ...]
注释内选项进行的设置具有最高优先级,但通常可以通过设置选项use_incomment_options
为false
来禁用它。
示例:
create or replace function fx(anyelement)
returns text as $$
begin
/*
* rewrite default polymorphic type to text
* @plpgsql_check_options: anyelementtype = text
*/
return $1;
end;
$$ language plpgsql;
检查所有代码
您可以使用plpgsql_check_function
,对函数/存储过程进行批量检查,也可以对触发器进行批量检查。请测试以下查询:
-- check all nontrigger plpgsql functions
SELECT p.oid, p.proname, plpgsql_check_function(p.oid)
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p ON pronamespace = n.oid
JOIN pg_catalog.pg_language l ON p.prolang = l.oid
WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279;
或
-- check all trigger plpgsql functions
SELECT p.proname, tgrelid::regclass, cf.*
FROM pg_proc p
JOIN pg_trigger t ON t.tgfoid = p.oid
JOIN pg_language l ON p.prolang = l.oid
JOIN pg_namespace n ON p.pronamespace = n.oid,
LATERAL plpgsql_check_function(p.oid, t.tgrelid) cf
WHERE n.nspname = 'public' and l.lanname = 'plpgsql';
或
-- check all plpgsql functions (functions or trigger functions with defined triggers)
SELECT
(pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement,
(pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level,
(pcf)."position", (pcf).query, (pcf).context
FROM
(
SELECT
plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0)) AS pcf
FROM pg_proc
LEFT JOIN pg_trigger
ON (pg_trigger.tgfoid = pg_proc.oid)
WHERE
prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND
pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND
-- ignore unused triggers
(pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR
pg_trigger.tgfoid IS NOT NULL)
OFFSET 0
) ss
ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno;
被动模式
警告:仅建议用于开发或预生产环境。
可以在执行时检查函数 - 必须加载了plpgsql_check
模块(通过 postgresql.conf)。
配置设置
plpgsql_check.mode = [ disabled | by_function | fresh_start | every_start ]
plpgsql_check.fatal_errors = [ yes | no ]
plpgsql_check.show_nonperformance_warnings = false
plpgsql_check.show_performance_warnings = false
缺省模式是by_function
,这意味着增强检查仅在主动模式下完成 - 通过调用plpgsql_check_function
的方式。fresh_start
表示冷启动(首先调用函数)。
您可以通过以下方式启用被动模式
load 'plpgsql'; -- 1.1 and higher doesn't need it
load 'plpgsql_check';
set plpgsql_check.mode = 'every_start'; -- This scans all code before it is executed
SELECT fx(10); -- run functions - function is checked before runtime starts it
兼容性警告
将字符串赋值给 refcursor 变量
PostgreSQL 的 cursor 和 refcursor 变量是增强的字符串变量,具有相关 portal(Postgres 的一个内部结构,用于游标的实现)的唯一名称。在 PostgreSQL 16 之前,portal 的名称与游标变量的名称相同。PostgreSQL 16 及更高版本更改了此机制,默认情况下,相关 portal 将以某个唯一名称命名。它解决了嵌套代码块中游标的一些问题,或者在递归调用函数中使用游标时的一些问题。
按照上述更改,refcursor 的变量应从另一个 refcursor 变量或某个 cursor 变量(当游标打开时)获取值。
-- obsolete pattern
DECLARE
cur CURSOR FOR SELECT 1;
rcur refcursor;
BEGIN
rcur := 'cur';
OPEN cur;
...
-- new pattern
DECLARE
cur CURSOR FOR SELECT 1;
rcur refcursor;
BEGIN
OPEN cur;
rcur := cur;
...
当compatibility_warnings
标记处于启用状态时,plpgsql_check
会尝试识别,给 refcursor 变量进行的一些可疑的赋值,或者返回 refcursor 值的情况:
CREATE OR REPLACE FUNCTION public.foo()
RETURNS refcursor
AS $$
declare
c cursor for select 1;
r refcursor;
begin
open c;
r := 'c';
return r;
end;
$$ LANGUAGE plpgsql;
select * from plpgsql_check_function('foo', extra_warnings =>false, compatibility_warnings => true);
plpgsql_check_function
-----------------------------------------------------------------------------------
compatibility:00000:6:assignment:obsolete setting of refcursor or cursor variable
Detail: Internal name of cursor should not be specified by users.
Context: at assignment to variable "r" declared on line 3
(3 rows)
限制
在真正的静态代码上,plpgsql_check
应该会找到几乎所有错误。当开发人员在使用 PL/pgSQL 的动态功能(如动态 SQL 或 record 数据类型)时,可能会出现误报。在编写良好的代码中,这些应该很少见,然后受影响的函数应该进行重新设计,或者应该对此函数禁用plpgsql_check
。
CREATE OR REPLACE FUNCTION f1()
RETURNS void AS $$
DECLARE r record;
BEGIN
FOR r IN EXECUTE 'SELECT * FROM t1'
LOOP
RAISE NOTICE '%', r.c;
END LOOP;
END;
$$ LANGUAGE plpgsql SET plpgsql.enable_check TO false;
注意:使用 plpgsql_check 会增加少量开销(在启用被动模式时),您应该仅在开发或预生产环境中使用该设置。
动态 SQL
该模块不会检查在运行时组装的查询。它无法识别动态查询的结果,因此plpgsql_check
无法设置正确的类型来记录变量,也无法检查依赖的 SQL 和表达式。
当 record 变量的类型未知时,可以使用检查指示type
显式指定它:
DECLARE r record;
BEGIN
EXECUTE format('SELECT * FROM %I', _tablename) INTO r;
PERFORM plpgsql_check_pragma('type: r (id int, processed bool)');
IF NOT r.processed THEN
...
注意:SQL 注入检查只能检测部分 SQL 注入漏洞。此工具不能用于安全审计!某些问题会无法检测到。此检查也可能引发误报 - 可能是当变量被其他命令擦除,或者该值为某种复合类型时。
引用游标
plpgsql_check
不能用于检测引用游标的结构。PL/pgSQL 中对游标的引用被实现为全局游标的名称。在检查时,名称是未知的(并非所有可能的情况),并且全局游标是不存在的。对于任何静态分析来说,这都是一个重要问题。PL/pgSQL 不知道如何为 record 变量设置正确的类型,也无法检查依赖的 SQL 语句和表达式。对于动态 SQL,解决方案是相同的。在使用refcursor
类型时,请勿使用 record 变量作为赋值表达式中的目标变量,如果确实需要,可以对这些函数禁用plpgsql_check
。
CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
rec_var record;
BEGIN
FETCH refcur_var INTO rec_var; -- this is STOP for plpgsql_check
RAISE NOTICE '%', rec_var; -- record rec_var is not assigned yet error
在这种情况下,不应使用 record 类型(请改用已知的行类型):
CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
rec_var some_rowtype;
BEGIN
FETCH refcur_var INTO rec_var;
RAISE NOTICE '%', rec_var;
临时表
plpgsql_check
无法验证在 plpgsql 函数运行时创建的临时表上的查询。对于这种场景,有必要创建一个假的临时表,或者对函数禁用plpgsql_check
。
实际上,临时表存储在(每个用户)自己的模式中,其优先级高于普通表。因此,您可以安全地使用下面的方法进行操作:
CREATE OR REPLACE FUNCTION public.disable_dml()
RETURNS trigger
LANGUAGE plpgsql AS $function$
BEGIN
RAISE EXCEPTION SQLSTATE '42P01'
USING message = format('this instance of %I table doesn''t allow any DML operation', TG_TABLE_NAME),
hint = format('you should use "CREATE TEMP TABLE %1$I(LIKE %1$I INCLUDING ALL);" statement',
TG_TABLE_NAME);
RETURN NULL;
END;
$function$;
CREATE TABLE foo(a int, b int); -- doesn't hold data, ever
CREATE TRIGGER foo_disable_dml
BEFORE INSERT OR UPDATE OR DELETE ON foo
EXECUTE PROCEDURE disable_dml();
postgres=# INSERT INTO foo VALUES(10,20);
ERROR: this instance of foo table doesn't allow any DML operation
HINT: you should to run "CREATE TEMP TABLE foo(LIKE foo INCLUDING ALL);" statement
postgres=#
CREATE TABLE
postgres=# INSERT INTO foo VALUES(10,20);
INSERT 0 1
该方法部分模拟了 GLOBAL TEMP 表,这样就可以进行静态的验证。
您可以使用检查指示table
,来创建临时使用的表:
BEGIN
CREATE TEMP TABLE xxx(a int);
PERFORM plpgsql_check_pragma('table: xxx(a int)');
INSERT INTO xxx VALUES(10);
PERFORM plpgsql_check_pragma('table: [pg_temp].zzz(like schemaname.table1 including all)');
...
依赖项列表
函数plpgsql_show_dependency_tb
可以显示,处理的函数内部使用到的所有函数、运算符和关系:
postgres=# select * from plpgsql_show_dependency_tb('testfunc(int,float)');
type | oid | schema | name | params
----------|-------|--------|---------|----------------------------
FUNCTION | 36008 | public | myfunc1 | (integer,double precision)
FUNCTION | 35999 | public | myfunc2 | (integer,double precision)
OPERATOR | 36007 | public | ** | (integer,integer)
RELATION | 36005 | public | myview |
RELATION | 36002 | public | mytable |
(4 rows)
plpgsql_show_dependency_tb
的可选参数有:relid
、anyelementtype
、enumtype
、anyrangetype
、anycompatibletype
和anycompatiblerangetype
。
分析器
plpgsql_check
包含了一个简单的 PL/pgSQL 函数和存储过程的分析器。在可以或者不能访问共享内存的情况下,它都可以工作。这取决于shared_preload_libraries
配置。当plpgsql_check
通过shared_preload_libraries
初始化时,它可以分配共享内存,并将函数的概要信息存储在那里。当plpgsql_check
无法分配共享内存时,函数概要信息将会存储在会话内存中。
由于依赖关系,shared_preload_libraries
应该先包含plpgsql
。
postgres=# show shared_preload_libraries;
shared_preload_libraries
--------------------------
plpgsql,plpgsql_check
(1 row)
当 GUC 参数plpgsql_check.profiler
打开时,分析器处于启用状态。分析器不需要共享内存,但如果没有足够的共享内存,则分析器仅限于活跃会话。分析器可以通过调用函数plpgsql_check_profiler(true)
来激活,也可以通过使用false
参数(或者on
、off
)调用相同的函数来禁用。
当通过shared_preload_libraries
初始化plpgsql_check
时,另一个 GUC 参数可用于配置分析器使用的共享内存量:plpgsql_check.profiler_max_shared_chunks
。这定义了共享内存中可以存储的最大语句块数。对于每个 PL/pgSQL 函数(或存储过程),整个内容被拆分为 30 个语句的块。如果需要,可以使用多个块来存储单个函数的全部内容。单个块为 1704 字节。这个 GUC 参数的默认值是 15000,这对于在 plpgsql 中包含数十万条语句的大型项目来说应该足够了,并且会消耗大约 24MB 的内存。如果您的项目不需要那么多块,您可以将此参数设置为较小的数字,以减少内存使用量。最小值为 50(会消耗大约 83kB 的内存),最大值为 100000(会消耗大约 163MB 的内存)。更改此参数需要重新启动 PostgreSQL 数据库服务。
分析器还会为包含表达式或可优化语句的每条指令检索查询标识符。请注意,这需要安装pg_stat_statements
或其他类似的第三方插件。查询标识符的检索存在一些限制:
- 如果一个 PL/pgSQL 表达式包含了下层的语句,则只会检索顶层查询的标识符。
- 分析器本身不会计算查询标识符,而是依赖于外部插件(如
pg_stat_statements
)来实现。这意味着,根据外部插件的行为,您可能无法看到某些语句的查询标识符。例如,DDL 语句就是这种情况,因为pg_stat_statements
不会提供此类查询的查询标识符。 - 只有包含表达式的指令,才会检索查询标识符。这意味着在单行代码中,
plpgsql_profiler_function_tb()
函数报告的查询标识符会比指令少。
注意:在负载较高的服务器上,更新共享的概要信息,可能会降低数据库性能。
概要信息可以使用函数plpgsql_profiler_function_tb
显示:
postgres=# select lineno, avg_time, source from plpgsql_profiler_function_tb('fx(int)');
lineno | avg_time | source
--------|----------|-------------------------------------------------------------------
1 | |
2 | | declare result int = 0;
3 | 0.075 | begin
4 | 0.202 | for i in 1..$1 loop
5 | 0.005 | select result + i into result; select result + i into result;
6 | | end loop;
7 | 0 | return result;
8 | | end;
(9 rows)
结果中的时间以毫秒为单位。
每个语句(不是每行)的概要信息,可以通过函数plpgsql_profiler_function_statements_tb
来显示:
CREATE OR REPLACE FUNCTION public.fx1(a integer)
RETURNS integer
LANGUAGE plpgsql
1 AS $function$
2 begin
3 if a > 10 then
4 raise notice 'ahoj';
5 return -1;
6 else
7 raise notice 'nazdar';
8 return 1;
9 end if;
10 end;
11 $function$
postgres=# select stmtid, parent_stmtid, parent_note, lineno, exec_stmts, stmtname
from plpgsql_profiler_function_statements_tb('fx1');
stmtid | parent_stmtid | parent_note | lineno | exec_stmts | stmtname
--------|---------------|-------------|--------|------------|-----------------
0 | ∅ | ∅ | 2 | 0 | statement block
1 | 0 | body | 3 | 0 | IF
2 | 1 | then body | 4 | 0 | RAISE
3 | 1 | then body | 5 | 0 | RETURN
4 | 1 | else body | 7 | 0 | RAISE
5 | 1 | else body | 8 | 0 | RETURN
(6 rows)
所有存储的概要信息,可以通过调用函数plpgsql_profiler_functions_all
来显示:
postgres=# select * from plpgsql_profiler_functions_all();
funcoid | exec_count | total_time | avg_time | stddev_time | min_time | max_time
-----------------------|------------|------------|----------|-------------|----------|----------
fxx(double precision) | 1 | 0.01 | 0.01 | 0.00 | 0.01 | 0.01
(1 row)
有两个函数可用于清理存储的概要信息:plpgsql_profiler_reset_all()
和plpgsql_profiler_reset(regprocedure)
。
覆盖率指标
plpgsql_check
提供了两个函数:
plpgsql_coverage_statements(name)
plpgsql_coverage_branches(name)
备注
还有一个非常好用的 PL/pgSQL 分析器 - https://github.com/glynastill/plprofiler
plpgsql_check
的分析器设计简单易用且实用。仅此而已。
plprofiler 更加复杂。它会构建调用图,并从此图可以创建执行时间的火焰图。
这两个插件都可以与 PostgreSQL 内置的功能(跟踪函数)一起使用。
set track_functions to 'pl';
...
select * from pg_stat_user_functions;
追踪器
plpgsql_check
提供了一种追踪的可能性 - 在此模式下,您可以看到有关开始或结束函数(简洁和默认的输出模式下)的通知,以及开始或结束语句(详细的输出模式下)的通知。对于默认和详细的输出模式,会显示出函数参数的内容。在详细的输出模式下,会显示相关变量的内容。
postgres=# do $$ begin perform fx(10,null, 'now', e'stěhule'); end; $$;
NOTICE: #0 ->> start of inline_code_block (Oid=0)
NOTICE: #2 ->> start of function fx(integer,integer,date,text) (Oid=16405)
NOTICE: #2 call by inline_code_block line 1 at PERFORM
NOTICE: #2 "a" => '10', "b" => null, "c" => '2020-08-03', "d" => 'stěhule'
NOTICE: #4 ->> start of function fx(integer) (Oid=16404)
NOTICE: #4 call by fx(integer,integer,date,text) line 1 at PERFORM
NOTICE: #4 "a" => '10'
NOTICE: #4 <<- end of function fx (elapsed time=0.098 ms)
NOTICE: #2 <<- end of function fx (elapsed time=0.399 ms)
NOTICE: #0 <<- end of block (elapsed time=0.754 ms)
#
后面的数字是执行栈帧的计数器,此数字与错误上下文堆栈的深度有关。它允许函数的开始和结束配对显示。
通过将plpgsql_check.tracer
设置为on
可以启用追踪。注意 - 启用此行为会对性能产生重大负面影响(这与分析器不同)。您可以为追踪器使用到的plpgsql_check.tracer_errlevel
参数,设置输出级别(默认值为notice
)。输出内容受plpgsql_check.tracer_variable_max_length
配置变量指定的长度限制。追踪器可以通过调用函数plpgsql_check_tracer(true)
来激活,也可以通过使用false
参数(或着on
、off
)调用相同的函数来禁用。
首先,超级用户应该通过执行set plpgsql_check.enable_tracer to on;
,或者在postgresql.conf
中添加plpgsql_check.enable_tracer = on
,来显式地启用追踪器。这是一项安全保障措施。追踪器会显示 plpgsql 变量的内容,然后可以向非特权用户显示一些安全敏感信息(在他运行安全性定义者函数时)。其次,应该加载插件plpgsql_check
。它可以通过执行某些plpgsql_check
函数完成,也可以通过命令load 'plpgsql_check';
显式完成。您可以使用配置选项shared_preload_libraries
、local_preload_libraries
或者session_preload_libraries
。
在简洁输出的模式下,输出会减少:
postgres=# set plpgsql_check.tracer_verbosity TO terse;
SET
postgres=# do $$ begin perform fx(10,null, 'now', e'stěhule'); end; $$;
NOTICE: #0 start of inline code block (oid=0)
NOTICE: #2 start of fx (oid=16405)
NOTICE: #4 start of fx (oid=16404)
NOTICE: #4 end of fx
NOTICE: #2 end of fx
NOTICE: #0 end of inline code block
在详细输出的模式下,输出增加了有关语句的详细信息:
postgres=# do $$ begin perform fx(10,null, 'now', e'stěhule'); end; $$;
NOTICE: #0 ->> start of block inline_code_block (oid=0)
NOTICE: #0.1 1 --> start of PERFORM
NOTICE: #2 ->> start of function fx(integer,integer,date,text) (oid=16405)
NOTICE: #2 call by inline_code_block line 1 at PERFORM
NOTICE: #2 "a" => '10', "b" => null, "c" => '2020-08-04', "d" => 'stěhule'
NOTICE: #2.1 1 --> start of PERFORM
NOTICE: #2.1 "a" => '10'
NOTICE: #4 ->> start of function fx(integer) (oid=16404)
NOTICE: #4 call by fx(integer,integer,date,text) line 1 at PERFORM
NOTICE: #4 "a" => '10'
NOTICE: #4.1 6 --> start of assignment
NOTICE: #4.1 "a" => '10', "b" => '20'
NOTICE: #4.1 <-- end of assignment (elapsed time=0.076 ms)
NOTICE: #4.1 "res" => '130'
NOTICE: #4.2 7 --> start of RETURN
NOTICE: #4.2 "res" => '130'
NOTICE: #4.2 <-- end of RETURN (elapsed time=0.054 ms)
NOTICE: #4 <<- end of function fx (elapsed time=0.373 ms)
NOTICE: #2.1 <-- end of PERFORM (elapsed time=0.589 ms)
NOTICE: #2 <<- end of function fx (elapsed time=0.727 ms)
NOTICE: #0.1 <-- end of PERFORM (elapsed time=1.147 ms)
NOTICE: #0 <<- end of block (elapsed time=1.286 ms)
追踪器的一个特殊功能是,在将plpgsql_check.trace_assert
设置为on
时,可以跟踪ASSERT
语句。在plpgsql_check.trace_assert_verbosity
设置为DEFAULT
时,当断言表达式结果为 false 时,会显示函数或存储过程中的相关变量。 在该配置设置为VERBOSE
时,所有 plpgsql 栈帧中的所有变量都会显示。这种行为与plpgsql.check_asserts
参数的值无关。尽管断言在 plpgsql 运行时被禁用,但可以使用它。
postgres=# set plpgsql_check.tracer to off;
postgres=# set plpgsql_check.trace_assert_verbosity TO verbose;
postgres=# do $$ begin perform fx(10,null, 'now', e'stěhule'); end; $$;
NOTICE: #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false
NOTICE: "a" => '10', "res" => null, "b" => '20'
NOTICE: #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
NOTICE: "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'stěhule'
NOTICE: #0 PL/pgSQL function inline_code_block line 1 at PERFORM
ERROR: assertion failed
CONTEXT: PL/pgSQL function fx(integer) line 12 at ASSERT
SQL statement "SELECT fx(a)"
PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
SQL statement "SELECT fx(10,null, 'now', e'stěhule')"
PL/pgSQL function inline_code_block line 1 at PERFORM
postgres=# set plpgsql.check_asserts to off;
SET
postgres=# do $$ begin perform fx(10,null, 'now', e'stěhule'); end; $$;
NOTICE: #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false
NOTICE: "a" => '10', "res" => null, "b" => '20'
NOTICE: #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
NOTICE: "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'stěhule'
NOTICE: #0 PL/pgSQL function inline_code_block line 1 at PERFORM
DO
追踪器可以显示子事务缓冲区 ID(nxids
)的使用情况。显示的tnl
数字是事务嵌套层级数(对于 plpgsql,它取决于带有异常处理的代码块的深度)。
检测未关闭的游标
PL/pgSQL 中的游标只是 SQL 游标的名称。SQL 游标的生命周期并没有关联到相关 plpgsql 游标变量的作用域。SQL 游标在事务结束时会被自己关闭,这对于较长的事务和过多打开的游标,可能会有点迟。当不需要游标时,最好显式地关闭游标(通过 CLOSE 语句)。不显式关闭的话,可能会出现严重的内存问题。
当 OPEN 语句尝试使用尚未关闭的游标时,会引发警告。可以通过设置plpgsql_check.cursors_leaks
为off
来禁用此功能。当函数被递归地调用时,该检查处于关闭状态。
函数完成后,可以立即检查未关闭的游标。默认情况下,该检查处于关闭状态,应该通过设置plpgsql_check.strict_cursors_leaks
为on
来启用检查。
任何未关闭的游标都只会报告一次。
结合使用 plugin_debugger
如果您将plugin_debugger
(plpgsql 调试器)与plpgsql_check
一起使用,则应在plugin_debugger
之后初始化plpgsql_check
(因为plugin_debugger
不支持共享 PL/pgSQL 的调试 API)。例如,可以这样配置postgresql.conf
:
shared_preload_libraries = 'plugin_debugger,plpgsql,plpgsql_check'
注意 - 安全
追踪器会输出变量或函数参数的内容。对于安全性定义者函数,这些内容可能包含安全敏感数据。这就是为什么默认情况下要禁用追踪器,并且只能使用超级用户权限启用plpgsql_check.enable_tracer
的原因。
检查指示
您可以在检查的函数中,使用“pragma”函数配置plpgsql_check
的行为。这是 PL/SQL 或 ADA 语言中 PRAGMA 功能的类比。PL/pgSQL 不支持 PRAGMA,但plpgsql_check
会检测名为plpgsql_check_pragma
的函数,并从此函数的参数中获取选项。这些plpgsql_check
选项,直到这组语句的末尾都是有效的。
CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
BEGIN
...
-- for following statements disable check
PERFORM plpgsql_check_pragma('disable:check');
...
-- enable check again
PERFORM plpgsql_check_pragma('enable:check');
...
END;
$$ LANGUAGE plpgsql;
函数plpgsql_check_pragma
是一个总是返回 1 的不可变函数。它由plpgsql_check
插件定义。您可以声明替代的plpgsql_check_pragma
函数,例如:
CREATE OR REPLACE FUNCTION plpgsql_check_pragma(VARIADIC args[])
RETURNS int AS $$
SELECT 1
$$ LANGUAGE sql IMMUTABLE;
在顶部代码块的声明部分使用 pragma 函数,也可以设置函数级别的选项。
CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
DECLARE
aux int := plpgsql_check_pragma('disable:extra_warnings');
...
也支持更短的检查指示语法:
CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
DECLARE r record;
BEGIN
PERFORM 'PRAGMA:TYPE:r (a int, b int)';
PERFORM 'PRAGMA:TABLE: x (like pg_class)';
...
支持的检查指示
-
echo:str
- 输出字符串(用于测试)。在字符串内部,可以使用“变量”:@@id、@@name、@@signature -
status:check
,status:tracer
,status:other_warnings
,status:performance_warnings
,status:extra_warnings
,status:security_warnings
这将输出当前值(例如other_warnings
启用时) -
enable:check
,enable:tracer
,enable:other_warnings
,enable:performance_warnings
,enable:extra_warnings
,enable:security_warnings
-
disable:check
,disable:tracer
,disable:other_warnings
,disable:performance_warnings
,disable:extra_warnings
,disable:security_warnings
这可用于从 anyelement 函数返回时禁用提示。只需将检查指示放在 RETURN 语句之前即可。 -
type:varname typename
或type:varname (fieldname type, ...)
- 设置 record 类型变量的实际类型 -
table: name (column_name type, ...)
或table: name (like tablename)
- 创建临时使用的临时表(如果要指定模式,则只允许使用pg_temp
模式)。 -
sequence: name
- 创建临时使用的临时序列 -
assert-schema: varname
- 检查时的断言 - 确保变量指定的模式有效 -
assert-table: [ varname_schema, ] , varname
- 确保变量(通过常量跟踪)指定的表名有效 -
assert-column: [varname_schema, ], varname_table , varname
- 确保变量指定的列有效
检查指示enable:tracer
和disable:tracer
,只在 PostgreSQL 12 及更高版本才处于启用状态。
更新
plpgsql_check
不支持版本的更新。在安装该插件的新版本之前,您应该先删除它。