Поднимите ошибку без отката в plpgsql / postgresql - PullRequest
1 голос
/ 01 апреля 2020

У меня есть две сохраненные функции. delete_item удаляет один элемент, регистрируя его успех или неудачу в таблице actionlog, возвращает 1 при ошибках и 0 при успешной работе.

Во-вторых, у меня есть другая функция remove_expired он находит, что нужно удалить, и перебирает его, вызывая delete_item.

Все это предназначено для вызова с использованием простого сценария bash (жесткое требование к операциям, поэтому такой вызов не обсуждается) и он должен выдавать код ошибки, когда вещи не работают для их инструментов отчетности.

Мы хотим, чтобы все возможные удаления были успешными (мы не ожидаем ошибок, но люди по-прежнему люди, и ошибки действительно происходит), поэтому, если мы хотим удалить 10 элементов, а 1 не удалось, мы по-прежнему хотим удалить остальные 9.

Во-вторых, нам бы очень хотелось, чтобы журналы были в таблице actionlog в успех и ошибка. То есть, мы хотим, чтобы этот журнал был завершен.

Поскольку функции plpg sql не позволяют ручное управление транзакциями, что, по-видимому, не вариант (если я не пропустил способ обойти это?).

Единственный способ, который я нашел для достижения этой цели, - это обернуть сценарии вокруг него вне plpg sql, но мы бы очень хотели, чтобы это было возможно в чистом plpg sql, поэтому мы можем просто выполнять операции команду pssql -C ..., и тогда они не должны беспокоиться ни о чем другом.

SQL, чтобы воспроизвести проблему:

DROP FUNCTION IF EXISTS remove_expired(timestamp with time zone);
DROP FUNCTION IF EXISTS delete_item(integer);

DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;

CREATE TABLE test (
    id         serial primary key       not null,
    t          timestamp with time zone not null
);

CREATE TABLE evil (
    test_id integer not null references test(id)
);

CREATE TABLE actionlog (
    eventTime timestamp with time zone not null default now(),
    message   text                     not null
);


INSERT INTO test (actualTime, t)
VALUES ('2020-04-01T10:00:00+0200'),
       ('2020-04-01T10:15:00+0200'), -- Will not be deleable due to foreign key
       ('2020-04-01T10:30:00+0200')
;

INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;


CREATE OR REPLACE FUNCTION remove_expired(timestamp with time zone)
    RETURNS void
AS
$$
DECLARE
    test_id int;
    failure_count int = 0;
BEGIN
    FOR test_id IN
        SELECT id FROM test WHERE t < $1
    LOOP
        failure_count := delete_item(test_id) + failure_count;
    END LOOP;

    IF failure_count > 0 THEN
        -- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
        RAISE 'There was one or more errors deleting. See the log for details';
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION delete_item(integer)
    RETURNS integer
AS
$$
BEGIN
    DELETE FROM test WHERE id = $1;
    INSERT INTO actionlog (message)
        VALUES ('Deleted with ID: ' || $1);
    RETURN 0;
EXCEPTION WHEN OTHERS THEN
    INSERT INTO actionlog (message)
        VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
    RETURN 1;
END
$$ LANGUAGE plpgsql;

Заранее благодарим за любой полезный ввод

1 Ответ

1 голос
/ 01 апреля 2020

Вы можете получить что-то близкое к тому, что вы ожидаете в PostgreSQL 11 или PostgreSQL 12, но только с процедурами, потому что, как уже было сказано, функции всегда будут откатывать все назад в случае ошибок.

С:

DROP PROCEDURE IF EXISTS remove_expired(timestamp with time zone);
DROP PROCEDURE IF EXISTS delete_item(integer);
DROP FUNCTION  IF EXISTS f_removed_expired;
DROP SEQUENCE  IF EXISTS failure_count_seq;

DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;

CREATE TABLE test (
    id         serial primary key       not null,
    t          timestamp with time zone not null
);

CREATE TABLE evil (
    test_id integer not null references test(id)
);

CREATE TABLE actionlog (
    eventTime timestamp with time zone not null default now(),
    message   text                     not null
);


INSERT INTO test (t)
VALUES ('2020-04-01T10:00:00+0200'),
       ('2020-04-01T10:15:00+0200'), -- Will not be removed due to foreign key
       ('2020-04-01T10:30:00+0200')
;

select * from test where t < current_timestamp;

INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;

CREATE SEQUENCE failure_count_seq MINVALUE 0;
SELECT SETVAL('failure_count_seq', 0, true);

CREATE OR REPLACE PROCEDURE remove_expired(timestamp with time zone)
AS
$$
DECLARE
    test_id int;
    failure_count int = 0;
    return_code int;
BEGIN
    FOR test_id IN
        SELECT id FROM test WHERE t < $1
    LOOP
        call delete_item(test_id);
        COMMIT;
    END LOOP;

    SELECT currval('failure_count_seq') INTO failure_count; 
    IF failure_count > 0 THEN
        -- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
        RAISE 'There was one or more errors deleting. See the log for details';
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE PROCEDURE delete_item(in integer)
AS
$$
DECLARE 
 forget_value int;
BEGIN
    DELETE FROM test WHERE id = $1;
    INSERT INTO actionlog (message)
        VALUES ('Deleted with ID: ' || $1);
EXCEPTION WHEN OTHERS THEN
    INSERT INTO actionlog (message)
        VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
    COMMIT;
    SELECT NEXTVAL('failure_count_seq') INTO forget_value;
END
$$ LANGUAGE plpgsql;
--

Я получаю:

select * from test;
 id |           t            
----+------------------------
  1 | 2020-04-01 10:00:00+02
  2 | 2020-04-01 10:15:00+02
  3 | 2020-04-01 10:30:00+02
(3 rows)

select current_timestamp;
       current_timestamp       
-------------------------------
 2020-04-01 16:52:26.171975+02
(1 row)

call remove_expired(current_timestamp);
psql:test.sql:80: ERROR:  There was one or more errors deleting. See the log for details
CONTEXT:  PL/pgSQL function remove_expired(timestamp with time zone) line 17 at RAISE
select currval('failure_count_seq');
 currval 
---------
       1
(1 row)

select * from test;
 id |           t            
----+------------------------
  2 | 2020-04-01 10:15:00+02
(1 row)

select * from actionlog;
           eventtime           |                                                                  message                                                                  
-------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------
 2020-04-01 16:52:26.172173+02 | Deleted with ID: 1
 2020-04-01 16:52:26.179794+02 | Error deleting ID: 2. The error was: update or delete on table "test" violates foreign key constraint "evil_test_id_fkey" on table "evil"
 2020-04-01 16:52:26.196503+02 | Deleted with ID: 3
(3 rows)

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...