plpgsql_check

Описание

Это расширение представляет собой полный линтер для процедурного языка PL/pgSQL в QHB. Оно использует только внутренний синтаксический анализатор/оценщик QHB, поэтому позволяет видеть, какие ошибки могут возникнуть во время выполнения. Кроме того, оно анализирует SQL внутри подпрограмм и находит ошибки, которые обычно не обнаруживаются при выполнении команды CREATE PROCEDURE/FUNCTION. Этот модуль позволяет контролировать уровни многих предупреждений и подсказок. Наконец, он предоставляет возможность добавить маркеры типа PRAGAMA для включения/выключения многих аспектов, что позволяет скрыть сообщения, о которых уже известно, или напомнить о необходимости возврата к более глубокой очистке позже.


Установка

Модуль расширенной проверки PL/pgSQL для QHB поставляется в виде пакета qhb-1.5.2-plpgsql-check.

Подключите репозиторий пакетов QHB и установите пакет расширения для выбранной платформы со страницы загрузки.


Функциональные возможности

  • Проверка полей ссылочных объектов базы данных и типов внутри встроенного SQL
  • Подтверждение того, что для параметров функции используются правильные типы
  • Выявление неиспользуемых переменных и аргументов функций, немодифицированных выходных аргументов (OUT)
  • Частичное обнаружение неисполняемого кода (кода после команды RETURN)
  • Обнаружение отсутствия команды RETURN в функции (часто после обработчиков исключений, сложная логика)
  • Выявление нежелательных скрытых приведений, которые могут вызвать проблемы с производительностью, как, например, неиспользуемые индексы
  • Способность собирать отношения и функции, используемые функцией
  • Способность проверять операторы EXECUTE на уязвимость SQL-инъекций.

Поддерживаются версии QHB 1.5.1 и выше.

Операторы SQL внутри функций на PL/pgSQL проверяются валидатором на предмет семантических ошибок. Эти ошибки можно обнаружить, вызвав функцию plpgsql_check_function:

qhb=# CREATE EXTENSION plpgsql_check;
LOAD
qhb=# CREATE TABLE t1(a int, b int);
CREATE TABLE

qhb=#
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; -- здесь ошибка - в таблице t1 отсутствует столбец "c"
  END LOOP;
END;
$function$;

CREATE FUNCTION

qhb=# SELECT f1(); -- выполнение не обнаруживает ошибку, поскольку таблица t1 пуста
  f1
 ────

 (1 row)

qhb=# \x
Expanded display is on.
qhb=# 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]

qhb=# \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; --  здесь ошибка - в таблице t1 отсутствует столбец "c"
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)

qhb=# 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
Имя или сигнатура функции — для функций требуется спецификация функции. Любую функцию в QHB можно указать по OID, по имени или по сигнатуре. Когда известен OID или полная сигнатура функции, можно использовать параметр типа regprocedure, например 'fx()'::regprocedure или 16799::regprocedure. Возможная альтернатива — использовать только имя, когда имя функции уникально, например 'fx'. Когда имя не уникально или функция не существует, возникает ошибка.


Необязательные параметры

relid OID отношения, которому назначена триггерная функция. Этот параметр полезен, когда нужно проверить какую-либо триггерную функцию. Передается таблица, в которой работает триггер. Значение по умолчанию — 0.

fatal_errors (boolean)
Останавливает проверки при первой же ошибке (предотвращает массовые сообщения об ошибках). Значение по умолчанию — true.

other_warnings (boolean)
Показывает такие предупреждения, как разное количество атрибутов в назначении слева и справа, переменные перекрывают параметр функции, неиспользуемые переменные, нежелательное приведение и т. д. Значение по умолчанию — true.

extra_warnings (boolean)
Показывает такие предупреждения, как отсутствующие команды RETURN, скрываемые переменные, неисполняемый код, никогда не считываемый (неиспользуемый) параметр функции, немодифицированные переменные, модифицированные автоматические переменные и т. д. Значение по умолчанию — true.

performance_warnings (boolean)
Предупреждения, связанные с производительностью, такие как объявленный тип с модификатором типа, приведение, неявное приведение в предложении WHERE (может быть причиной, по которой индекс не используется) и т. д. Значение по умолчанию — false.

security_warnings (boolean)
Проверки, связанные с безопасностью, такие как обнаружение уязвимостей к SQL-инъекциям. Значение по умолчанию — false.

compatibility_warnings (boolean)
Проверки, связанные с совместимостью, такие как устаревшие явные параметры внутренних имен курсоров в refcursor или переменные курсора. Значение по умолчанию — false.

anyelementtype regtype
Фактический тип, который будет использоваться при тестировании типа anyelement. Значение по умолчанию — int.

anyenumtype regtype
Фактический тип, который будет использоваться при тестировании типа anyenum. Значение по умолчанию — '-'.

anyrangetype regtype
Фактический тип, который будет использоваться при тестировании типа anyrange. Значение по умолчанию — int4range.

anycompatibletype
Фактический тип, который будет использоваться при тестировании типа anycompatible. Значение по умолчанию — int.

anycompatiblerangetype
Фактический диапазонный тип, который будет использоваться при тестировании типа anycompatiblerange. Значение по умолчанию — int4range.

without_warnings
Отключить все предупреждения (игнорирует все параметры xxxx_warning, быстрое переопределение). Значение по умолчанию — false.

all_warnings Включить все предупреждения (игнорирует другие параметры xxx_warning, быстрое срабатывание). Значение по умолчанию — false.

newtable
oldtable
Имена новых (NEW) или старых (OLD) таблиц переходов соответственно. Эти параметры необходимы, когда таблицы переходов используются в триггерных функциях. Значение по умолчанию — NULL.

use_incomment_options
При значении true (по умолчанию) параметры в комментариях активны.

incomment_options_usage_warning
При значении true при использовании параметра в комментарии выдается предупреждение. Значение по умолчанию — false.


Триггеры

Чтобы проверить какой-либо триггер, нужно ввести отношение, которое будет использоваться с триггерной функцией:

CREATE TABLE bar(a int, b int);

qhb=# \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$

Если не указать отношение, возникнет ошибка:

qhb=# SELECT * FROM plpgsql_check_function('foo_trg()');
ERROR:  missing trigger relation
HINT:  Trigger relation oid must be valid
-- ОШИБКА: отсутствует триггерное отношение
-- ПОДСКАЗКА: oid триггерного отношения должен быть допустимым

Правильная проверка триггера (с указанным отношением):

qhb=# 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
    -- должно выполниться нормально;
    SELECT count(*) FROM newtab INTO x;

    -- должно завершиться неудачей;
    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: имя_параметра [=] значение [, имя_параметра [=] значение ...]

Настройки из параметров в комментарии имеют наивысший приоритет, но, в целом, это можно отключить, установив в параметре use_incomment_options значение false.

Пример:

CREATE OR REPLACE FUNCTION fx(anyelement)
returns text AS $$
BEGIN
  /*
   * переписать стандартный полиморфный тип в text
   * @plpgsql_check_options: anyelementtype = text
   */
  return $1;
END;
$$ language plpgsql;

Полная проверка кода

Функцию plpgsql_check_function можно использовать для массовой проверки функций/процедур и массовой проверки триггеров. Рекомендуется протестировать следующие запросы:

-- проверка всех нетриггерных функций PL/pgSQL
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;

или

-- проверка всех триггерных функций PL/pgSQL
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';

или

-- проверка всех функций PL/pgSQL (функций или триггерных функций с определенными триггерами)
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
        -- игнорировать неиспользованные триггеры
        (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 должен обнаружить почти все ошибки в действительно статичном коде. Когда разработчики используют динамическую функциональность 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;

Динамический SQL

Этот модуль не проверяет запросы, сформированные во время выполнения. Идентифицировать результаты динамических запросов невозможно — поэтому plpgsql_check не может установить правильный тип для переменных записи и не может проверить зависимые конструкции SQL и выражения.

Когда тип переменной записи неизвестен, можно назначить его явно с помощью директивы 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-инъекций может выявить только некоторые уязвимости к ним. Это средство нельзя использовать для аудита безопасности! Некоторые проблемы не будут выявлены. Кроме того, эта проверка может выдать ложную тревогу — скорее всего, когда переменная цензурируется другой командой или когда это значение какого-либо составного типа.


Курсорные переменные

Модуль plpgsql_check нельзя использовать для выявления структуры ссылочных курсоров. В PL/pgSQL ссылка на курсор реализуется в виде имени глобального курсора. Во время проверки это имя неизвестно (ни при каких обстоятельствах), и глобальный курсор не существует. Это значительное затруднение для любого статического анализа. plpgsql_check не может установить правильный тип для переменных записи и не может проверить зависимые конструкции SQL и выражения. Решение такое же, как и для динамического SQL. Не выбирайте переменные записи в качестве целевых при использовании типа refcursor или выключайте plpgsql_check для таких функций.

CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
  rec_var record;
BEGIN
  FETCH refcur_var INTO rec_var; -- это команда СТОП для plpgsql_check
  RAISE NOTICE '%', rec_var;     -- ошибка — запись rec_var еще не присвоена

В этом случае не следует использовать тип записи (вместо этого используйте известный составной тип):

CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
  rec_var некоторый_составной_тип;
BEGIN
  FETCH refcur_var INTO rec_var;
  RAISE NOTICE '%', rec_var;

Временные таблицы

Модуль plpgsql_check не может верифицировать запросы к временным таблицам, созданным во время выполнения функции PL/pgSQL. В таком случае необходимо создать фальшивую временную таблицу или выключить 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); -- никогда не содержит данных
CREATE TRIGGER foo_disable_dml
   BEFORE INSERT OR UPDATE OR DELETE ON foo
   EXECUTE PROCEDURE disable_dml();

qhb=# INSERT INTO  foo VALUES(10,20);
ERROR:  this instance of foo table does not allow any DML operation
HINT:  you should to run "CREATE TEMP TABLE foo(LIKE foo INCLUDING ALL);" statement
-- ОШИБКА: этот экземпляр таблицы foo не допускает никакие операции DML
-- ПОДСКАЗКА: следует выполнить оператор "CREATE TEMP TABLE foo(LIKE foo INCLUDING ALL);"
qhb=#

CREATE TABLE
qhb=# INSERT INTO  foo VALUES(10,20);
INSERT 0 1

Эта уловка частично эмулирует глобальные временные таблицы и допускает статическую валидацию. Другая возможность — использование шаблона обертки сторонних данных.

Можно использовать директиву 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 покажет все функции, операторы и отношения, используемые внутри обрабатываемой функции:

qhb=# 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 не может выделить область в разделяемой памяти, профиль сохраняется в памяти сеанса.

Вследствие зависимостей библиотека plpgsql должна идти в shared_preload_libraries первой.

qhb=# show shared_preload_libraries ;
┌──────────────────────────┐
│ shared_preload_libraries │
╞══════════════════════════╡
│ plpgsql,plpgsql_check    │
└──────────────────────────┘
(1 row)

Профилировщик активен, когда включена GUC plpgsql_check.profiler. Профилировщику не требуется разделяемая память, но если ее недостаточно, то его возможности ограничиваются активным сеансом. Активировать профилировщик можно, вызвав функцию plpgsql_check_profiler(true), а выключить — вызвав ту же функцию, но с аргументом false (или с литералами on и off соответственно).

Когда модуль plpgsql_check инициализируется посредством параметра shared_preload_libraries, становится доступна еще одна GUC для конфигурирования объема разделяемой памяти, используемой профилировщиком: plpgsql_check.profiler_max_shared_chunks. Она определяет максимальное количество блоков операторов, которое можно хранить в разделяемой памяти. Для каждой функции (или процедуры) PL/pgSQL общее содержимое разделяется на блоки по 30 операторов. При необходимости для хранения полного содержимого одной функции может использоваться множество блоков. Размер одного блока составляет 1704 байта. Значение по умолчанию для этой GUC равно 15000; этого должно быть достаточно для больших проектов, содержащих сотни операторов на PL/pgSQL, и займет это примерно 24 МБ памяти. Если для проекта не требуется такое количество блоков, можно установить в этом параметре меньшее значение, чтобы снизить потребление памяти. Минимальное значение — 50 (что должно занять примерно 83 кБ памяти), а максимальное — 100000 (что должно занять примерно 163 МБ памяти). Изменение этого параметра требует перезапуска QHB.

Также профилировщик будет извлекать идентификатор запроса для каждой инструкции, содержащей выражение или оптимизируемый оператор. Обратите внимание, что для этого требуется установка pg_stat_statements (или иного схожего стороннего расширения). Для извлечения идентификатора запроса существует несколько ограничений:

  • если выражение на PL/pgSQL содержит нижележащие операторы, будет извлечен только идентификатор верхнего уровня
  • сам профилировщик не вычисляет идентификатор запроса, а полагается в этом на внешнее расширение, например, на pg_stat_statements. Это означает, что, в зависимости от поведения внешнего расширения, для некоторых операторов будет невозможно увидеть идентификатор расширения. Например, это происходит с операторами DDL, поскольку pg_stat_statements не демонстрирует идентификаторы для таких запросов.
  • идентификатор запроса извлекается только для инструкций, содержащих выражения. Это означает, что функция plpgsql_profiler_function_tb() может вывести в одной строке меньше идентификаторов запроса, чем инструкций.

ВНИМАНИЕ!
Изменение разделяемых профилей может снизить производительность на более загруженных серверах.

Профиль можно отобразить с помощью функции plpgsql_profiler_function_tb:

qhb=# 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$

qhb=# 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:

qhb=# 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)


Трассировщик

Модуль plpgsql_check предоставляет возможность трассировки — в этом режиме можно увидеть уведомления в начале или конце функций (краткий уровень детализации и уровень детализации по умолчанию) и в начале или конце операторов (подробный уровень детализации). Для подробного уровня и уровня по умолчанию отображается содержимое аргументов функций. Содержимое связанных переменных отображается на подробном уровне.

qhb=# 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; или установки в plpgsql_check.enable_tracer значения on в qhb.conf. Это служит гарантией безопасности. Трассировщик выводит содержимое переменных PL/pgSQL, а затем некоторая информация, требующая повышенной безопасности, может быть продемонстрирована непривилегированному пользователю (когда он выполняет функцию со свойством SECURITY DEFINER (безопасность определившего)). Во-вторых, должно быть загружено расширение plpgsql_check. Это можно сделать, выполнив какую-либо функцию plpgsql_check, либо явно с помощью команды LOAD 'plpgsql_check';. Можно воспользоваться параметром конфигурации shared_preload_libraries, local_preload_libraries или session_preload_libraries.

В режиме с кратким уровнем детализации вывод сокращается:

qhb=# set plpgsql_check.tracer_verbosity TO terse;
SET
qhb=# 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

В режиме с подробным уровнем детализации выводится подробная информация об операторах:

qhb=# 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)

Особым функционалом трассировщика является трассировка оператора ASSERT при установке в plpgsql_check.trace_assert значения on. Если переменная plpgsql_check.trace_assert_verbosity равна DEFAULT, то когда выражение условия ASSERT равно false, отображаются все переменные функции или процедуры. Когда эта конфигурация равна VERBOSE, то отображаются все переменные из всех блоков PL/pgSQL. Это поведение не зависит от значения plpgsql.check_asserts. Его можно использовать, хотя во время выполнения PL/pgSQL утверждения отключаются.

qhb=# set plpgsql_check.tracer to off;
qhb=# set plpgsql_check.trace_assert_verbosity TO verbose;

qhb=# 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

qhb=# set plpgsql.check_asserts to off;
SET
qhb=# 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

Использование с plugin_debugger

Если вы используете plpgsql_check вместе с plugin_debugger (отладчик PL/pgSQL), то plpgsql_check следует инициализировать после plugin_debugger (поскольку plugin_debugger не поддерживает совместное использование отладочных API PL/pgSQL). Например (в qhb.conf):

shared_preload_libraries = 'plugin_debugger,plpgsql,plpgsql_check'

ВНИМАНИЕ!
Трассировщик выводит содержимое переменных или аргументов функций. У функций SECURITY DEFINER там могут содержаться конфиденциальные данные. По этой причине трассировщик по умолчанию выключен и должен включаться только с правами суперпользователя для plpgsql_check.enable_tracer.


Директивы

Поведение plpgsql_check в проверяемой функции можно сконфигурировать «директивной» функцией. Это аналог функциональности PRAGMA в языке PL/SQL или ADA. PL/pgSQL не поддерживает PRAGMA, но plpgsql_check выявляет функцию с именем plpgsql_check_pragma и забирает значения из параметров этой функции. Эти параметры plpgsql_check будут считаться верными до конца заданной группы операторов.

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
BEGIN
  ...
  -- выключить проверку для следующих операторов
  PERFORM plpgsql_check_pragma('disable:check');
  ...
  -- снова включить проверку
  PERFORM plpgsql_check_pragma('enable:check');
  ...
END;
$$ LANGUAGE plpgsql;

Функция plpgsql_check_pragma представляет собой неизменяемую функцию, возвращающую единицу. Она определяется расширением plpgsql_check. Альтернативную функцию plpgsql_check_pragma можно объявить следующим образом:

CREATE OR REPLACE FUNCTION plpgsql_check_pragma(VARIADIC args[])
RETURNS int AS $$
SELECT 1
$$ LANGUAGE sql IMMUTABLE;

Использование директивной функции в разделе объявлений верхнего блока также устанавливает параметры на уровне функции.

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 — создать эфемерную временную последовательность

Директивы enable:tracer и disable:tracer активны для QHB версии 1.5.0 и выше.


Лицензия

Copyright (c) Павел Стехуле ( pavel.stehule@gmail.com )

Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации (далее — Программное обеспечение), безвозмездно использовать Программное обеспечение без ограничений, включая неограниченное право на использование, копирование, изменение, слияние, публикацию, распространение, сублицензирование и/или продажу копий Программного обеспечения, а также лицам, которым предоставляется данное Программное обеспечение, при соблюдении следующих условий:

Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части данного Программного обеспечения.

ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.


См. также

Документация по plpgsql_check на Github