Skip to content

Commit c6c020a

Browse files
authored
fix savepoint with auto-rollback (#62)
1 parent 9e847ec commit c6c020a

File tree

5 files changed

+67
-3
lines changed

5 files changed

+67
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pg_doorman"
3-
version = "2.4.2"
3+
version = "2.4.3"
44
edition = "2021"
55
rust-version = "1.87.0"
66
license = "MIT"

documentation/docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ title: Changelog
44

55
# Changelog
66

7+
### 2.4.3 <small>Nov 15, 2025</small> { id="2.4.3" }
8+
9+
**Bug Fixes:**
10+
- Fixed handling of nested transactions via `SAVEPOINT`: auto-rollback now correctly rolls back to the savepoint instead of breaking the outer transaction. This prevents clients from getting stuck in an inconsistent transactional state.
11+
12+
713
### 2.4.2 <small>Nov 13, 2025</small> { id="2.4.2" }
814

915
**Improvements:**

src/server.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ pub struct Server {
315315
/// Is the server inside a transaction and aborted.
316316
is_aborted: bool,
317317

318+
/// Nested transaction level, e.g. if we're in a transaction, then we're in level > 1.
319+
nested_transaction_level: i32,
320+
318321
/// Is there more data for the client to read.
319322
data_available: bool,
320323

@@ -524,12 +527,14 @@ impl Server {
524527
match transaction_state {
525528
// In transaction.
526529
'T' => {
530+
self.nested_transaction_level += 1;
527531
self.is_aborted = false;
528532
self.in_transaction = true;
529533
}
530534

531535
// Idle, transaction over.
532536
'I' => {
537+
self.nested_transaction_level -= 1;
533538
self.is_aborted = false;
534539
self.in_transaction = false;
535540
}
@@ -845,9 +850,11 @@ impl Server {
845850
self.in_transaction
846851
}
847852

853+
/// If the server is in a transaction and the transaction was aborted.
854+
/// If the client disconnects while the server is in a transaction, we will clean it up.
848855
#[inline(always)]
849856
pub fn in_aborted(&self) -> bool {
850-
self.in_transaction && self.is_aborted
857+
self.in_transaction && self.is_aborted && self.nested_transaction_level == 1
851858
}
852859

853860
#[inline(always)]
@@ -929,6 +936,9 @@ impl Server {
929936
}
930937
self.cleanup_state.reset();
931938
}
939+
self.nested_transaction_level = 0;
940+
self.in_transaction = false;
941+
self.is_aborted = false;
932942
Ok(())
933943
}
934944

@@ -1578,6 +1588,7 @@ impl Server {
15781588
secret_key,
15791589
in_transaction: false,
15801590
is_aborted: false,
1591+
nested_transaction_level: 0,
15811592
in_copy_mode: false,
15821593
data_available: false,
15831594
bad: false,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package doorman_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"os"
7+
"testing"
8+
9+
"github.com/jackc/pgx/v4"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_RollbackSavePoint(t *testing.T) {
14+
ctx := context.Background()
15+
session, errOpen := pgx.Connect(ctx, os.Getenv("DATABASE_URL_ROLLBACK"))
16+
assert.NoError(t, errOpen)
17+
tx, errTx := session.Begin(ctx)
18+
assert.NoError(t, errTx)
19+
tx.Exec(ctx, `select 1`)
20+
checkIdleInTx(t)
21+
_, err := tx.Exec(ctx, `
22+
create table test_savepoint(id serial primary key, value integer);
23+
insert into test_savepoint(value) values (1);
24+
savepoint sp;
25+
insert into test_savepoint(value) values (2);
26+
`)
27+
assert.NoError(t, err)
28+
_, err = tx.Exec(ctx, `select * from test_savepoint_unknown;`)
29+
assert.Error(t, err)
30+
_, err = tx.Exec(ctx, `ROLLBACK TO SAVEPOINT sp;`)
31+
assert.NoError(t, err)
32+
checkIdleInTx(t)
33+
var count int
34+
assert.NoError(t, tx.QueryRow(ctx, `select count(*) from test_savepoint;`).Scan(&count))
35+
assert.Equal(t, count, 1)
36+
_, err = tx.Exec(ctx, `ROLLback /*kek*/;`)
37+
assert.NoError(t, err)
38+
}
39+
40+
func checkIdleInTx(t *testing.T) {
41+
db, errOpenNew := sql.Open("postgres", os.Getenv("DATABASE_URL"))
42+
assert.NoError(t, errOpenNew)
43+
var count int
44+
assert.NoError(t, db.QueryRow("select count(*) from pg_stat_activity where usename = 'example_user_rollback' and state = 'idle in transaction'").Scan(&count))
45+
assert.Equal(t, 1, count)
46+
_ = db.Close()
47+
}

0 commit comments

Comments
 (0)