ON_ERROR_ROLLBACK

PostgreSQLВ очередной раз встретился с необходимостью использовать переменную утилиты psql, которая называется ON_ERROR_ROLLBACK. В прошлом году рассказывал про переменную ECHO_HIDDEN, настала пора на следующую посмотреть.

Это переменная утилиты psql, только в ней её можно установить.

\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.


Be the first to comment

Leave a Reply

Ваш Mail не будет опубликован.


*