В очередной раз встретился с необходимостью использовать переменную утилиты psql, которая называется ON_ERROR_ROLLBACK. В прошлом году рассказывал про переменную ECHO_HIDDEN, настала пора на следующую посмотреть.
Это переменная утилиты psql, только в ней её можно установить.
- \set ON_ERROR_ROLLBACK OFF (по умолчанию)
- Демонстрация \set ON_ERROR_ROLLBACK ON
- Как работает ON_ERROR_ROLLBACK
- Эксперимент 1 (\set ON_ERROR_ROLLBACK OFF)
- Эксперимент 2 (\set ON_ERROR_ROLLBACK ON)
- Эксперимент 3 (\set ON_ERROR_ROLLBACK INTERACTIVE)
- Журнал сообщений сервера
- Исходный код
\set ON_ERROR_ROLLBACK OFF (по умолчанию)
Когда мы начинаем транзакцию в psql, внутри неё не нужно совершать ошибок-опечаток, иначе мы не сможем продолжить работу, транзакция оборвется и придётся начинать всё заново. Давай проиллюстрируем:
Для начала — создадим таблицу для экспериментов:
CREATE TABLE OER (id integer);
И выполним несколько команд:
student=# BEGIN; BEGIN student=*# SELECT n; ERROR: column "n" does not exist LINE 1: SELECT n; ^ student=!# SELECT 1; ERROR: current transaction is aborted, commands ignored until end of transaction block student=!# ROLLBACK; ROLLBACK
Обратите внимание на символы звездочка, восклицательный знак в окончании приглашения psql. Они помогают понять что к чему. Восклицательный знак означает что транзакция уже оборвана и продолжать нельзя.
Поведение это не всегда удобно, хотелось бы продолжать действия внутри транзакции даже после опечатки.
\set ON_ERROR_ROLLBACK ON
Это можно сделать установив переменную psql ON_ERROR_ROLLBACK следующим образом:
\set ON_ERROR_ROLLBACK ON
Повторим пример выше:
student=# BEGIN; BEGIN student=*# SELECT n; ERROR: column "n" does not exist LINE 1: SELECT n; ^ student=*# SELECT 1; ?column? ---------- 1 (1 row) student=*# ROLLBACK; ROLLBACK
Делаем опечатки, но продолжаем работу внутри транзакции. Отлично!
Как работает ON_ERROR_ROLLBACK
За счет чего происходит такая работа? В документации на утилиту psql сказано, что, при включенном ON_ERROR_ROLLBACK неявно выполняется команда SAVEPOINT непосредственно перед каждой командой в блоке транзакции, а в случае ошибки команды происходит откат к этой точке сохранения.
Вроде всё понятно, но я бы хотел акцентировать внимание вот на чём — чтобы можно было откатывать в рамках текущей транзакции, в ней создаётся несколько вложенных транзакций (subtransaction), которыми мы далее управляем. И вложенные транзакции имеют свой собственный номер. Он больше, чем номер основной транзакции. Вы можете это всё проверить самостоятельно, хороший пример есть в книге Егора Рогова «PostgreSQL 15 изнутри», раздел «3.7. Вложенные транзакции».
Но помните, что txid_current() показывает номер основной транзакции, но не вложенных. А номер вложенных будет отличаться от основной! Так что полагаться на txid_current() при работе с вложенными транзакциями может быть опасно.
Эксперимент 1 (\set ON_ERROR_ROLLBACK OFF)
Для иллюстрации выполним ещё пару экспериментов. Выключим эту переменную:
\set ON_ERROR_ROLLBACK OFF
И выполним вот такие команды (в моём psql строка приглашения имеет вид #. Да, просто шарп).
BEGIN; SELECT txid_current(); INSERT INTO oer VALUES (1),(2),(3); SELECT *, xmin FROM oer; SELECT txid_current(); INSERT INTO oer VALUES (11),(22),(33); SELECT *, xmin FROM oer; SELECT txid_current(); ROLLBACK;
Команды с результатом:
# BEGIN; BEGIN # SELECT txid_current(); txid_current -------------- 3098 (1 row) # INSERT INTO oer VALUES (1),(2),(3); INSERT 0 3 # SELECT *, xmin FROM oer; id | xmin ----+------ 1 | 3098 2 | 3098 3 | 3098 (3 rows) # SELECT txid_current(); txid_current -------------- 3098 (1 row) # INSERT INTO oer VALUES (11),(22),(33); INSERT 0 3 # SELECT *, xmin FROM oer; id | xmin ----+------ 1 | 3098 2 | 3098 3 | 3098 11 | 3098 22 | 3098 33 | 3098 (6 rows) # SELECT txid_current(); txid_current -------------- 3098 (1 row) # ROLLBACK; ROLLBACK
Эксперимент 2 (\set ON_ERROR_ROLLBACK ON)
А теперь опять включим переменную…:
\set ON_ERROR_ROLLBACK ON
… и повторим тот же эксперимент, только добавим ошибку в середине — чтобы показать что транзакция успешно продолжает выполнение.
BEGIN; SELECT txid_current(); INSERT INTO oer VALUES (1),(2),(3); SELECT *, xmin FROM oer; SELECT n; SELECT txid_current(); INSERT INTO oer VALUES (11),(22),(33); SELECT *, xmin FROM oer; SELECT txid_current(); ROLLBACK;
Команды с результатом:
# BEGIN; BEGIN # SELECT txid_current(); txid_current -------------- 3100 (1 row) # INSERT INTO oer VALUES (1),(2),(3); INSERT 0 3 # SELECT *, xmin FROM oer; id | xmin ----+------ 1 | 3101 2 | 3101 3 | 3101 (3 rows) # SELECT n; ERROR: column "n" does not exist LINE 1: SELECT n; # SELECT txid_current(); txid_current -------------- 3100 (1 row) # INSERT INTO oer VALUES (11),(22),(33); INSERT 0 3 # SELECT *, xmin FROM oer; id | xmin ----+------ 1 | 3101 2 | 3101 3 | 3101 11 | 3103 22 | 3103 33 | 3103 (6 rows) # SELECT txid_current(); txid_current -------------- 3100 (1 row) # ROLLBACK; ROLLBACK
Эксперимент 3 (\set ON_ERROR_ROLLBACK INTERACTIVE)
А теперь покажу что происходит с режимом INTERACTIVE. Включим этот режим работы:
\set ON_ERROR_ROLLBACK INTERACTIVE
И опять выполним в psql’e команды из второго эксперимента, там, где была ошибка в середине. Сейчас выполнение будет идентично, создаются вложенные транзакции.
А теперь создадим файл oer.sql и скопируем туда те же команды, из второго эксперимента.
BEGIN; SELECT txid_current(); INSERT INTO oer VALUES (1),(2),(3); SELECT *, xmin FROM oer; SELECT n; SELECT txid_current(); INSERT INTO oer VALUES (11),(22),(33); SELECT *, xmin FROM oer; SELECT txid_current(); ROLLBACK;
И выполним этот файл из psql с помощью метакоманды \i (либо \include). Эта команда читает и выполняет команды из переданного файла. И теперь результат будет следующим:
# \i oer.sql BEGIN txid_current -------------- 3126 (1 row) INSERT 0 3 id | xmin ----+------ 1 | 3126 2 | 3126 3 | 3126 (3 rows) psql:oer.sql:5: ERROR: column "n" does not exist LINE 1: SELECT n; ^ psql:oer.sql:6: ERROR: current transaction is aborted, commands ignored until end of transaction block psql:oer.sql:7: ERROR: current transaction is aborted, commands ignored until end of transaction block psql:oer.sql:8: ERROR: current transaction is aborted, commands ignored until end of transaction block psql:oer.sql:9: ERROR: current transaction is aborted, commands ignored until end of transaction block ROLLBACK
Теперь мы видим ошибку, так как режим ON_ERROR_ROLLBACK INTERACTIVE. Его можно будет установить по умолчанию (в .psqlrc), но обязательно нужно помнить про особенность отображения номера текущей транзакции.
Журнал сообщений сервера
А теперь я покажу что неявная точка сохранения точно добавляется. Достаточно заглянуть в журнал сообщений сервера — там эта информация будет записана явно:
BEGIN; SAVEPOINT pg_psql_temporary_savepoint SELECT txid_current(); RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint INSERT INTO oer VALUES (1),(2),(3); RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint SELECT *, xmin FROM oer; RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint column "n" does not exist at character 8 SELECT n; ROLLBACK TO pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint SELECT txid_current(); RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint INSERT INTO oer VALUES (11),(22),(33); RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint SELECT *, xmin FROM oer; RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint SELECT txid_current(); RELEASE pg_psql_temporary_savepoint SAVEPOINT pg_psql_temporary_savepoint ROLLBACK;
Перед каждой командой добавляется SAVEPOINT pg_psql_temporary_savepoint и после каждой команды происходит откат. Накладные расходы точно тут будут. Точки сохранения называются одинаково, но это и не принципиально — нам нужно откатиться только к предыдущему состоянию, а не куда-то ниже по стеку.
Исходный код
Ну и в самом конце — заглянем в исходный код (16_STABLE), в файл postgres/src/bin/psql/common.c:
if (transaction_status == PQTRANS_INTRANS && pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF && (pset.cur_cmd_interactive || pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON)) { PGresult *result; result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint"); if (PQresultStatus(result) != PGRES_COMMAND_OK) { pg_log_info("%s", PQerrorMessage(pset.db)); ClearOrSaveResult(result); goto sendquery_cleanup; } ClearOrSaveResult(result); on_error_rollback_savepoint = true; }
В условии, в том числе, проверяется и установка переменной ON_ERROR_ROLLBACK, далее добавляется точка сохранения («SAVEPOINT pg_psql_temporary_savepoint») и выполняется с помощью команды PQexec. Что мы и видим в логе. Ниже есть и кусочки про RELEASE pg_psql_temporary_savepoint и ROLLBACK TO pg_psql_temporary_savepoint.
Leave a Reply