пятница, 24 февраля 2017 г.

PLDebugger - отладчик PL/pgSQL

Возникло желание посмотреть как в pgAdmin работает отладчик PL/pgSQL и можно ли его использовать самостоятельно, например в psql.

В итоге всё получилось. Но не всё прошло гладко, поэтому есть смысл остановиться подробнее на каждом шаге:
  1. Установить pgAdmin 4.
  2. Установить pldebugger.
  3. Проверить отладчик в pgAdmin и найти описание использования api.
  4. Сделать пример в psql.
Исходные данные:
  • ОС Ubuntu 14.04 
  • PostgreSQL 9.6.2 (установлен из пакета пользователем root)

1. Установка pgAdmin 4

Установка pgAdmin 4 v1.2 на Ubuntu осложняется тем, что на сайте и в документации нет раздела "Установка".
Для обладателей Windows и MacOS программы установки уже подготовлены в разделе Download. Пользователи Ubuntu могут воспользоваться инструкцией с askubuntu.com. Только вместо несуществующего пакета virtualenv нужно установить python-virtualenv.

2. Установка отладчика pldebugger

Отладчик PL/pgSQL устанавливается отдельно от pgAdmin. Он представляет собой расширение PostgreSQL, которое нужно собрать и установить, а также прописать в параметре shared_preload_libraries библиотеку plugin_debugger.

В файле README.pldebugger есть раздел про установку, ориентированный на сборку PostgreSQL из исходных кодов. Для PostgreSQL, установленного из пакета, можно сделать так:
  1. Понадобятся пакеты:
    apt-get install postgresql-server-dev-9.6 libssl-dev libkrb5-dev
  2. Расширение устанавливаем из под root, используя PGXS:
    sudo su root
    git clone git://git.postgresql.org/git/pldebugger.git
    cd pldebugger
    export USE_PGXS=1
    make
    make install
  3. Не забываем перезапустить PostgreSQL после установки параметра shared_preload_libraries.

3. Проверка отладчика в pgAdmin и описание работы api

Отладчик работает согласно документации. Здесь особо нечего добавить. Разве что для установки точки останова, нужно "тыкать" мышкой не в число с номером строки исходного кода, а чуть-чуть правее, на свободный участок, в котором должна появиться точка.

Что касается описания функций API для использования без pgAdmin. Отдельного описания найти не удалось, но можно воспользоваться следующим:
  1. Достаточно подробные комментарии в исходном коде. Например, в файле pldbgapi.c.
  2. В переписке разработчиков нашлось такое письмо за 2007 год от Korry Douglas.
  3. Никто не отменял самый "честный" способ. Включить журналирование всех команд (log_statement='all'), запустить отладку в pgAdmin и смотреть, что и в каком порядке пишется в журнал сервера.

4. Пример отладки в psql

Вооружившись необходимыми знаниями можно попробовать выполнить отладку в psql, используя расширение pldbgapi.

Понадобятся два сеанса psql: 1 - сеанс управления отладкой (proxy), 2 - сеанс, в котором запускается функция для отладки (target).

Установим отладчик:

 1| => show shared_preload_libraries;

 1|  shared_preload_libraries 
 1| ---------------------------
 1|  "$libdir/plugin_debugger"
 1| (1 row)
 1|

 1| => create extension pldbgapi;

 1| CREATE EXTENSION


Создадим функцию для подсчета количества строк в таблице, имя которой
передается параметром.

При помощи отладчика мы хотим убедиться, что текст команды SELECT формируется корректно:

 1| => CREATE FUNCTION get_count (tabname text) RETURNS bigint
 1| AS $$DECLARE
 1|     cmd text;
 1|     retval bigint;
 1| BEGIN
 1|     cmd := 'SELECT COUNT(*) FROM '
 1|            || quote_ident(tabname);
 1|
 1|     EXECUTE cmd INTO retval;
 1|     RETURN retval;
 1| END;
 1| $$ LANGUAGE plpgsql STABLE;

 1| CREATE FUNCTION


Получим идентификатор сеанса отладки и OID функции:

 1| => select pldbg_create_listener() as sessionid,
 1|          'get_count'::regproc::oid as funcoid \gset


 1| => \echo :sessionid :funcoid

 1| 1 115750


Установим точку останова в начале функции (3-й параметр: -1) и ждем вызова функции из любого (4-й параметр: NULL) сеанса:

 1| => select * from pldbg_set_global_breakpoint(:sessionid, :funcoid, -1, NULL);

 1|  pldbg_set_global_breakpoint
 1| -----------------------------
 1|  t
 1| (1 row)
 1|

 1| => select * from pldbg_wait_for_target(:sessionid);


Отладочный сеанс перешел в режим ожидания...

Откроем второй сеанс и вызываем функцию:

 2|          => select get_count('pg_class');

Выполнение доходит до точки останова и теперь второй сеанс переходит в режим ожидания команд отладчика.

Первый сеанс просыпается и переходит в режим управления отладкой:

 1|  pldbg_wait_for_target
 1| -----------------------
 1|                  21413
 1| (1 row)


В режиме управления можно:
  • проверить стек вызова (pldbg_get_stack),
  • перемещаться по функциям в стеке (pldbg_select_frame),
  • смотреть значения параметров и локальных переменных текущей функции в стеке (pldbg_get_variables),
  • изменить значения переменных (pldbg_deposit_value),
  • установить/удалить точки останова (pldbg_set_breakpoint/pldbg_drop_breakpoint),
  • продолжить выполнение до следующей точки (pldbg_continue),
  • выполнять пошаговую отладку с заходом внутрь функций (pldbg_step_into),
  • или выполняя строку целиком (pldbg_step_over),
  • в конечном итоге, прекратить сеанс отладки (pldbg_abort_target).
Посмотрим, например,  значения переменных:

 1| => select name, varClass, value, pg_catalog.format_type(dtype, NULL) as dtype
 1|       from   pldbg_get_variables(:sessionid)
 1|       order by varClass;

 1|   name   | varclass |  value   | dtype 
 1| ---------+----------+----------+--------
 1|  tabname | A        | pg_class | text
 1|  cmd     | L        | NULL     | text
 1|  retval  | L        | NULL     | bigint
 1| (3 rows)


Видим, что параметр функции tabname = 'pg_class', а значения переменных cmd и retval еще не установлены.

Добавим еще одну точку останова на команде EXECUTE (8 строка функции):

 1| => select * from pldbg_set_breakpoint(:sessionid, :funcoid, 8);

 1|  pldbg_set_breakpoint
 1| ----------------------
 1|  t
 1| (1 row)


Продолжим выполнение до новой точки останова:

 1| => select p.targetName,
 1|             (select string_agg(t.n::text||
 1|                        case when t.n=p.linenumber-1 then '*' else '' end,
 1|                        chr(10)) from generate_series(1, 11) as t(n)) as line,
 1|             pldbg_get_source(:sessionid, p.func) AS src
 1| from pldbg_continue(:sessionid) p;

 1|    targetname    | line |                 src                
 1| -----------------+------+-------------------------------------
 1|  get_count(text) | 1   +| DECLARE                            +
 1|                  | 2   +|     cmd text;                      +
 1|                  | 3   +|     retval bigint;                 +
 1|                  | 4   +| BEGIN                              +
 1|                  | 5   +|     cmd := 'SELECT COUNT(*) FROM ' +
 1|                  | 6   +|            || quote_ident(tabname);+
 1|                  | 7   +|                                    +
 1|                  | 8*  +|     EXECUTE cmd INTO retval;       +
 1|                  | 9   +|     RETURN retval;                 +
 1|                  | 10  +| END;                               +
 1|                  | 11   |
 1| (1 row)


Еще раз посмотрим значения локальных переменных:

 1| => select name, varClass, value, pg_catalog.format_type(dtype, NULL) as dtype
 1|       from   pldbg_get_variables(:sessionid)
 1|       order by varClass;

 1|   name   | varclass |             value             | dtype 
 1| ---------+----------+-------------------------------+--------
 1|  tabname | A        | pg_class                      | text
 1|  cmd     | L        | SELECT COUNT(*) FROM pg_class | text
 1|  retval  | L        | NULL                          | bigint
 1| (3 rows)


Видим, что переменной cmd присвоен текст команды SELECT.

Выполним следующую строку функции:

 1| => select s.targetname, s.linenumber-1 as line
 1|       from pldbg_step_over(:sessionid) s;

 1|    targetname    | line
 1| -----------------+------
 1|  get_count(text) |    9
 1| (1 row)


И  еще раз посмотрим значения локальных переменных:

 1| => select name, varClass, value, pg_catalog.format_type(dtype, NULL) as dtype
 1|       from   pldbg_get_variables(:sessionid)
 1|       order by varClass;

 1|   name   | varclass |             value             | dtype 
 1| ---------+----------+-------------------------------+--------
 1|  tabname | A        | pg_class                      | text
 1|  cmd     | L        | SELECT COUNT(*) FROM pg_class | text
 1|  retval  | L        | 321                           | bigint
 1| (3 rows)


Команда SELECT выполнена, в переменную retval записан результат.

Прекратим сеанс отладки:

 1| => select * from pldbg_abort_target(:sessionid);

 1|  pldbg_abort_target
 1| --------------------
 1|  t
 1| (1 row)


 2|          ERROR:  canceling statement due to user request
 2|          CONTEXT:  PL/pgSQL function get_count(text) line 9 at RETURN


Выполнение функции во втором сеансе завершается ошибкой.

Итоги

Отладчик PL/pgSQL работает, его API можно использовать без pgAdmin. 
Осталось дождаться, когда найдется умелец, который возьмет и сделает удобный графический интерфейс, лишенный недостатков, которыми столь "грешит" pgAdmin.

1 комментарий:

Andrewww комментирует...

https://github.com/dbeaver/dbeaver/wiki/PGDebugger