Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,5 @@ Version 3.4.0 - 2026 ???
- Fix Column::operator<< to stream the exact column bytes via getString() (#556)
- Restore the Coverity Scan static analysis as a GitHub Actions workflow, replacing the old Travis CI job
- Fix Database::getHeaderInfo() signed-shift UB and use fixed-width types for the Header struct (#558)
- Fix Savepoint destructor to catch all exceptions and track rollback state to avoid std::terminate (#559)
- Fix Transaction destructor to catch all exceptions to avoid std::terminate (#559)
7 changes: 4 additions & 3 deletions include/SQLiteCpp/Savepoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ class SQLITECPP_API Savepoint
void rollback() { rollbackTo(); }

private:
Database& mDatabase; ///< Reference to the SQLite Database Connection
std::string msName; ///< Name of the Savepoint
bool mbReleased = false; ///< True when release has been called
Database& mDatabase; ///< Reference to the SQLite Database Connection
std::string msName; ///< Name of the Savepoint
bool mbReleased = false; ///< True when release has been called
bool mbRolledBack = false; ///< True when a rollback to the savepoint has been done
};

} // namespace SQLite
8 changes: 6 additions & 2 deletions src/Savepoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ Savepoint::~Savepoint()
{
try
{
rollback();
if (!mbRolledBack)
{
rollbackTo();
}
release();
}
catch (SQLite::Exception&)
catch (...)
{
// Never throw an exception in a destructor: error if already released,
// but no harm is caused by this.
Expand Down Expand Up @@ -71,6 +74,7 @@ void Savepoint::rollbackTo()
if (!mbReleased)
{
mDatabase.exec(std::string("ROLLBACK TO SAVEPOINT ") + msName);
mbRolledBack = true;
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Transaction::~Transaction()
{
mDatabase.exec("ROLLBACK TRANSACTION");
}
catch (SQLite::Exception&)
catch (...)
{
// Never throw an exception in a destructor: error if already rollbacked, but no harm is caused by this.
}
Expand Down
25 changes: 25 additions & 0 deletions tests/Savepoint_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ TEST(Savepoint, commitRollback)
EXPECT_EQ(1, nbRows);
}

TEST(Savepoint, rollbackToThenRelease)
{
// Create a new database
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
EXPECT_EQ(SQLite::OK, db.getErrorCode());
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");

{
SQLite::Savepoint savepoint(db, "sp");

EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'rolled back')"));

// A manual rollback leaves the savepoint on the stack, so releasing it afterwards succeeds.
savepoint.rollbackTo();
EXPECT_NO_THROW(savepoint.release());

// end of scope: already released, the destructor must do nothing and not throw
}

// The rolled-back insert must not be persisted
SQLite::Statement query(db, "SELECT COUNT(*) FROM test");
ASSERT_TRUE(query.executeStep());
EXPECT_EQ(0, query.getColumn(0).getInt());
}

TEST(Savepoint, destructorSwallowsException)
{
// Create a new database
Expand Down
Loading