18

I'm writing a script for PostgreSQL and since I want it to be executed atomically, I'm wrapping it inside a transaction.
I expected the script to look something like this:

BEGIN
-- 1) Execute some valid actions;
-- 2) Execute some action that causes an error.
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
END; -- A.k.a. COMMIT;

However, in this case pgAdmin warns me about a syntax error right after the initial BEGIN. If I terminate the command there by appending a semicolon like so: BEGIN; it instead informs me about error near EXCEPTION.
I realize that perhaps I'm mixing up syntax for control structures and transactions, however I couldn't find any mention of how to roll back a failed transaction in the docs (nor in SO for that matter).

I also considered that perhaps the transaction is rolled back automatically on error, but it doesn't seem to be the case since the following script:

BEGIN;
-- 1) Execute some valid actions;
-- 2) Execute some action that causes an error.
COMMIT;

warns me that: ERROR: current transaction is aborted, commands ignored until end of transaction block and I have to then manually ROLLBACK; the transaction.

It seems I'm missing something fundamental here, but what?

EDIT:
I tried using DO as well like so:

DO $$
BEGIN
-- 1) Execute some valid actions;
-- 2) Execute some action that causes an error.
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
END; $$

pgAdmin hits me back with a: ERROR: cannot begin/end transactions in PL/pgSQL. HINT: Use a BEGIN block with an EXCEPTION clause instead. which confuses me to no end, because that is exactly what I am (I think) doing.

POST-ACCEPT EDIT: Regarding Laurenz's comment: "Your SQL script would contain a COMMIT. That ends the transaction and rolls it back." - this is not the behavior that I observe. Please consider the following example (which is just a concrete version of an example I already provided in my original question):

BEGIN;

-- Just a simple, self-referencing table.
CREATE TABLE "Dummy" (
    "Id" INT GENERATED ALWAYS AS IDENTITY,
    "ParentId" INT NULL,
    CONSTRAINT "PK_Dummy" PRIMARY KEY ("Id"),
    CONSTRAINT "FK_Dummy_Dummy" FOREIGN KEY ("ParentId") REFERENCES "Dummy" ("Id")
);

-- Foreign key violation terminates the transaction.
INSERT INTO "Dummy" ("ParentId")
VALUES (99);

COMMIT;

When I execute the script above, I'm greeted with: ERROR: insert or update on table "Dummy" violates foreign key constraint "FK_Dummy_Dummy". DETAIL: Key (ParentId)=(99) is not present in table "Dummy". which is as expected. However, if I then try to check whether my Dummy table was created or rolled back like so:

SELECT EXISTS (
    SELECT FROM information_schema."tables"
    WHERE "table_name" = 'Dummy');

instead of a simple false, I get the same error that I already mentioned twice: ERROR: current transaction is aborted, commands ignored until end of transaction block. Then I have to manually terminate the transaction via issuing ROLLBACK;.

So to me it seems that either the comment mentioned above is false or at least I'm heavily misinterpreting something here.

Marchyello
  • 621
  • 2
  • 7
  • 18
  • `EXCEPTION` can only be used in PL/pgSQL, not in SQL If you want to use that, you need to use a [`do`](https://www.postgresql.org/docs/current/sql-do.html) block –  Aug 19 '20 at 15:10
  • @a_horse_with_no_name I added an edit regarding your comment if you care to take a look. – Marchyello Aug 19 '20 at 15:26
  • @a_horse_with_no_name also I wouldn't say I *want* to use `EXCEPTION`. What I want is to roll back the transaction on an error. Perhaps there are other ways around that that I'm missing? – Marchyello Aug 19 '20 at 15:30
  • I see that this question has attracted some interest over time. I actually have been lucky enough to (with reasonably high confidence) get to the bottom of this. What I have had less luck with though, is to find myself some time to muster up a proper, detailed-enough answer. Will do my best to get to it in the next 2-3 months (so until August 1st 2021). – Marchyello May 01 '21 at 13:20
  • I fully understand your question as I'm experiencing this problem as well. This doesn't work: [dbfiddle](https://dbfiddle.uk/?rdbms=postgres_13&fiddle=411b13b0d28e0fab6837e4e44e43b57b). But this works: [dbfiddle](https://dbfiddle.uk/?rdbms=postgres_13&fiddle=1f423884ccb93397dbde4ea84a08bcdc). Although I would need to test this on a real PostgreSQL setup to confirm. My goal is to write an API without using ORMs and I wouldn't like to have to issue a 2nd query to rollback the 1st one if needed. – GuiRitter Aug 03 '21 at 18:08
  • I tested it on a real PostgreSQL setup to confirm. I'm using [pg](https://www.npmjs.com/package/pg) to run single queries using a pool. The 1st query ends with a `COMMIT;`, fails and gets rolled back automatically. The 2nd query is a common `SELECT` and works. – GuiRitter Aug 13 '21 at 13:08

2 Answers2

14

You cannot use ROLLBACK in PL/pgSQL, except in certain limited cases inside procedures.

You don't need to explicitly roll back in your PL/pgSQL code. Just let the exception propagate out of the PL/pgSQL code, and it will cause an error, which will cause the whole transaction to be rolled back.

Your comments suggest that this code is called from an SQL script. Then the solution would be to have a COMMIT in that SQL script at some place after the PL/pgSQL code. That would end the transaction and roll it back.

Laurenz Albe
  • 209,280
  • 17
  • 206
  • 263
  • 2
    Okay, but if that is the case then why do I (and I mention this in my question) get: `ERROR: current transaction is aborted, commands ignored until end of transaction block` and have to manually `ROLLBACK` the transaction to be able to execute any further commands? – Marchyello Aug 19 '20 at 19:01
  • 1
    @Marchyello - Postgres run ROLLBACK implicitly when you use EXCEPTION block. – Pavel Stehule Aug 20 '20 at 04:23
  • You need to roll back to tell PostgreSQL that the transaction has ended. Otherwise, if PostgreSQL implicitly ended the transaction, the parts of the transaction after the error would be executed, which would violate the all-or-nothing atomicity principle. So yes, you have to roll back, but you cannot do that in the PL/pgSQL code: the whole PL/pgSQL code is part of a single transaction. – Laurenz Albe Aug 20 '20 at 05:26
  • @LaurenzAlbe Sorry, but the main issue is still unclear to me - how do I rollback a transaction as soon as the first error was encountered? You say that I need to let the exception propogate outside of my PL/pgSQL code. But propogate to where? Do I need an exception catching wrapper around my transaction? To me that doesn't make sense, because that would mean I'm already outside the scope of the transaction, but perhaps I'm wrong in this assumption. If I may add I find it odd that such a (imo) common scenario isn't documented and cannot be explained with a simple example. – Marchyello Aug 20 '20 at 09:40
  • 2
    You cannot end the transaction in the PL/pgSQL code. Your PL/pgSQL code is called from somewhere. That's the place where you need to perform `ROLLBACK`. – Laurenz Albe Aug 20 '20 at 09:51
  • @LaurenzAlbe Thank you for your advice, I accepted your answer (mostly based on your clarifying comments). I really appreciate a person taking the time out of their day to help a total stranger. However, if I may, I have one last clarification in mind regarding this. In this scenario it's not some back-end executing this transaction, it's a "standalone" SQL script. What would be that *"somewhere"* that it is called from in this particular case? Would that be just some SQL wrapper code around the transaction (as I mention in my previous comment)? – Marchyello Aug 20 '20 at 12:52
  • Your SQL script would contain a `COMMIT`. That ends the transaction and rolls it back. – Laurenz Albe Aug 20 '20 at 12:58
  • 1
    @LaurenzAlbe "[..] That ends the transaction and rolls it back" - unfortunately this is not what I see. If you still have some time and patience, take a look at my latest edit, where I provide a concrete example of this contradiction. – Marchyello Aug 20 '20 at 14:00
  • How exactly do you execute that SQL script? – Laurenz Albe Aug 20 '20 at 14:10
  • @LaurenzAlbe via the Query Tool in pgAdmin4 – Marchyello Aug 20 '20 at 14:11
  • Then I have no explanation. A commit in an SQL script should be executed like any other statement. I never use pgAdmin, so I cannot say more. – Laurenz Albe Aug 21 '20 at 06:50
  • @Marchyello You're right, it looks like end/commit is not being executed automatically in pgAdmin4. It's working fine in my application code though, there's no need to manually call end/rollback there (I'm using Dapper). Did you find a fix for this?, maybe there's a configuration setting or something? (or is it a bug perhaps?) – Raúl Bojalil Dec 12 '20 at 03:56
-1

I think you must be using an older version, as the exact code from your question works without error for me:

(The above is with PostgreSQL 13.1, and pgAdmin 4.28.)

It also works fine for me, without the exception block:

As per this comment, you can remove the exception block within a function, and if an error occurs, the transaction run within it will automatically be rolled back. That appears to be the case, from my limited testing.

Venryx
  • 15,624
  • 10
  • 70
  • 96