I know this is a very old ticket but (as of 2017) PostgreSQL still have this same behavior of auto-rolling back itself when something goes wrong in the commit. I'd like to share some thoughts here.
I don't know if we can change this behavior, and I don't need this, maybe for the best of delegating PostgreSQL to manage the rollback for us (he knows what he is doing, right ?). Rolling back means changing the data back to its original state before the failed transaction, that means altered or inserted data from triggers will also be discarded. In an ACID logic, this is what we want. Let say you are managing the rollback on the back-end yourself, if something goes wrong during your custom rollback or if the database is changed at the same time from external transactions during your rollback, the data becomes inconsistent and your whole structure most likely to collapse.
So knowing that PostgreSQL will manage its own rollback strategy, the question to ask is "how can I extend the rollback strategy ?". The thing you first should think of is "what caused the transaction to fail ?". In your try/catch structure, try to handle all the possible exceptions and run the transaction again or send feedback to the front-end application with some appropriate "don't do" messages. For me, this is the best way of handling things, it is less code, less overhead, more control, more user-friendly and your database will thank you.
A last point I want to shed light on, SQL standard is having a sqlstate code that can be use to communicate with back-end modules.
The failing operation during a transaction will return a sqlstate code, you can then use these codes to make appropriate drawbacks. You can make your own sqlstate codes, as long as it doesn't mess with the reserved ones (https://www.postgresql.org/message-id/20185.1359219138%40sss.pgh.pa.us).
For instance in a plpgsql function
...
$$
begin
...do something...if it goes wrong
raise exception 'custom exception message' using errcode='12345';
end
$$
...
This is a example using PDO in PHP (using the error code above) :
...
$pdo->beginTransaction();
try {
$s = $pdo->prepare('...');
$s->execute([$value]);
/**
* Simulate a null violation exception
* If it fails, PDO will not wait the commit
* and will throw the exception, the code below
* is not executed.
*/
$s->execute([null]);
/**
* If nothing wrong happened, we commit to change
* the database state.
*/
$pdo->commit();
}
catch (PDOException $e) {
/**
* It is important to also have the commit here.
* It will trigger PostgreSQL rollback.
* And make the pdo Object back to auto-commit mode.
*/
$pdo->commit();
if ($e->getCode() === '12345') {
send_feedback_to_client('please do not hack us.');
}
}
...